From 0f659d1d6798d67e8cbed9c9a703cee21a4f4229 Mon Sep 17 00:00:00 2001 From: asalem Date: Tue, 11 Feb 2020 14:52:15 -0800 Subject: [PATCH 01/12] chore(ui): moved logic into thunk reducer --- ui/src/cells/actions/thunks.ts | 46 +++++++++++++++--- .../components/SaveAsCellForm.tsx | 47 +++++++------------ 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/ui/src/cells/actions/thunks.ts b/ui/src/cells/actions/thunks.ts index 69e28cfd2a8..5fe9639a944 100644 --- a/ui/src/cells/actions/thunks.ts +++ b/ui/src/cells/actions/thunks.ts @@ -2,8 +2,14 @@ import {normalize} from 'normalizr' // APIs -import * as api from 'src/client' -import * as dashAPI from 'src/dashboards/apis' +import { + getDashboard, + deleteDashboardsCell, + postDashboard, + postDashboardsCell, + putDashboardsCells, +} from 'src/client' +import {updateView} from 'src/dashboards/apis' // Schemas import { @@ -55,7 +61,7 @@ export const deleteCell = (dashboardID: string, cellID: string) => async ( ) await Promise.all([ - api.deleteDashboardsCell({dashboardID: dashboardID, cellID: cellID}), + deleteDashboardsCell({dashboardID: dashboardID, cellID: cellID}), dispatch(refreshDashboardVariableValues(dashboardID, views)), ]) @@ -81,7 +87,7 @@ export const createCellWithView = ( try { if (!dashboard) { - const resp = await api.getDashboard({dashboardID}) + const resp = await getDashboard({dashboardID}) if (resp.status !== 200) { throw new Error(resp.data.message) } @@ -98,7 +104,7 @@ export const createCellWithView = ( const cell: NewCell = getNewDashboardCell(state, dashboard, clonedCell) // Create the cell - const cellResp = await api.postDashboardsCell({dashboardID, data: cell}) + const cellResp = await postDashboardsCell({dashboardID, data: cell}) if (cellResp.status !== 201) { throw new Error(cellResp.data.message) @@ -107,7 +113,7 @@ export const createCellWithView = ( const cellID = cellResp.data.id // Create the view and associate it with the cell - const newView = await dashAPI.updateView(dashboardID, cellID, view) + const newView = await updateView(dashboardID, cellID, view) const normCell = normalize( {...cellResp.data, dashboardID}, @@ -129,11 +135,37 @@ export const createCellWithView = ( } } +export const createDashboardWithView = async ( + orgID: string, + dashboardName: string, + view: View +): Promise => { + try { + const newDashboard = { + orgID, + name: dashboardName, + cells: [], + } + + const resp = await postDashboard({data: newDashboard}) + + if (resp.status !== 201) { + throw new Error(resp.data.message) + } + + await createCellWithView(resp.data.id, view) + } catch (error) { + console.error(error) + notify(copy.cellAddFailed()) + throw error + } +} + export const updateCells = (dashboardID: string, cells: Cell[]) => async ( dispatch ): Promise => { try { - const resp = await api.putDashboardsCells({ + const resp = await putDashboardsCells({ dashboardID, data: cells, }) diff --git a/ui/src/dataExplorer/components/SaveAsCellForm.tsx b/ui/src/dataExplorer/components/SaveAsCellForm.tsx index c402fa3bd52..8b8e5652684 100644 --- a/ui/src/dataExplorer/components/SaveAsCellForm.tsx +++ b/ui/src/dataExplorer/components/SaveAsCellForm.tsx @@ -23,8 +23,10 @@ import { // Actions import {getDashboards} from 'src/dashboards/actions/thunks' -import {createCellWithView} from 'src/cells/actions/thunks' -import {postDashboard} from 'src/client' +import { + createCellWithView, + createDashboardWithView, +} from 'src/cells/actions/thunks' import {notify} from 'src/shared/actions/notifications' // Types @@ -53,6 +55,7 @@ interface StateProps { interface DispatchProps { onGetDashboards: typeof getDashboards onCreateCellWithView: typeof createCellWithView + onCreateDashboardWithView: typeof createDashboardWithView notify: typeof notify } @@ -164,7 +167,15 @@ class SaveAsCellForm extends PureComponent { } private handleSubmit = () => { - const {onCreateCellWithView, dashboards, view, dismiss, notify} = this.props + const { + onCreateCellWithView, + onCreateDashboardWithView, + dashboards, + view, + dismiss, + notify, + orgID, + } = this.props const {targetDashboardIDs} = this.state const cellName = this.state.cellName || DEFAULT_CELL_NAME @@ -178,8 +189,8 @@ class SaveAsCellForm extends PureComponent { let targetDashboardName = '' try { if (dashID === DashboardTemplate.id) { - targetDashboardName = newDashboardName - this.handleCreateDashboardWithView(newDashboardName, viewWithProps) + targetDashboardName = newDashboardName || DEFAULT_DASHBOARD_NAME + onCreateDashboardWithView(orgID, newDashboardName, viewWithProps) } else { const selectedDashboard = dashboards.find(d => d.id === dashID) targetDashboardName = selectedDashboard.name @@ -196,31 +207,6 @@ class SaveAsCellForm extends PureComponent { } } - private handleCreateDashboardWithView = async ( - dashboardName: string, - view: View - ): Promise => { - const {onCreateCellWithView, orgID} = this.props - try { - const newDashboard = { - orgID, - name: dashboardName || DEFAULT_DASHBOARD_NAME, - cells: [], - } - - const resp = await postDashboard({data: newDashboard}) - - if (resp.status !== 201) { - throw new Error(resp.data.message) - } - - onCreateCellWithView(resp.data.id, view) - } catch (error) { - console.error(error) - throw error - } - } - private resetForm() { this.setState({ targetDashboardIDs: [], @@ -262,6 +248,7 @@ const mstp = (state: AppState): StateProps => { const mdtp: DispatchProps = { onGetDashboards: getDashboards, onCreateCellWithView: createCellWithView, + onCreateDashboardWithView: createDashboardWithView, notify, } From da583702071eac29a74124176398f7e42922eec6 Mon Sep 17 00:00:00 2001 From: asalem Date: Wed, 12 Feb 2020 16:44:31 -0800 Subject: [PATCH 02/12] feat(ui): added labels to buckets --- ui/cypress/e2e/buckets.test.ts | 14 +++ ui/src/buckets/actions/thunks.ts | 130 +++++++++++++++++++---- ui/src/buckets/components/BucketCard.tsx | 48 +++++++-- ui/src/buckets/components/BucketList.tsx | 11 +- ui/src/schemas/buckets.ts | 6 +- ui/src/shared/copy/notifications.ts | 10 ++ ui/src/types/buckets.ts | 12 ++- 7 files changed, 193 insertions(+), 38 deletions(-) diff --git a/ui/cypress/e2e/buckets.test.ts b/ui/cypress/e2e/buckets.test.ts index cdd06ac44f3..4d03db3bfb7 100644 --- a/ui/cypress/e2e/buckets.test.ts +++ b/ui/cypress/e2e/buckets.test.ts @@ -31,6 +31,20 @@ describe('Buckets', () => { }) cy.getByTestID(`bucket--card--name ${newBucket}`).should('exist') + + // Add a label + cy.getByTestID(`bucket--card ${newBucket}`).within(() => { + cy.getByTestID('inline-labels--add').click() + }) + + const labelName = 'l1' + cy.getByTestID('inline-labels--popover--contents').type(labelName) + cy.getByTestID('inline-labels--create-new').click() + cy.getByTestID('create-label-form--submit').click() + + // Delete the label + cy.getByTestID(`label--pill--delete ${labelName}`).click({force: true}) + cy.getByTestID('inline-labels--empty').should('exist') }) it("can update a bucket's retention rules", () => { diff --git a/ui/src/buckets/actions/thunks.ts b/ui/src/buckets/actions/thunks.ts index 6621b87689b..5c3a6efb3e6 100644 --- a/ui/src/buckets/actions/thunks.ts +++ b/ui/src/buckets/actions/thunks.ts @@ -9,7 +9,15 @@ import * as api from 'src/client' import {bucketSchema, arrayOfBuckets} from 'src/schemas' // Types -import {RemoteDataState, GetState, Bucket, BucketEntities} from 'src/types' +import { + AppState, + RemoteDataState, + GetState, + GenBucket, + Bucket, + BucketEntities, + Label, +} from 'src/types' // Utils import {getErrorMessage} from 'src/utils/api' @@ -35,7 +43,10 @@ import { bucketUpdateSuccess, bucketRenameSuccess, bucketRenameFailed, + addBucketLabelFailed, + removeBucketLabelFailed, } from 'src/shared/copy/notifications' +import {getLabels} from 'src/resources/selectors' type Action = BucketAction | NotifyAction @@ -59,8 +70,8 @@ export const getBuckets = () => async ( ) dispatch(setBuckets(RemoteDataState.Done, buckets)) - } catch (e) { - console.error(e) + } catch (error) { + console.error(error) dispatch(setBuckets(RemoteDataState.Error)) dispatch(notify(getBucketsFailed())) } @@ -93,13 +104,17 @@ export const createBucket = (bucket: Bucket) => async ( } } -export const updateBucket = (updatedBucket: Bucket) => async ( - dispatch: Dispatch +export const updateBucket = (bucket: Bucket) => async ( + dispatch: Dispatch, + getState: GetState ) => { try { + const state = getState() + const data = denormalizeBucket(state, bucket) + const resp = await api.patchBucket({ - bucketID: updatedBucket.id, - data: updatedBucket, + bucketID: bucket.id, + data, }) if (resp.status !== 200) { @@ -112,22 +127,25 @@ export const updateBucket = (updatedBucket: Bucket) => async ( ) dispatch(editBucket(newBucket)) - dispatch(notify(bucketUpdateSuccess(updatedBucket.name))) - } catch (e) { - console.error(e) - const message = getErrorMessage(e) + dispatch(notify(bucketUpdateSuccess(bucket.name))) + } catch (error) { + console.error(error) + const message = getErrorMessage(error) dispatch(notify(bucketUpdateFailed(message))) } } -export const renameBucket = ( - originalName: string, - updatedBucket: Bucket -) => async (dispatch: Dispatch) => { +export const renameBucket = (originalName: string, bucket: Bucket) => async ( + dispatch: Dispatch, + getState: GetState +) => { try { + const state = getState() + const data = denormalizeBucket(state, bucket) + const resp = await api.patchBucket({ - bucketID: updatedBucket.id, - data: updatedBucket, + bucketID: bucket.id, + data, }) if (resp.status !== 200) { @@ -140,9 +158,9 @@ export const renameBucket = ( ) dispatch(editBucket(newBucket)) - dispatch(notify(bucketRenameSuccess(updatedBucket.name))) - } catch (e) { - console.error(e) + dispatch(notify(bucketRenameSuccess(bucket.name))) + } catch (error) { + console.error(error) dispatch(notify(bucketRenameFailed(originalName))) } } @@ -159,8 +177,76 @@ export const deleteBucket = (id: string, name: string) => async ( dispatch(removeBucket(id)) dispatch(checkBucketLimits()) - } catch (e) { - console.error(e) + } catch (error) { + console.error(error) dispatch(notify(bucketDeleteFailed(name))) } } + +export const addBucketLabel = (bucketID: string, label: Label) => async ( + dispatch: Dispatch +): Promise => { + try { + const postResp = await api.postBucketsLabel({ + bucketID, + data: {labelID: label.id}, + }) + + if (postResp.status !== 201) { + throw new Error(postResp.data.message) + } + + const resp = await api.getBucket({bucketID}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const newBucket = normalize( + resp.data, + bucketSchema + ) + + dispatch(editBucket(newBucket)) + } catch (error) { + console.error(error) + dispatch(notify(addBucketLabelFailed())) + } +} + +export const deleteBucketLabel = (bucketID: string, label: Label) => async ( + dispatch: Dispatch +): Promise => { + try { + const deleteResp = await api.deleteBucketsLabel({ + bucketID, + labelID: label.id, + }) + if (deleteResp.status !== 204) { + throw new Error(deleteResp.data.message) + } + + const resp = await api.getBucket({bucketID}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const newBucket = normalize( + resp.data, + bucketSchema + ) + + dispatch(editBucket(newBucket)) + } catch (error) { + console.error(error) + dispatch(notify(removeBucketLabelFailed())) + } +} + +const denormalizeBucket = (state: AppState, bucket: Bucket): GenBucket => { + const labels = getLabels(state, bucket.labels) + return { + ...bucket, + labels, + } +} diff --git a/ui/src/buckets/components/BucketCard.tsx b/ui/src/buckets/components/BucketCard.tsx index d221564a361..d8b4233a354 100644 --- a/ui/src/buckets/components/BucketCard.tsx +++ b/ui/src/buckets/components/BucketCard.tsx @@ -1,6 +1,7 @@ // Libraries import React, {PureComponent} from 'react' import {withRouter, WithRouterProps} from 'react-router' +import {connect} from 'react-redux' import _ from 'lodash' // Components @@ -13,19 +14,28 @@ import { } from '@influxdata/clockface' import BucketContextMenu from 'src/buckets/components/BucketContextMenu' import BucketAddDataButton from 'src/buckets/components/BucketAddDataButton' +import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels' import {FeatureFlag} from 'src/shared/utils/featureFlag' // Constants import {isSystemBucket} from 'src/buckets/constants/index' // Types -import {Bucket} from 'src/types' +import {Bucket, Label} from 'src/types' import {DataLoaderType} from 'src/types/dataLoaders' +// Actions +import {addBucketLabel, deleteBucketLabel} from 'src/buckets/actions/thunks' + export interface PrettyBucket extends Bucket { ruleString: string } +interface DispatchProps { + onAddBucketLabel: typeof addBucketLabel + onDeleteBucketLabel: typeof deleteBucketLabel +} + interface Props { bucket: PrettyBucket onEditBucket: (b: PrettyBucket) => void @@ -36,12 +46,12 @@ interface Props { onFilterChange: (searchTerm: string) => void } -class BucketRow extends PureComponent { +class BucketRow extends PureComponent { public render() { const {bucket, onDeleteBucket} = this.props return ( { } private get actionButtons(): JSX.Element { - const {bucket} = this.props + const {bucket, onFilterChange} = this.props if (bucket.type === 'user') { return ( { margin={ComponentSize.Small} style={{marginTop: '4px'}} > + {