From 6c7a61e8387099768e9fffaa4c3532f6675becc0 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 15 Jan 2020 10:34:47 -0800 Subject: [PATCH] refactor(dashboard): normalization (#16535) * refactor: change client api impoorts * refactor(dashAPI): remove unneccessary helpers * refactor: remove unneccessary helpers * chore: move action creators to separate file * refactor(dashboard): action creators use const attribute * refactor(dashoards): normalization * chore: cleanup names of thunks * chore: sort action creator types * fix: saving to a dashboard cell * chore: move dashboard thunks to thunks file * fix: dash index table imports * fix: declare class properties * chore: skip monaco test --- ui/cypress/e2e/explorer.test.ts | 3 +- ui/src/cloud/actions/limits.ts | 15 +- ui/src/dashboards/actions/creators.ts | 84 +++ ui/src/dashboards/actions/notes.ts | 17 +- .../actions/{index.ts => thunks.ts} | 527 +++++++----------- ui/src/dashboards/apis/index.ts | 172 ++---- .../components/DashboardExportOverlay.tsx | 2 +- .../components/DashboardImportOverlay.tsx | 14 +- .../dashboards/components/DashboardPage.tsx | 31 +- ui/src/dashboards/components/EditVEO.tsx | 2 +- ui/src/dashboards/components/NewVEO.tsx | 2 +- .../dashboard_index/DashboardCard.tsx | 22 +- .../dashboard_index/DashboardCards.tsx | 5 + .../dashboard_index/DashboardsIndex.tsx | 2 +- .../DashboardsIndexContents.tsx | 6 +- .../components/dashboard_index/Table.tsx | 8 +- .../variablesControlBar/VariableDropdown.tsx | 3 +- ui/src/dashboards/index.ts | 0 ui/src/dashboards/reducers/dashboards.test.ts | 118 ++-- ui/src/dashboards/reducers/dashboards.ts | 131 ++--- ui/src/dashboards/selectors/index.ts | 9 +- .../components/SaveAsCellForm.tsx | 30 +- .../verify/LineProtocolStepSwitcher.tsx | 4 +- ui/src/me/components/DashboardsList.tsx | 6 +- ui/src/organizations/apis/index.ts | 2 +- ui/src/resources/selectors/index.ts | 14 +- ui/src/schemas/index.ts | 52 ++ ui/src/shared/components/GetResources.tsx | 26 +- ui/src/shared/selectors/getResourcesStatus.ts | 1 + ui/src/store/configureStore.ts | 2 +- ui/src/templates/actions/index.ts | 2 +- ui/src/templates/api/index.ts | 6 +- .../CreateFromTemplateOverlay.tsx | 6 +- ui/src/types/resources.ts | 3 + ui/src/types/schemas.ts | 9 + ui/src/types/stores.ts | 2 - 36 files changed, 682 insertions(+), 656 deletions(-) create mode 100644 ui/src/dashboards/actions/creators.ts rename ui/src/dashboards/actions/{index.ts => thunks.ts} (53%) delete mode 100644 ui/src/dashboards/index.ts diff --git a/ui/cypress/e2e/explorer.test.ts b/ui/cypress/e2e/explorer.test.ts index d092a00504..8a6b62abf4 100644 --- a/ui/cypress/e2e/explorer.test.ts +++ b/ui/cypress/e2e/explorer.test.ts @@ -360,7 +360,8 @@ describe('DataExplorer', () => { cy.getByTestID('switch-to-script-editor').click() }) - it('enables the submit button when a query is typed', () => { + // TODO: fix flakeyness of this test + it.skip('enables the submit button when a query is typed', () => { cy.getByTestID('time-machine-submit-button').should('be.disabled') cy.getByTestID('flux-editor').within(() => { diff --git a/ui/src/cloud/actions/limits.ts b/ui/src/cloud/actions/limits.ts index 56a453eede..32052c2043 100644 --- a/ui/src/cloud/actions/limits.ts +++ b/ui/src/cloud/actions/limits.ts @@ -260,21 +260,22 @@ export const getAssetLimits = () => async (dispatch, getState: GetState) => { export const checkDashboardLimits = () => (dispatch, getState: GetState) => { try { + const state = getState() const { - dashboards: {list}, cloud: {limits}, - } = getState() + resources, + } = state const dashboardsMax = extractDashboardMax(limits) - const dashboardsCount = list.length + const dashboardsCount = resources.dashboards.allIDs.length if (dashboardsCount >= dashboardsMax) { dispatch(setDashboardLimitStatus(LimitStatus.EXCEEDED)) } else { dispatch(setDashboardLimitStatus(LimitStatus.OK)) } - } catch (e) { - console.error(e) + } catch (error) { + console.error(error) } } @@ -367,9 +368,9 @@ export const checkEndpointsLimits = () => (dispatch, getState: GetState) => { } = getState() const endpointsMax = extractEndpointsMax(limits) - const endpoinstCount = endpointsList.length + const endpointsCount = endpointsList.length - if (endpoinstCount >= endpointsMax) { + if (endpointsCount >= endpointsMax) { dispatch(setEndpointsLimitStatus(LimitStatus.EXCEEDED)) } else { dispatch(setEndpointsLimitStatus(LimitStatus.OK)) diff --git a/ui/src/dashboards/actions/creators.ts b/ui/src/dashboards/actions/creators.ts new file mode 100644 index 0000000000..7f4c9e240a --- /dev/null +++ b/ui/src/dashboards/actions/creators.ts @@ -0,0 +1,84 @@ +// Types +import {Dashboard, Label, RemoteDataState, DashboardEntities} from 'src/types' +import {NormalizedSchema} from 'normalizr' + +export const ADD_DASHBOARD_LABEL = 'ADD_DASHBOARD_LABEL' +export const DELETE_DASHBOARD_FAILED = 'DELETE_DASHBOARD_FAILED' +export const EDIT_DASHBOARD = 'EDIT_DASHBOARD' +export const REMOVE_CELL = 'REMOVE_CELL' +export const REMOVE_DASHBOARD = 'REMOVE_DASHBOARD' +export const REMOVE_DASHBOARD_LABEL = 'REMOVE_DASHBOARD_LABEL' +export const SET_DASHBOARD = 'SET_DASHBOARD' +export const SET_DASHBOARDS = 'SET_DASHBOARDS' + +export type Action = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + +// R is the type of the value of the "result" key in normalizr's normalization +type DashboardSchema = NormalizedSchema< + DashboardEntities, + R +> + +// Action Creators +export const editDashboard = (schema: DashboardSchema) => + ({ + type: EDIT_DASHBOARD, + schema, + } as const) + +export const setDashboards = ( + status: RemoteDataState, + schema?: DashboardSchema +) => + ({ + type: SET_DASHBOARDS, + status, + schema, + } as const) + +export const setDashboard = (schema: DashboardSchema) => + ({ + type: SET_DASHBOARD, + schema, + } as const) + +export const removeDashboard = (id: string) => + ({ + type: REMOVE_DASHBOARD, + id, + } as const) + +export const deleteDashboardFailed = (dashboard: Dashboard) => + ({ + type: DELETE_DASHBOARD_FAILED, + payload: {dashboard}, + } as const) + +export const removeCell = (dashboardID: string, cellID: string) => + ({ + type: REMOVE_CELL, + dashboardID, + cellID, + } as const) + +export const addDashboardLabel = (dashboardID: string, label: Label) => + ({ + type: ADD_DASHBOARD_LABEL, + dashboardID, + label, + } as const) + +export const removeDashboardLabel = (dashboardID: string, labelID: string) => + ({ + type: REMOVE_DASHBOARD_LABEL, + dashboardID, + labelID, + } as const) diff --git a/ui/src/dashboards/actions/notes.ts b/ui/src/dashboards/actions/notes.ts index 3eff806829..01e8ba5dd1 100644 --- a/ui/src/dashboards/actions/notes.ts +++ b/ui/src/dashboards/actions/notes.ts @@ -2,7 +2,7 @@ import {get, isUndefined} from 'lodash' // Actions -import {createCellWithView} from 'src/dashboards/actions' +import {createCellWithView} from 'src/dashboards/actions/thunks' import {updateView} from 'src/dashboards/actions/views' // Utils @@ -10,9 +10,16 @@ import {createView} from 'src/shared/utils/view' import {getView} from 'src/dashboards/selectors' // Types -import {GetState, MarkdownViewProperties, NoteEditorMode} from 'src/types' +import { + GetState, + MarkdownViewProperties, + NoteEditorMode, + ResourceType, + Dashboard, +} from 'src/types' import {NoteEditorState} from 'src/dashboards/reducers/notes' import {Dispatch} from 'react' +import {getByID} from 'src/resources/selectors' export type Action = | CloseNoteEditorAction @@ -64,7 +71,11 @@ export const createNoteCell = (dashboardID: string) => ( dispatch: Dispatch>, getState: GetState ) => { - const dashboard = getState().dashboards.list.find(d => d.id === dashboardID) + const dashboard = getByID( + getState(), + ResourceType.Dashboards, + dashboardID + ) if (!dashboard) { throw new Error(`could not find dashboard with id "${dashboardID}"`) diff --git a/ui/src/dashboards/actions/index.ts b/ui/src/dashboards/actions/thunks.ts similarity index 53% rename from ui/src/dashboards/actions/index.ts rename to ui/src/dashboards/actions/thunks.ts index 3c3dc316f0..ec93a68952 100644 --- a/ui/src/dashboards/actions/index.ts +++ b/ui/src/dashboards/actions/thunks.ts @@ -1,31 +1,15 @@ // Libraries -import {Dispatch} from 'redux' +import {normalize} from 'normalizr' +import {Dispatch} from 'react' import {push} from 'react-router-redux' // APIs -import { - createDashboard as createDashboardAJAX, - getDashboard as getDashboardAJAX, - getDashboards as getDashboardsAJAX, - deleteDashboard as deleteDashboardAJAX, - updateDashboard as updateDashboardAJAX, - updateCells as updateCellsAJAX, - addCell as addCellAJAX, - deleteCell as deleteCellAJAX, - getView as getViewAJAX, - updateView as updateViewAJAX, -} from 'src/dashboards/apis' -import { - getVariables as apiGetVariables, - getDashboard as apiGetDashboard, - postDashboard as apiPostDashboard, - postDashboardsCell as apiPostDashboardsCell, - postDashboardsLabel as apiPostDashboardsLabel, - deleteDashboardsLabel as apiDeleteDashboardsLabel, - patchDashboardsCellsView as apiPatchDashboardsCellsView, - getDashboardsCellsView as apiGetDashboardsCellsView, -} from 'src/client' -import {createDashboardFromTemplate as createDashboardFromTemplateAJAX} from 'src/templates/api' +import * as dashAPI from 'src/dashboards/apis' +import * as api from 'src/client' +import * as tempAPI from 'src/templates/api' + +// Schemas +import * as schemas from 'src/schemas' // Actions import { @@ -35,13 +19,13 @@ import { import { deleteTimeRange, updateTimeRangeFromQueryParams, - DeleteTimeRangeAction, } from 'src/dashboards/actions/ranges' -import {setView, SetViewAction, setViews} from 'src/dashboards/actions/views' +import {setView, setViews} from 'src/dashboards/actions/views' import {selectValue} from 'src/variables/actions/creators' import {getVariables, refreshVariableValues} from 'src/variables/actions/thunks' import {setExportTemplate} from 'src/templates/actions' import {checkDashboardLimits} from 'src/cloud/actions/limits' +import * as creators from 'src/dashboards/actions/creators' // Utils import {addVariableDefaults} from 'src/variables/actions/thunks' @@ -62,6 +46,7 @@ import {incrementCloneName} from 'src/utils/naming' import {isLimitError} from 'src/cloud/utils/limits' import {getOrg} from 'src/organizations/selectors' import {addLabelDefaults} from 'src/labels/utils' +import {getAll, getByID} from 'src/resources/selectors' // Constants import * as copy from 'src/shared/copy/notifications' @@ -78,196 +63,13 @@ import { Label, RemoteDataState, NewCell, + DashboardEntities, + ResourceType, } from 'src/types' -import { - Dashboard as IDashboard, - Cell as ICell, - DashboardWithViewProperties, -} from 'src/client' -export const addDashboardIDToCells = ( - cells: ICell[], - dashboardID: string -): Cell[] => { - return cells.map(c => { - return {...c, dashboardID} - }) -} - -export const addDashboardDefaults = ( - dashboard: IDashboard | DashboardWithViewProperties -): Dashboard => { - return { - ...dashboard, - cells: addDashboardIDToCells(dashboard.cells, dashboard.id) || [], - id: dashboard.id || '', - labels: (dashboard.labels || []).map(addLabelDefaults), - name: dashboard.name || '', - orgID: dashboard.orgID || '', - } -} - -export enum ActionTypes { - SetDashboards = 'SET_DASHBOARDS', - SetDashboard = 'SET_DASHBOARD', - RemoveDashboard = 'REMOVE_DASHBOARD', - DeleteDashboardFailed = 'DELETE_DASHBOARD_FAILED', - EditDashboard = 'EDIT_DASHBOARD', - RemoveCell = 'REMOVE_CELL', - AddDashboardLabel = 'ADD_DASHBOARD_LABEL', - RemoveDashboardLabel = 'REMOVE_DASHBOARD_LABEL', -} - -export type Action = - | SetDashboardsAction - | RemoveDashboardAction - | SetDashboardAction - | EditDashboardAction - | RemoveCellAction - | SetViewAction - | DeleteTimeRangeAction - | DeleteDashboardFailedAction - | AddDashboardLabelAction - | RemoveDashboardLabelAction - -interface RemoveCellAction { - type: ActionTypes.RemoveCell - payload: { - dashboard: Dashboard - cell: Cell - } -} - -interface EditDashboardAction { - type: ActionTypes.EditDashboard - payload: { - dashboard: Dashboard - } -} - -interface SetDashboardsAction { - type: ActionTypes.SetDashboards - payload: { - status: RemoteDataState - list: Dashboard[] - } -} - -interface RemoveDashboardAction { - type: ActionTypes.RemoveDashboard - payload: { - id: string - } -} - -interface DeleteDashboardFailedAction { - type: ActionTypes.DeleteDashboardFailed - payload: { - dashboard: Dashboard - } -} - -interface SetDashboardAction { - type: ActionTypes.SetDashboard - payload: { - dashboard: Dashboard - } -} - -interface AddDashboardLabelAction { - type: ActionTypes.AddDashboardLabel - payload: { - dashboardID: string - label: Label - } -} - -interface RemoveDashboardLabelAction { - type: ActionTypes.RemoveDashboardLabel - payload: { - dashboardID: string - label: Label - } -} - -// Action Creators - -export const editDashboard = (dashboard: Dashboard): EditDashboardAction => ({ - type: ActionTypes.EditDashboard, - payload: {dashboard}, -}) - -export const setDashboards = ( - status: RemoteDataState, - list?: Dashboard[] -): SetDashboardsAction => { - if (list) { - list = list.map(obj => { - if (obj.name === undefined) { - obj.name = '' - } - if (obj.meta === undefined) { - obj.meta = {} - } - if (obj.meta.updatedAt === undefined) { - obj.meta.updatedAt = new Date().toDateString() - } - return obj - }) - } - - return { - type: ActionTypes.SetDashboards, - payload: { - status, - list, - }, - } -} - -export const setDashboard = (dashboard: Dashboard): SetDashboardAction => ({ - type: ActionTypes.SetDashboard, - payload: {dashboard}, -}) - -export const removeDashboard = (id: string): RemoveDashboardAction => ({ - type: ActionTypes.RemoveDashboard, - payload: {id}, -}) - -export const deleteDashboardFailed = ( - dashboard: Dashboard -): DeleteDashboardFailedAction => ({ - type: ActionTypes.DeleteDashboardFailed, - payload: {dashboard}, -}) - -export const removeCell = ( - dashboard: Dashboard, - cell: Cell -): RemoveCellAction => ({ - type: ActionTypes.RemoveCell, - payload: {dashboard, cell}, -}) - -export const addDashboardLabel = ( - dashboardID: string, - label: Label -): AddDashboardLabelAction => ({ - type: ActionTypes.AddDashboardLabel, - payload: {dashboardID, label}, -}) - -export const removeDashboardLabel = ( - dashboardID: string, - label: Label -): RemoveDashboardLabelAction => ({ - type: ActionTypes.RemoveDashboardLabel, - payload: {dashboardID, label}, -}) +type Action = creators.Action // Thunks - export const createDashboard = () => async ( dispatch, getState: GetState @@ -281,8 +83,13 @@ export const createDashboard = () => async ( orgID: org.id, } - const data = await createDashboardAJAX(newDashboard) - dispatch(push(`/orgs/${org.id}/dashboards/${data.id}`)) + const resp = await api.postDashboard({data: newDashboard}) + + if (resp.status !== 201) { + throw new Error(resp.data.message) + } + + dispatch(push(`/orgs/${org.id}/dashboards/${resp.data.id}`)) dispatch(checkDashboardLimits()) } catch (error) { console.error(error) @@ -295,67 +102,32 @@ export const createDashboard = () => async ( } } -export const cloneUtilFunc = async (dash: Dashboard, id: string) => { - const cells = dash.cells - const pendingViews = cells.map(cell => - apiGetDashboardsCellsView({ - dashboardID: dash.id, - cellID: cell.id, - }).then(res => { - return { - ...res, - cellID: cell.id, - } - }) - ) - const views = await Promise.all(pendingViews) - - if (views.length > 0 && views.some(v => v.status !== 200)) { - throw new Error('An error occurred cloning the dashboard') - } - - return views.map(async v => { - const view = v.data as View - const cell = cells.find(c => c.id === view.id) - - if (cell && id) { - const newCell = await apiPostDashboardsCell({ - dashboardID: id, - data: cell, - }) - if (newCell.status !== 201) { - throw new Error('An error occurred cloning the dashboard') - } - return apiPatchDashboardsCellsView({ - dashboardID: id, - cellID: newCell.data.id, - data: view, - }) - } - }) -} - export const cloneDashboard = (dashboard: Dashboard) => async ( dispatch, getState: GetState ): Promise => { try { - const {dashboards} = getState() - - const org = getOrg(getState()) - const allDashboardNames = dashboards.list.map(d => d.name) + const state = getState() + const org = getOrg(state) + const dashboards = getAll(state, ResourceType.Dashboards) + const allDashboardNames = dashboards.map(d => d.name) const clonedName = incrementCloneName(allDashboardNames, dashboard.name) - const getResp = await apiGetDashboard({dashboardID: dashboard.id}) + const getResp = await api.getDashboard({dashboardID: dashboard.id}) if (getResp.status !== 200) { throw new Error(getResp.data.message) } - const dash = addDashboardDefaults(getResp.data) + const {entities, result} = normalize( + getResp.data, + schemas.dashboard + ) - const postResp = await apiPostDashboard({ + const dash: Dashboard = entities[result] + + const postResp = await api.postDashboard({ data: { orgID: org.id, name: clonedName, @@ -368,7 +140,7 @@ export const cloneDashboard = (dashboard: Dashboard) => async ( } const pendingLabels = dash.labels.map(l => - apiPostDashboardsLabel({ + api.postDashboardsLabel({ dashboardID: postResp.data.id, data: {labelID: l.id}, }) @@ -380,7 +152,7 @@ export const cloneDashboard = (dashboard: Dashboard) => async ( throw new Error('An error occurred cloning the labels for this dashboard') } - const clonedViews = await cloneUtilFunc(dash, postResp.data.id) + const clonedViews = await dashAPI.cloneUtilFunc(dash, postResp.data.id) const newViews = await Promise.all(clonedViews) @@ -400,20 +172,29 @@ export const cloneDashboard = (dashboard: Dashboard) => async ( } } -export const getDashboardsAsync = () => async ( +export const getDashboards = () => async ( dispatch: Dispatch, getState: GetState -): Promise => { +): Promise => { try { const org = getOrg(getState()) + const {setDashboards} = creators dispatch(setDashboards(RemoteDataState.Loading)) - const dashboards = await getDashboardsAJAX(org.id) - dispatch(setDashboards(RemoteDataState.Done, dashboards)) + const resp = await api.getDashboards({query: {orgID: org.id}}) - return dashboards + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const dashboards = normalize( + resp.data.dashboards, + schemas.arrayOfDashboards + ) + + dispatch(setDashboards(RemoteDataState.Done, dashboards)) } catch (error) { - dispatch(setDashboards(RemoteDataState.Error)) + dispatch(creators.setDashboards(RemoteDataState.Error)) console.error(error) throw error } @@ -425,11 +206,20 @@ export const createDashboardFromTemplate = ( try { const org = getOrg(getState()) - await createDashboardFromTemplateAJAX(template, org.id) + await tempAPI.createDashboardFromTemplate(template, org.id) - const dashboards = await getDashboardsAJAX(org.id) + const resp = await api.getDashboards({query: {orgID: org.id}}) - dispatch(setDashboards(RemoteDataState.Done, dashboards)) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const dashboards = normalize( + resp.data.dashboards, + schemas.arrayOfDashboards + ) + + dispatch(creators.setDashboards(RemoteDataState.Done, dashboards)) dispatch(notify(copy.importDashboardSucceeded())) dispatch(checkDashboardLimits()) } catch (error) { @@ -441,14 +231,19 @@ export const createDashboardFromTemplate = ( } } -export const deleteDashboardAsync = (dashboard: Dashboard) => async ( +export const deleteDashboard = (dashboard: Dashboard) => async ( dispatch ): Promise => { - dispatch(removeDashboard(dashboard.id)) + dispatch(creators.removeDashboard(dashboard.id)) dispatch(deleteTimeRange(dashboard.id)) try { - await deleteDashboardAJAX(dashboard) + const resp = await api.deleteDashboard({dashboardID: dashboard.id}) + + if (resp.data !== 204) { + throw new Error(resp.data.message) + } + dispatch(notify(copy.dashboardDeleted(dashboard.name))) dispatch(checkDashboardLimits()) } catch (error) { @@ -456,7 +251,7 @@ export const deleteDashboardAsync = (dashboard: Dashboard) => async ( notify(copy.dashboardDeleteFailed(dashboard.name, error.data.message)) ) - dispatch(deleteDashboardFailed(dashboard)) + dispatch(creators.deleteDashboardFailed(dashboard)) } } @@ -470,30 +265,40 @@ export const refreshDashboardVariableValues = ( return dispatch(refreshVariableValues(dashboardID, variablesInUse)) } -export const getDashboardAsync = (dashboardID: string) => async ( +export const getDashboard = (dashboardID: string) => async ( dispatch, getState: GetState ): Promise => { try { // Fetch the dashboard and all variables a user has access to - const [dashboard] = await Promise.all([ - getDashboardAJAX(dashboardID), + const [resp] = await Promise.all([ + api.getDashboard({dashboardID}), dispatch(getVariables()), ]) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const normDash = normalize( + resp.data, + schemas.dashboard + ) + const {cells, id}: Dashboard = normDash.entities.dashboards[normDash.result] + // Fetch all the views in use on the dashboard const views = await Promise.all( - dashboard.cells.map(cell => getViewAJAX(dashboard.id, cell.id)) + cells.map(cell => dashAPI.getView(id, cell.id)) ) dispatch(setViews(RemoteDataState.Done, views)) // Ensure the values for the variables in use on the dashboard are populated - await dispatch(refreshDashboardVariableValues(dashboard.id, views)) + await dispatch(refreshDashboardVariableValues(id, views)) // Now that all the necessary state has been loaded, set the dashboard - dispatch(setDashboard(dashboard)) - dispatch(updateTimeRangeFromQueryParams(dashboardID)) + dispatch(creators.setDashboard(normDash)) + dispatch(updateTimeRangeFromQueryParams(id)) } catch (error) { const org = getOrg(getState()) dispatch(push(`/orgs/${org.id}/dashboards`)) @@ -502,12 +307,25 @@ export const getDashboardAsync = (dashboardID: string) => async ( } } -export const updateDashboardAsync = (dashboard: Dashboard) => async ( - dispatch: Dispatch +export const updateDashboard = (dashboard: Dashboard) => async ( + dispatch: Dispatch ): Promise => { try { - const updatedDashboard = await updateDashboardAJAX(dashboard) - dispatch(editDashboard(updatedDashboard)) + const resp = await api.patchDashboard({ + dashboardID: dashboard.id, + data: dashboard, + }) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const updatedDashboard = normalize( + resp.data, + schemas.dashboard + ) + + dispatch(creators.editDashboard(updatedDashboard)) } catch (error) { console.error(error) dispatch(notify(copy.dashboardUpdateFailed())) @@ -521,26 +339,60 @@ export const createCellWithView = ( ) => async (dispatch, getState: GetState): Promise => { try { const state = getState() - let dashboard = state.dashboards.list.find(d => d.id === dashboardID) + let dashboard = getByID( + state, + ResourceType.Dashboards, + dashboardID + ) + if (!dashboard) { - dashboard = await getDashboardAJAX(dashboardID) + const resp = await api.getDashboard({dashboardID}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const {entities, result} = normalize< + Dashboard, + DashboardEntities, + string + >(resp.data, schemas.dashboard) + + dashboard = entities.dashboards[result] } const cell: NewCell = getNewDashboardCell(dashboard, clonedCell) // Create the cell - const createdCell = await addCellAJAX(dashboardID, cell) + const cellResp = await api.postDashboardsCell({dashboardID, data: cell}) + + if (cellResp.status !== 201) { + throw new Error(cellResp.data.message) + } + + const createdCell = cellResp.data // Create the view and associate it with the cell - const newView = await updateViewAJAX(dashboardID, createdCell.id, view) + const newView = await dashAPI.updateView(dashboardID, createdCell.id, view) // Update the dashboard with the new cell let updatedDashboard: Dashboard = { ...dashboard, - cells: [...dashboard.cells, createdCell], + cells: [...dashboard.cells, {...cellResp.data, dashboardID}], } - updatedDashboard = await updateDashboardAJAX(dashboard) + const resp = await api.patchDashboard({dashboardID, data: updatedDashboard}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const normDash = normalize( + resp.data, + schemas.dashboard + ) + const {entities, result} = normDash + + updatedDashboard = entities[result] // Refresh variables in use on dashboard const views = [...getViewsForDashboard(state, dashboard.id), newView] @@ -548,7 +400,7 @@ export const createCellWithView = ( await dispatch(refreshDashboardVariableValues(dashboard.id, views)) dispatch(setView(createdCell.id, newView, RemoteDataState.Done)) - dispatch(editDashboard(updatedDashboard)) + dispatch(creators.editDashboard(normDash)) } catch { notify(copy.cellAddFailed()) } @@ -561,7 +413,7 @@ export const updateView = (dashboardID: string, view: View) => async ( const cellID = view.cellID try { - const newView = await updateViewAJAX(dashboardID, cellID, view) + const newView = await dashAPI.updateView(dashboardID, cellID, view) const views = getViewsForDashboard(getState(), dashboardID) @@ -577,23 +429,36 @@ export const updateView = (dashboardID: string, view: View) => async ( } } -export const updateCellsAsync = (dashboard: Dashboard, cells: Cell[]) => async ( +export const updateCells = (dashboard: Dashboard, cells: Cell[]) => async ( dispatch: Dispatch ): Promise => { try { - const updatedCells = await updateCellsAJAX(dashboard.id, cells) - const updatedDashboard = { - ...dashboard, - cells: updatedCells, + const resp = await api.putDashboardsCells({ + dashboardID: dashboard.id, + data: cells, + }) + + if (resp.status !== 200) { + throw new Error(resp.data.message) } - dispatch(setDashboard(updatedDashboard)) + const updatedDashboard = { + ...dashboard, + cells: resp.data.cells, + } + + const normDash = normalize( + updatedDashboard, + schemas.dashboard + ) + + dispatch(creators.setDashboard(normDash)) } catch (error) { console.error(error) } } -export const deleteCellAsync = (dashboard: Dashboard, cell: Cell) => async ( +export const deleteCell = (dashboard: Dashboard, cell: Cell) => async ( dispatch, getState: GetState ): Promise => { @@ -603,18 +468,18 @@ export const deleteCellAsync = (dashboard: Dashboard, cell: Cell) => async ( ) await Promise.all([ - deleteCellAJAX(dashboard.id, cell), + api.deleteDashboardsCell({dashboardID: dashboard.id, cellID: cell.id}), dispatch(refreshDashboardVariableValues(dashboard.id, views)), ]) - dispatch(removeCell(dashboard, cell)) + dispatch(creators.removeCell(dashboard.id, cell.id)) dispatch(notify(copy.cellDeleted())) } catch (error) { console.error(error) } } -export const copyDashboardCellAsync = (dashboard: Dashboard, cell: Cell) => ( +export const copyDashboardCell = (dashboard: Dashboard, cell: Cell) => ( dispatch: Dispatch ) => { try { @@ -624,19 +489,23 @@ export const copyDashboardCellAsync = (dashboard: Dashboard, cell: Cell) => ( cells: [...dashboard.cells, clonedCell], } - dispatch(setDashboard(updatedDashboard)) + const normDash = normalize( + updatedDashboard, + schemas.dashboard + ) + + dispatch(creators.setDashboard(normDash)) dispatch(notify(copy.cellAdded())) } catch (error) { console.error(error) } } -export const addDashboardLabelAsync = ( - dashboardID: string, - label: Label -) => async (dispatch: Dispatch) => { +export const addDashboardLabel = (dashboardID: string, label: Label) => async ( + dispatch: Dispatch +) => { try { - const resp = await apiPostDashboardsLabel({ + const resp = await api.postDashboardsLabel({ dashboardID, data: {labelID: label.id}, }) @@ -647,19 +516,19 @@ export const addDashboardLabelAsync = ( const lab = addLabelDefaults(resp.data.label) - dispatch(addDashboardLabel(dashboardID, lab)) + dispatch(creators.addDashboardLabel(dashboardID, lab)) } catch (error) { console.error(error) dispatch(notify(copy.addDashboardLabelFailed())) } } -export const removeDashboardLabelAsync = ( +export const removeDashboardLabel = ( dashboardID: string, label: Label ) => async (dispatch: Dispatch) => { try { - const resp = await apiDeleteDashboardsLabel({ + const resp = await api.deleteDashboardsLabel({ dashboardID, labelID: label.id, }) @@ -668,7 +537,7 @@ export const removeDashboardLabelAsync = ( throw new Error(resp.data.message) } - dispatch(removeDashboardLabel(dashboardID, label)) + dispatch(creators.removeDashboardLabel(dashboardID, label.id)) } catch (error) { console.error(error) dispatch(notify(copy.removedDashboardLabelFailed())) @@ -680,8 +549,13 @@ export const selectVariableValue = ( variableID: string, value: string ) => async (dispatch, getState: GetState): Promise => { - const variables = getHydratedVariables(getState(), dashboardID) - const dashboard = getState().dashboards.list.find(d => d.id === dashboardID) + const state = getState() + const variables = getHydratedVariables(state, dashboardID) + const dashboard = getByID( + state, + ResourceType.Dashboards, + dashboardID + ) dispatch(selectValue(dashboardID, variableID, value)) @@ -695,12 +569,25 @@ export const convertToTemplate = (dashboardID: string) => async ( try { dispatch(setExportTemplate(RemoteDataState.Loading)) const org = getOrg(getState()) - const dashboard = await getDashboardAJAX(dashboardID) + + const dashResp = await api.getDashboard({dashboardID}) + + if (dashResp.status !== 200) { + throw new Error(dashResp.data.message) + } + + const {entities, result} = normalize( + dashResp.data, + schemas.dashboard + ) + + const dashboard: Dashboard = entities[result] + const pendingViews = dashboard.cells.map(c => - getViewAJAX(dashboardID, c.id) + dashAPI.getView(dashboardID, c.id) ) const views = await Promise.all(pendingViews) - const resp = await apiGetVariables({query: {orgID: org.id}}) + const resp = await api.getVariables({query: {orgID: org.id}}) if (resp.status !== 200) { throw new Error(resp.data.message) } diff --git a/ui/src/dashboards/apis/index.ts b/ui/src/dashboards/apis/index.ts index d8b67e9052..8b7b0c3cc0 100644 --- a/ui/src/dashboards/apis/index.ts +++ b/ui/src/dashboards/apis/index.ts @@ -1,132 +1,14 @@ -// Libraries -import _ from 'lodash' - // APIs -import { - getDashboards as apiGetDashboards, - getDashboard as apiGetDashboard, - postDashboard as apiPostDashboard, - deleteDashboard as apiDeleteDashboard, - patchDashboard as apiPatchDashboard, - postDashboardsCell as apiPostDashboardsCell, - putDashboardsCells as apiPutDashboardsCells, - deleteDashboardsCell as apiDeleteDashboardsCell, - getDashboardsCellsView as apiGetDashboardsCellsView, - patchDashboardsCellsView as apiPatchDashboardsCellsView, -} from 'src/client' +import * as api from 'src/client' // Types -import {Cell, NewCell, Dashboard, View, CreateDashboardRequest} from 'src/types' - -// Utils -import { - addDashboardDefaults, - addDashboardIDToCells, -} from 'src/dashboards/actions' - -export const getDashboards = async (orgID: string): Promise => { - const resp = await apiGetDashboards({query: {orgID}}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - return resp.data.dashboards.map(d => addDashboardDefaults(d)) -} - -export const getDashboard = async (id: string): Promise => { - const resp = await apiGetDashboard({dashboardID: id}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - return addDashboardDefaults(resp.data) -} - -export const createDashboard = async ( - props: CreateDashboardRequest -): Promise => { - const resp = await apiPostDashboard({data: props}) - - if (resp.status !== 201) { - throw new Error(resp.data.message) - } - - return addDashboardDefaults(resp.data) -} - -export const deleteDashboard = async (dashboard: Dashboard): Promise => { - const resp = await apiDeleteDashboard({dashboardID: dashboard.id}) - - if (resp.status !== 204) { - throw new Error(resp.data.message) - } -} - -export const updateDashboard = async ( - dashboard: Dashboard -): Promise => { - const resp = await apiPatchDashboard({ - dashboardID: dashboard.id, - data: dashboard, - }) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - return addDashboardDefaults(resp.data) -} - -export const addCell = async ( - dashboardID: string, - cell: NewCell -): Promise => { - const resp = await apiPostDashboardsCell({dashboardID, data: cell}) - - if (resp.status !== 201) { - throw new Error(resp.data.message) - } - - const result = resp.data - - const cellWithID = {...result, dashboardID} - - return cellWithID -} - -export const updateCells = async ( - id: string, - cells: Cell[] -): Promise => { - const resp = await apiPutDashboardsCells({dashboardID: id, data: cells}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const result = resp.data.cells - - return addDashboardIDToCells(result, id) -} - -export const deleteCell = async ( - dashboardID: string, - cell: Cell -): Promise => { - const resp = await apiDeleteDashboardsCell({dashboardID, cellID: cell.id}) - - if (resp.status !== 204) { - throw new Error(resp.data.message) - } -} +import {View, NewView, Dashboard} from 'src/types' export const getView = async ( dashboardID: string, cellID: string ): Promise => { - const resp = await apiGetDashboardsCellsView({dashboardID, cellID}) + const resp = await api.getDashboardsCellsView({dashboardID, cellID}) if (resp.status !== 200) { throw new Error(resp.data.message) @@ -138,9 +20,9 @@ export const getView = async ( export const updateView = async ( dashboardID: string, cellID: string, - view: Partial + view: NewView ): Promise => { - const resp = await apiPatchDashboardsCellsView({ + const resp = await api.patchDashboardsCellsView({ dashboardID, cellID, data: view as View, @@ -154,3 +36,47 @@ export const updateView = async ( return viewWithIDs } + +export const cloneUtilFunc = async (dash: Dashboard, id: string) => { + const cells = dash.cells + const pendingViews = cells.map(cell => + api + .getDashboardsCellsView({ + dashboardID: dash.id, + cellID: cell.id, + }) + .then(res => { + return { + ...res, + cellID: cell.id, + } + }) + ) + const views = await Promise.all(pendingViews) + + if (views.length > 0 && views.some(v => v.status !== 200)) { + throw new Error('An error occurred cloning the dashboard') + } + + return views.map(async v => { + const view = v.data as View + const cell = cells.find(c => c.id === view.id) + + if (cell && id) { + const newCell = await api.postDashboardsCell({ + dashboardID: id, + data: cell, + }) + + if (newCell.status !== 201) { + throw new Error('An error occurred cloning the dashboard') + } + + return api.patchDashboardsCellsView({ + dashboardID: id, + cellID: newCell.data.id, + data: view, + }) + } + }) +} diff --git a/ui/src/dashboards/components/DashboardExportOverlay.tsx b/ui/src/dashboards/components/DashboardExportOverlay.tsx index 872bdae2fe..162608d0de 100644 --- a/ui/src/dashboards/components/DashboardExportOverlay.tsx +++ b/ui/src/dashboards/components/DashboardExportOverlay.tsx @@ -6,7 +6,7 @@ import {withRouter, WithRouterProps} from 'react-router' import ExportOverlay from 'src/shared/components/ExportOverlay' // Actions -import {convertToTemplate as convertToTemplateAction} from 'src/dashboards/actions/index' +import {convertToTemplate as convertToTemplateAction} from 'src/dashboards/actions/thunks' import {clearExportTemplate as clearExportTemplateAction} from 'src/templates/actions' // Types diff --git a/ui/src/dashboards/components/DashboardImportOverlay.tsx b/ui/src/dashboards/components/DashboardImportOverlay.tsx index f22e146bb8..ca9d6513d7 100644 --- a/ui/src/dashboards/components/DashboardImportOverlay.tsx +++ b/ui/src/dashboards/components/DashboardImportOverlay.tsx @@ -1,7 +1,7 @@ // Libraries import React, {PureComponent} from 'react' import {withRouter, WithRouterProps} from 'react-router' -import _ from 'lodash' +import {isEmpty} from 'lodash' import {connect} from 'react-redux' // Components @@ -12,9 +12,9 @@ import {invalidJSON} from 'src/shared/copy/notifications' // Actions import { - getDashboardsAsync, + getDashboards, createDashboardFromTemplate as createDashboardFromTemplateAction, -} from 'src/dashboards/actions' +} from 'src/dashboards/actions/thunks' import {notify as notifyAction} from 'src/shared/actions/notifications' // Types @@ -30,7 +30,7 @@ interface State { interface DispatchProps { createDashboardFromTemplate: typeof createDashboardFromTemplateAction notify: typeof notifyAction - populateDashboards: typeof getDashboardsAsync + populateDashboards: typeof getDashboards } interface OwnProps extends WithRouterProps { @@ -73,7 +73,7 @@ class DashboardImportOverlay extends PureComponent { return } - if (_.isEmpty(template)) { + if (isEmpty(template)) { this.onDismiss() } @@ -89,9 +89,9 @@ class DashboardImportOverlay extends PureComponent { } const mdtp: DispatchProps = { - createDashboardFromTemplate: createDashboardFromTemplateAction, notify: notifyAction, - populateDashboards: getDashboardsAsync, + populateDashboards: getDashboards, + createDashboardFromTemplate: createDashboardFromTemplateAction, } export default connect<{}, DispatchProps, OwnProps>( diff --git a/ui/src/dashboards/components/DashboardPage.tsx b/ui/src/dashboards/components/DashboardPage.tsx index 5ac1b72690..d68ca0666a 100644 --- a/ui/src/dashboards/components/DashboardPage.tsx +++ b/ui/src/dashboards/components/DashboardPage.tsx @@ -16,7 +16,7 @@ import LimitChecker from 'src/cloud/components/LimitChecker' import RateLimitAlert from 'src/cloud/components/RateLimitAlert' // Actions -import * as dashboardActions from 'src/dashboards/actions' +import * as dashboardActions from 'src/dashboards/actions/thunks' import * as rangesActions from 'src/dashboards/actions/ranges' import * as appActions from 'src/shared/actions/app' import { @@ -39,6 +39,7 @@ import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' // Selectors import {getTimeRangeByDashboardID} from 'src/dashboards/selectors' import {getOrg} from 'src/organizations/selectors' +import {getByID} from 'src/resources/selectors' // Types import { @@ -52,6 +53,7 @@ import { AutoRefreshStatus, Organization, RemoteDataState, + ResourceType, } from 'src/types' import {WithRouterProps} from 'react-router' import {ManualRefreshProps} from 'src/shared/components/ManualRefresh' @@ -73,11 +75,11 @@ interface StateProps { } interface DispatchProps { - deleteCell: typeof dashboardActions.deleteCellAsync - copyCell: typeof dashboardActions.copyDashboardCellAsync - getDashboard: typeof dashboardActions.getDashboardAsync - updateDashboard: typeof dashboardActions.updateDashboardAsync - updateCells: typeof dashboardActions.updateCellsAsync + deleteCell: typeof dashboardActions.deleteCell + copyCell: typeof dashboardActions.copyDashboardCell + getDashboard: typeof dashboardActions.getDashboard + updateDashboard: typeof dashboardActions.updateDashboard + updateCells: typeof dashboardActions.updateCells updateQueryParams: typeof rangesActions.updateQueryParams setDashboardTimeRange: typeof rangesActions.setDashboardTimeRange handleChooseAutoRefresh: typeof setAutoRefreshInterval @@ -299,7 +301,6 @@ class DashboardPage extends Component { const mstp = (state: AppState, {params: {dashboardID}}): StateProps => { const { links, - dashboards, views: {views}, userSettings: {showVariablesControls}, cloud: {limits}, @@ -311,7 +312,11 @@ const mstp = (state: AppState, {params: {dashboardID}}): StateProps => { const autoRefresh = state.autoRefresh[dashboardID] || AUTOREFRESH_DEFAULT - const dashboard = dashboards.list.find(d => d.id === dashboardID) + const dashboard = getByID( + state, + ResourceType.Dashboards, + dashboardID + ) const limitedResources = extractRateLimitResources(limits) const limitStatus = extractRateLimitStatus(limits) @@ -330,11 +335,11 @@ const mstp = (state: AppState, {params: {dashboardID}}): StateProps => { } const mdtp: DispatchProps = { - getDashboard: dashboardActions.getDashboardAsync, - updateDashboard: dashboardActions.updateDashboardAsync, - copyCell: dashboardActions.copyDashboardCellAsync, - deleteCell: dashboardActions.deleteCellAsync, - updateCells: dashboardActions.updateCellsAsync, + getDashboard: dashboardActions.getDashboard, + updateDashboard: dashboardActions.updateDashboard, + copyCell: dashboardActions.copyDashboardCell, + deleteCell: dashboardActions.deleteCell, + updateCells: dashboardActions.updateCells, handleChooseAutoRefresh: setAutoRefreshInterval, onSetAutoRefreshStatus: setAutoRefreshStatus, handleClickPresentationButton: appActions.delayEnablePresentationMode, diff --git a/ui/src/dashboards/components/EditVEO.tsx b/ui/src/dashboards/components/EditVEO.tsx index 3a9e785502..77b618f93e 100644 --- a/ui/src/dashboards/components/EditVEO.tsx +++ b/ui/src/dashboards/components/EditVEO.tsx @@ -11,7 +11,7 @@ import VEOHeader from 'src/dashboards/components/VEOHeader' // Actions import {setName} from 'src/timeMachine/actions' -import {saveVEOView} from 'src/dashboards/actions' +import {saveVEOView} from 'src/dashboards/actions/thunks' import {getViewForTimeMachine} from 'src/dashboards/actions/views' // Utils diff --git a/ui/src/dashboards/components/NewVEO.tsx b/ui/src/dashboards/components/NewVEO.tsx index cda734d6f1..b549ecd966 100644 --- a/ui/src/dashboards/components/NewVEO.tsx +++ b/ui/src/dashboards/components/NewVEO.tsx @@ -12,7 +12,7 @@ import VEOHeader from 'src/dashboards/components/VEOHeader' // Actions import {loadNewVEO} from 'src/timeMachine/actions' import {setName} from 'src/timeMachine/actions' -import {saveVEOView} from 'src/dashboards/actions' +import {saveVEOView} from 'src/dashboards/actions/thunks' // Utils import {getActiveTimeMachine} from 'src/timeMachine/selectors' diff --git a/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx b/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx index 61dd85240c..6b29a2e895 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardCard.tsx @@ -11,11 +11,11 @@ import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels' // Actions import { cloneDashboard, - deleteDashboardAsync, - updateDashboardAsync, - addDashboardLabelAsync, - removeDashboardLabelAsync, -} from 'src/dashboards/actions' + deleteDashboard, + updateDashboard, + addDashboardLabel, + removeDashboardLabel, +} from 'src/dashboards/actions/thunks' import {createLabel as createLabelAsync} from 'src/labels/actions' // Selectors @@ -44,8 +44,8 @@ interface DispatchProps { onDeleteDashboard: (dashboard: Dashboard) => void onUpdateDashboard: (dashboard: Dashboard) => void onCloneDashboard: (dashboard: Dashboard) => void - onAddDashboardLabel: typeof addDashboardLabelAsync - onRemoveDashboardLabel: typeof removeDashboardLabelAsync + onAddDashboardLabel: typeof addDashboardLabel + onRemoveDashboardLabel: typeof removeDashboardLabel onCreateLabel: typeof createLabelAsync onResetViews: typeof resetViews } @@ -199,12 +199,12 @@ const mstp = ({labels}: AppState): StateProps => { const mdtp: DispatchProps = { onCreateLabel: createLabelAsync, - onAddDashboardLabel: addDashboardLabelAsync, - onRemoveDashboardLabel: removeDashboardLabelAsync, + onAddDashboardLabel: addDashboardLabel, + onRemoveDashboardLabel: removeDashboardLabel, onResetViews: resetViews, onCloneDashboard: cloneDashboard, - onDeleteDashboard: deleteDashboardAsync, - onUpdateDashboard: updateDashboardAsync, + onDeleteDashboard: deleteDashboard, + onUpdateDashboard: updateDashboard, } export default connect( diff --git a/ui/src/dashboards/components/dashboard_index/DashboardCards.tsx b/ui/src/dashboards/components/dashboard_index/DashboardCards.tsx index b48186f63f..24e8781152 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardCards.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardCards.tsx @@ -22,6 +22,11 @@ interface Props { } export default class DashboardCards extends PureComponent { + private _frame + private _window + private _observer + private _spinner + private memGetSortedResources = memoizeOne( getSortedResources ) diff --git a/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx b/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx index a06e10c318..2de0a999ec 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardsIndex.tsx @@ -20,7 +20,7 @@ import {pageTitleSuffixer} from 'src/shared/utils/pageTitles' import {extractDashboardLimits} from 'src/cloud/utils/limits' // Actions -import {createDashboard as createDashboardAction} from 'src/dashboards/actions' +import {createDashboard as createDashboardAction} from 'src/dashboards/actions/thunks' // Types import {AppState} from 'src/types' diff --git a/ui/src/dashboards/components/dashboard_index/DashboardsIndexContents.tsx b/ui/src/dashboards/components/dashboard_index/DashboardsIndexContents.tsx index 7e4a3e9574..7c3fcc6ff2 100644 --- a/ui/src/dashboards/components/dashboard_index/DashboardsIndexContents.tsx +++ b/ui/src/dashboards/components/dashboard_index/DashboardsIndexContents.tsx @@ -15,7 +15,8 @@ import {checkDashboardLimits as checkDashboardLimitsAction} from 'src/cloud/acti import {ErrorHandling} from 'src/shared/decorators/errors' // Types -import {Dashboard, AppState, RemoteDataState} from 'src/types' +import {Dashboard, AppState, RemoteDataState, ResourceType} from 'src/types' +import {getAll} from 'src/resources/selectors' interface OwnProps { onFilterChange: (searchTerm: string) => void @@ -70,14 +71,13 @@ class DashboardsIndexContents extends Component { const mstp = (state: AppState): StateProps => { const { - dashboards: {list: dashboards}, cloud: { limits: {status}, }, } = state return { - dashboards, + dashboards: getAll(state, ResourceType.Dashboards), limitStatus: status, } } diff --git a/ui/src/dashboards/components/dashboard_index/Table.tsx b/ui/src/dashboards/components/dashboard_index/Table.tsx index bdce4e5c85..5f2efa1bf9 100644 --- a/ui/src/dashboards/components/dashboard_index/Table.tsx +++ b/ui/src/dashboards/components/dashboard_index/Table.tsx @@ -8,7 +8,7 @@ import _ from 'lodash' import {EmptyState, ResourceList} from '@influxdata/clockface' import AddResourceDropdown from 'src/shared/components/AddResourceDropdown' import DashboardCards from 'src/dashboards/components/dashboard_index/DashboardCards' -import {createDashboard, getDashboardsAsync} from 'src/dashboards/actions' +import {createDashboard, getDashboards} from 'src/dashboards/actions/thunks' import {getLabels} from 'src/labels/actions' // Types @@ -34,7 +34,7 @@ interface StateProps { } interface DispatchProps { - getDashboards: typeof getDashboardsAsync + getDashboards: typeof getDashboards onCreateDashboard: typeof createDashboard getLabels: typeof getLabels } @@ -159,7 +159,7 @@ class DashboardsTable extends PureComponent { } const mstp = (state: AppState): StateProps => { - const status = state.dashboards.status + const status = state.resources.dashboards.status return { status, @@ -167,7 +167,7 @@ const mstp = (state: AppState): StateProps => { } const mdtp: DispatchProps = { - getDashboards: getDashboardsAsync, + getDashboards: getDashboards, onCreateDashboard: createDashboard, getLabels: getLabels, } diff --git a/ui/src/dashboards/components/variablesControlBar/VariableDropdown.tsx b/ui/src/dashboards/components/variablesControlBar/VariableDropdown.tsx index 92df8cce60..756607b6b4 100644 --- a/ui/src/dashboards/components/variablesControlBar/VariableDropdown.tsx +++ b/ui/src/dashboards/components/variablesControlBar/VariableDropdown.tsx @@ -1,7 +1,6 @@ // Libraries import React, {PureComponent} from 'react' import {connect} from 'react-redux' -import _ from 'lodash' // Components import { @@ -11,7 +10,7 @@ import { } from '@influxdata/clockface' // Actions -import {selectVariableValue} from 'src/dashboards/actions/index' +import {selectVariableValue} from 'src/dashboards/actions/thunks' // Utils import {getVariableValuesForDropdown} from 'src/dashboards/selectors' diff --git a/ui/src/dashboards/index.ts b/ui/src/dashboards/index.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ui/src/dashboards/reducers/dashboards.test.ts b/ui/src/dashboards/reducers/dashboards.test.ts index 634c8971e8..ef35ae5fed 100644 --- a/ui/src/dashboards/reducers/dashboards.test.ts +++ b/ui/src/dashboards/reducers/dashboards.test.ts @@ -1,3 +1,9 @@ +// Libraries +import {normalize} from 'normalizr' + +// Schema +import * as schemas from 'src/schemas' + // Reducer import {dashboardsReducer as reducer} from 'src/dashboards/reducers/dashboards' @@ -10,86 +16,110 @@ import { removeCell, addDashboardLabel, removeDashboardLabel, -} from 'src/dashboards/actions/' +} from 'src/dashboards/actions/creators' // Resources import {dashboard} from 'src/dashboards/resources' import {labels} from 'mocks/dummyData' -import {RemoteDataState} from '@influxdata/clockface' + +// Types +import {RemoteDataState, DashboardEntities, Dashboard} from 'src/types' const status = RemoteDataState.Done +const initialState = () => ({ + status, + byID: { + [dashboard.id]: dashboard, + ['2']: {...dashboard, id: '2'}, + }, + allIDs: [dashboard.id, '2'], +}) + describe('dashboards reducer', () => { it('can set the dashboards', () => { - const list = [dashboard] + const schema = normalize( + [dashboard], + schemas.arrayOfDashboards + ) - const expected = {status, list} - const actual = reducer(undefined, setDashboards(status, list)) + const byID = schema.entities.dashboards + const allIDs = schema.result - expect(actual).toEqual(expected) + const actual = reducer(undefined, setDashboards(status, schema)) + + expect(actual.byID).toEqual(byID) + expect(actual.allIDs).toEqual(allIDs) }) it('can remove a dashboard', () => { - const d2 = {...dashboard, id: '2'} - const list = [dashboard, d2] - const expected = {list: [dashboard], status} - const actual = reducer({list, status}, removeDashboard(d2.id)) + const allIDs = [dashboard.id] + const byID = {[dashboard.id]: dashboard} + + const state = initialState() + const expected = {status, byID, allIDs} + const actual = reducer(state, removeDashboard(state.allIDs[1])) expect(actual).toEqual(expected) }) it('can set a dashboard', () => { - const loadedDashboard = {...dashboard, name: 'updated'} - const d2 = {...dashboard, id: '2'} - const state = {status, list: [dashboard, d2]} + const name = 'updated name' + const loadedDashboard = {...dashboard, name: 'updated name'} + const schema = normalize( + loadedDashboard, + schemas.dashboard + ) - const expected = {status, list: [loadedDashboard, d2]} - const actual = reducer(state, setDashboard(loadedDashboard)) + const state = initialState() - expect(actual).toEqual(expected) + const actual = reducer(state, setDashboard(schema)) + + expect(actual.byID[dashboard.id].name).toEqual(name) }) it('can edit a dashboard', () => { - const updates = {...dashboard, name: 'updated dash'} - const expected = {status, list: [updates]} - const actual = reducer({status, list: [dashboard]}, editDashboard(updates)) + const name = 'updated name' + const updates = {...dashboard, name} - expect(actual).toEqual(expected) + const schema = normalize( + updates, + schemas.dashboard + ) + + const state = initialState() + const actual = reducer(state, editDashboard(schema)) + + expect(actual.byID[dashboard.id].name).toEqual(name) }) it('can remove a cell from a dashboard', () => { - const expected = {status, list: [{...dashboard, cells: []}]} - const actual = reducer( - {status, list: [dashboard]}, - removeCell(dashboard, dashboard.cells[0]) - ) + const state = initialState() + const {id} = dashboard + const cellID = dashboard.cells[0].id + const actual = reducer(state, removeCell(id, cellID)) - expect(actual).toEqual(expected) + expect(actual.byID[id].cells).toEqual([]) }) it('can add labels to a dashboard', () => { - const dashboardWithoutLabels = {...dashboard, labels: []} - const expected = {status, list: [{...dashboard, labels: [labels[0]]}]} - const actual = reducer( - {status, list: [dashboardWithoutLabels]}, - addDashboardLabel(dashboardWithoutLabels.id, labels[0]) - ) + const {id} = dashboard + const state = initialState() + const label = labels[0] - expect(actual).toEqual(expected) + const actual = reducer(state, addDashboardLabel(id, label)) + + expect(actual.byID[id].labels).toEqual([label]) }) it('can remove labels from a dashboard', () => { - const leftOverLabel = {...labels[0], name: 'wowowowo', id: '3'} - const dashboardWithLabels = { - ...dashboard, - labels: [labels[0], leftOverLabel], - } - const expected = {status, list: [{...dashboard, labels: [leftOverLabel]}]} - const actual = reducer( - {status, list: [dashboardWithLabels]}, - removeDashboardLabel(dashboardWithLabels.id, labels[0]) - ) + const {id} = dashboard + const label = labels[0] - expect(actual).toEqual(expected) + const state = initialState() + const withLabel = reducer(state, addDashboardLabel(id, label)) + const actual = reducer(withLabel, removeDashboardLabel(id, labels[0].id)) + + expect(actual.byID[id].labels).toEqual([]) }) }) diff --git a/ui/src/dashboards/reducers/dashboards.ts b/ui/src/dashboards/reducers/dashboards.ts index 38159b1969..05f7a86f52 100644 --- a/ui/src/dashboards/reducers/dashboards.ts +++ b/ui/src/dashboards/reducers/dashboards.ts @@ -1,18 +1,38 @@ // Libraries import {produce} from 'immer' -import _ from 'lodash' // Types -import {Action, ActionTypes} from 'src/dashboards/actions' -import {Dashboard, RemoteDataState} from 'src/types' +import { + RemoteDataState, + ResourceState, + Dashboard, + ResourceType, +} from 'src/types' -export interface DashboardsState { - list: Dashboard[] - status: RemoteDataState -} +// Actions +import { + Action, + SET_DASHBOARD, + REMOVE_DASHBOARD, + SET_DASHBOARDS, + REMOVE_CELL, + REMOVE_DASHBOARD_LABEL, + ADD_DASHBOARD_LABEL, + EDIT_DASHBOARD, +} from 'src/dashboards/actions/creators' + +// Utils +import { + setResource, + removeResource, + editResource, +} from 'src/resources/reducers/helpers' + +type DashboardsState = ResourceState['dashboards'] const initialState = () => ({ - list: [], + byID: {}, + allIDs: [], status: RemoteDataState.NotStarted, }) @@ -22,83 +42,66 @@ export const dashboardsReducer = ( ): DashboardsState => { return produce(state, draftState => { switch (action.type) { - case ActionTypes.SetDashboards: { - const {list, status} = action.payload + case SET_DASHBOARDS: { + setResource(draftState, action, ResourceType.Dashboards) - draftState.status = status - if (list) { - draftState.list = list + return + } + + case REMOVE_DASHBOARD: { + removeResource(draftState, action) + + return + } + + case SET_DASHBOARD: { + const {schema} = action + const {entities, result} = schema + + draftState.byID[result] = entities.dashboards[result] + const exists = draftState.allIDs.find(id => id === result) + + if (!exists) { + draftState.allIDs.push(result) } return } - case ActionTypes.RemoveDashboard: { - const {id} = action.payload - draftState.list = draftState.list.filter(l => l.id !== id) + case EDIT_DASHBOARD: { + editResource(draftState, action, ResourceType.Dashboards) return } - case ActionTypes.SetDashboard: { - const {dashboard} = action.payload - draftState.list = _.unionBy([dashboard], state.list, 'id') + case REMOVE_CELL: { + const {dashboardID, cellID} = action + + const {cells} = draftState.byID[dashboardID] + + draftState.byID[dashboardID].cells = cells.filter( + cell => cell.id !== cellID + ) return } - case ActionTypes.EditDashboard: { - const {dashboard} = action.payload + case ADD_DASHBOARD_LABEL: { + const {dashboardID, label} = action - draftState.list = draftState.list.map(d => { - if (d.id === dashboard.id) { - return dashboard - } - return d - }) + draftState.byID[dashboardID].labels.push(label) return } - case ActionTypes.RemoveCell: { - const {dashboard, cell} = action.payload - draftState.list = draftState.list.map(d => { - if (d.id === dashboard.id) { - const cells = d.cells.filter(c => c.id !== cell.id) - d.cells = cells - } + case REMOVE_DASHBOARD_LABEL: { + const {dashboardID, labelID} = action - return d - }) + const {labels} = draftState.byID[dashboardID] - return - } - - case ActionTypes.AddDashboardLabel: { - const {dashboardID, label} = action.payload - - draftState.list = draftState.list.map(d => { - if (d.id === dashboardID) { - d.labels = [...d.labels, label] - } - - return d - }) - - return - } - - case ActionTypes.RemoveDashboardLabel: { - const {dashboardID, label} = action.payload - draftState.list = draftState.list.map(d => { - if (d.id === dashboardID) { - const updatedLabels = d.labels.filter(el => !(label.id === el.id)) - - d.labels = updatedLabels - } - - return d - }) + draftState.byID[dashboardID].labels = labels.filter( + label => label.id !== labelID + ) return } diff --git a/ui/src/dashboards/selectors/index.ts b/ui/src/dashboards/selectors/index.ts index 86c91289e9..f24136c78d 100644 --- a/ui/src/dashboards/selectors/index.ts +++ b/ui/src/dashboards/selectors/index.ts @@ -7,6 +7,8 @@ import { ViewType, RemoteDataState, TimeRange, + ResourceType, + Dashboard, } from 'src/types' import { @@ -17,6 +19,7 @@ import { // Constants import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges' +import {getByID} from 'src/resources/selectors' export const getView = (state: AppState, id: string): View => { return get(state, `views.views.${id}.view`) @@ -47,8 +50,10 @@ export const getViewsForDashboard = ( state: AppState, dashboardID: string ): View[] => { - const dashboard = state.dashboards.list.find( - dashboard => dashboard.id === dashboardID + const dashboard = getByID( + state, + ResourceType.Dashboards, + dashboardID ) const cellIDs = new Set(dashboard.cells.map(cell => cell.id)) diff --git a/ui/src/dataExplorer/components/SaveAsCellForm.tsx b/ui/src/dataExplorer/components/SaveAsCellForm.tsx index 3049eb48a1..475a7a31b2 100644 --- a/ui/src/dataExplorer/components/SaveAsCellForm.tsx +++ b/ui/src/dataExplorer/components/SaveAsCellForm.tsx @@ -6,6 +6,7 @@ import {get, isEmpty} from 'lodash' // Selectors import {getSaveableView} from 'src/timeMachine/selectors' import {getOrg} from 'src/organizations/selectors' +import {getAll} from 'src/resources/selectors' // Components import {Form, Input, Button, Grid} from '@influxdata/clockface' @@ -21,12 +22,12 @@ import { } from 'src/dashboards/constants' // Actions -import {getDashboardsAsync, createCellWithView} from 'src/dashboards/actions' -import {createDashboard} from 'src/dashboards/apis' +import {getDashboards, createCellWithView} from 'src/dashboards/actions/thunks' +import {postDashboard} from 'src/client' import {notify} from 'src/shared/actions/notifications' // Types -import {AppState, Dashboard, View} from 'src/types' +import {AppState, Dashboard, View, ResourceType} from 'src/types' import { Columns, InputType, @@ -49,7 +50,7 @@ interface StateProps { } interface DispatchProps { - handleGetDashboards: typeof getDashboardsAsync + onGetDashboards: typeof getDashboards onCreateCellWithView: typeof createCellWithView notify: typeof notify } @@ -70,8 +71,8 @@ class SaveAsCellForm extends PureComponent { } public componentDidMount() { - const {handleGetDashboards} = this.props - handleGetDashboards() + const {onGetDashboards} = this.props + onGetDashboards() } public render() { @@ -199,8 +200,14 @@ class SaveAsCellForm extends PureComponent { name: dashboardName || DEFAULT_DASHBOARD_NAME, cells: [], } - const dashboard = await createDashboard(newDashboard) - onCreateCellWithView(dashboard.id, view) + + 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) } @@ -237,18 +244,15 @@ class SaveAsCellForm extends PureComponent { } const mstp = (state: AppState): StateProps => { - const { - dashboards: {list: dashboards}, - } = state - const view = getSaveableView(state) const org = getOrg(state) + const dashboards = getAll(state, ResourceType.Dashboards) return {dashboards, view, orgID: get(org, 'id', '')} } const mdtp: DispatchProps = { - handleGetDashboards: getDashboardsAsync, + onGetDashboards: getDashboards, onCreateCellWithView: createCellWithView, notify, } diff --git a/ui/src/dataLoaders/components/lineProtocolWizard/verify/LineProtocolStepSwitcher.tsx b/ui/src/dataLoaders/components/lineProtocolWizard/verify/LineProtocolStepSwitcher.tsx index 28797282fb..de48947428 100644 --- a/ui/src/dataLoaders/components/lineProtocolWizard/verify/LineProtocolStepSwitcher.tsx +++ b/ui/src/dataLoaders/components/lineProtocolWizard/verify/LineProtocolStepSwitcher.tsx @@ -1,6 +1,5 @@ // Libraries import React, {PureComponent} from 'react' -import _ from 'lodash' // Components import {ErrorHandling} from 'src/shared/decorators/errors' @@ -8,9 +7,8 @@ import LineProtocol from 'src/dataLoaders/components/lineProtocolWizard/configur import LineProtocolVerifyStep from 'src/dataLoaders/components/lineProtocolWizard/verify/LineProtocolVerifyStep' // Types -import {LineProtocolStep} from 'src/types' import {LineProtocolStepProps} from 'src/dataLoaders/components/lineProtocolWizard/LineProtocolWizard' -import {Bucket} from 'src/types' +import {Bucket, LineProtocolStep} from 'src/types' interface Props { stepProps: LineProtocolStepProps diff --git a/ui/src/me/components/DashboardsList.tsx b/ui/src/me/components/DashboardsList.tsx index d14558abd5..448600e977 100644 --- a/ui/src/me/components/DashboardsList.tsx +++ b/ui/src/me/components/DashboardsList.tsx @@ -7,11 +7,13 @@ import {connect} from 'react-redux' import {EmptyState} from '@influxdata/clockface' // Types -import {Dashboard, Organization, AppState} from 'src/types' +import {Dashboard, Organization, AppState, ResourceType} from 'src/types' import {ComponentSize} from '@influxdata/clockface' // Selectors import {getOrg} from 'src/organizations/selectors' +import {getAll} from 'src/resources/selectors' + interface StateProps { dashboards: Dashboard[] org: Organization @@ -50,7 +52,7 @@ class DashboardList extends PureComponent { } const mstp = (state: AppState): StateProps => ({ - dashboards: state.dashboards.list, + dashboards: getAll(state, ResourceType.Dashboards), org: getOrg(state), }) diff --git a/ui/src/organizations/apis/index.ts b/ui/src/organizations/apis/index.ts index b97419c58f..a10de00bfe 100644 --- a/ui/src/organizations/apis/index.ts +++ b/ui/src/organizations/apis/index.ts @@ -5,7 +5,7 @@ import {getDashboards as apiGetDashboards} from 'src/client' import {Dashboard, Organization} from 'src/types' // Utils -import {addDashboardDefaults} from 'src/dashboards/actions' +import {addDashboardDefaults} from 'src/schemas' // CRUD APIs for Organizations and Organization resources // i.e. Organization Members, Buckets, Dashboards etc diff --git a/ui/src/resources/selectors/index.ts b/ui/src/resources/selectors/index.ts index dc865ae971..7356ec456a 100644 --- a/ui/src/resources/selectors/index.ts +++ b/ui/src/resources/selectors/index.ts @@ -4,11 +4,17 @@ import {get} from 'lodash' // Types import {AppState, ResourceType, RemoteDataState} from 'src/types' -export const getStatus = ({resources}: AppState, resource): RemoteDataState => { +export const getStatus = ( + {resources}: AppState, + resource: ResourceType +): RemoteDataState => { return resources[resource].status } -export const getAll = ({resources}: AppState, resource): R[] => { +export const getAll = ( + {resources}: AppState, + resource: ResourceType +): R[] => { const allIDs: string[] = resources[resource].allIDs const byID: {[uuid: string]: R} = resources[resource].byID return allIDs.map(id => byID[id]) @@ -27,9 +33,5 @@ export const getByID = ( const resource = get(byID, `${id}`) - if (!resource) { - throw new Error(`Could not find resource of type "${type}" with id "${id}"`) - } - return resource } diff --git a/ui/src/schemas/index.ts b/ui/src/schemas/index.ts index 38fe01eb42..6f9a1b6929 100644 --- a/ui/src/schemas/index.ts +++ b/ui/src/schemas/index.ts @@ -9,6 +9,7 @@ import { Label, RemoteDataState, Variable, + Dashboard, } from 'src/types' // Utils @@ -26,6 +27,56 @@ export const arrayOfAuths = [auth] export const bucket = new schema.Entity(ResourceType.Buckets) export const arrayOfBuckets = [bucket] +/* Cells */ + +// Defines the schema for the "cells" resource +// export const cell = new schema.Entity( +// ResourceType.Cells, +// {}, +// { +// processStrategy: (cell: Cell, parent: Dashboard) => ({ +// ...cell, +// dashboardID: parent.id, +// }), +// } +// ) +// export const arrayOfCells = [cell] + +/* Dashboards */ + +// Defines the schema for the "dashboards" resource +export const dashboard = new schema.Entity( + ResourceType.Dashboards, + {}, + { + processStrategy: (dashboard: Dashboard) => addDashboardDefaults(dashboard), + } +) +export const arrayOfDashboards = [dashboard] + +export const addDashboardDefaults = (dashboard: Dashboard): Dashboard => { + return { + ...dashboard, + id: dashboard.id || '', + labels: (dashboard.labels || []).map(addLabelDefaults), + name: dashboard.name || '', + orgID: dashboard.orgID || '', + meta: addDashboardMetaDefaults(dashboard.meta), + } +} + +const addDashboardMetaDefaults = (meta: Dashboard['meta']) => { + if (!meta) { + return {} + } + + if (!meta.updatedAt) { + return {...meta, updatedAt: new Date().toDateString()} + } + + return meta +} + /* Members */ // Defines the schema for the "members" resource @@ -39,6 +90,7 @@ export const org = new schema.Entity(ResourceType.Orgs) export const arrayOfOrgs = [org] /* Tasks */ + // Defines the schema for the tasks resource export const task = new schema.Entity( ResourceType.Tasks, diff --git a/ui/src/shared/components/GetResources.tsx b/ui/src/shared/components/GetResources.tsx index 672c764096..cfc963f989 100644 --- a/ui/src/shared/components/GetResources.tsx +++ b/ui/src/shared/components/GetResources.tsx @@ -3,20 +3,20 @@ import React, {PureComponent} from 'react' import {connect} from 'react-redux' // Actions -import {getLabels} from 'src/labels/actions' -import {getBuckets} from 'src/buckets/actions/thunks' -import {getTelegrafs} from 'src/telegrafs/actions/thunks' -import {getPlugins} from 'src/dataLoaders/actions/telegrafEditor' -import {getVariables} from 'src/variables/actions/thunks' -import {getScrapers} from 'src/scrapers/actions/thunks' -import {getDashboardsAsync} from 'src/dashboards/actions' -import {getTasks} from 'src/tasks/actions/thunks' import {getAuthorizations} from 'src/authorizations/actions/thunks' -import {getTemplates} from 'src/templates/actions' -import {getMembers} from 'src/members/actions/thunks' +import {getBuckets} from 'src/buckets/actions/thunks' import {getChecks} from 'src/alerting/actions/checks' -import {getNotificationRules} from 'src/alerting/actions/notifications/rules' +import {getDashboards} from 'src/dashboards/actions/thunks' import {getEndpoints} from 'src/alerting/actions/notifications/endpoints' +import {getLabels} from 'src/labels/actions' +import {getMembers} from 'src/members/actions/thunks' +import {getNotificationRules} from 'src/alerting/actions/notifications/rules' +import {getPlugins} from 'src/dataLoaders/actions/telegrafEditor' +import {getScrapers} from 'src/scrapers/actions/thunks' +import {getTasks} from 'src/tasks/actions/thunks' +import {getTelegrafs} from 'src/telegrafs/actions/thunks' +import {getTemplates} from 'src/templates/actions' +import {getVariables} from 'src/variables/actions/thunks' // Types import {AppState, RemoteDataState, ResourceType} from 'src/types' @@ -40,7 +40,7 @@ interface DispatchProps { getVariables: typeof getVariables getScrapers: typeof getScrapers getAuthorizations: typeof getAuthorizations - getDashboards: typeof getDashboardsAsync + getDashboards: typeof getDashboards getTasks: typeof getTasks getTemplates: typeof getTemplates getMembers: typeof getMembers @@ -160,7 +160,7 @@ const mdtp = { getVariables: getVariables, getScrapers: getScrapers, getAuthorizations: getAuthorizations, - getDashboards: getDashboardsAsync, + getDashboards: getDashboards, getTasks: getTasks, getTemplates: getTemplates, getMembers: getMembers, diff --git a/ui/src/shared/selectors/getResourcesStatus.ts b/ui/src/shared/selectors/getResourcesStatus.ts index 67d56b8666..dfc68b5edf 100644 --- a/ui/src/shared/selectors/getResourcesStatus.ts +++ b/ui/src/shared/selectors/getResourcesStatus.ts @@ -14,6 +14,7 @@ export const getResourcesStatus = ( case ResourceType.Tasks: case ResourceType.Scrapers: case ResourceType.Variables: + case ResourceType.Dashboards: case ResourceType.Authorizations: { return state.resources[resource].status } diff --git a/ui/src/store/configureStore.ts b/ui/src/store/configureStore.ts index a55cef42d0..f0a0b0abbe 100644 --- a/ui/src/store/configureStore.ts +++ b/ui/src/store/configureStore.ts @@ -55,7 +55,6 @@ export const rootReducer = combineReducers({ alertBuilder: alertBuilderReducer, checks: checksReducer, cloud: combineReducers<{limits: LimitsState}>({limits: limitsReducer}), - dashboards: dashboardsReducer, dataLoading: dataLoadingReducer, endpoints: endpointsReducer, labels: labelsReducer, @@ -75,6 +74,7 @@ export const rootReducer = combineReducers({ telegrafs: telegrafsReducer, tokens: authsReducer, variables: variablesReducer, + dashboards: dashboardsReducer, }), routing: routerReducer, rules: rulesReducer, diff --git a/ui/src/templates/actions/index.ts b/ui/src/templates/actions/index.ts index d659ce5978..5df12e4baf 100644 --- a/ui/src/templates/actions/index.ts +++ b/ui/src/templates/actions/index.ts @@ -28,7 +28,7 @@ import * as copy from 'src/shared/copy/notifications' // API import {client} from 'src/utils/api' -import {createDashboardFromTemplate} from 'src/dashboards/actions' +import {createDashboardFromTemplate} from 'src/dashboards/actions/thunks' import {createVariableFromTemplate} from 'src/variables/actions/thunks' import {createTaskFromTemplate} from 'src/tasks/actions/thunks' diff --git a/ui/src/templates/api/index.ts b/ui/src/templates/api/index.ts index 98ead6956d..1dc17b6699 100644 --- a/ui/src/templates/api/index.ts +++ b/ui/src/templates/api/index.ts @@ -35,7 +35,7 @@ import { postDashboardsCell as apiPostDashboardsCell, patchDashboardsCellsView as apiPatchDashboardsCellsView, } from 'src/client' -import {addDashboardDefaults} from 'src/dashboards/actions' +import {addDashboardDefaults} from 'src/schemas' // Create Dashboard Templates // Types @@ -80,7 +80,7 @@ export const createDashboardFromTemplate = async ( throw new Error(resp.data.message) } - const createdDashboard = addDashboardDefaults(resp.data) + const createdDashboard = addDashboardDefaults(resp.data as Dashboard) // associate imported label id with new label const labelMap = await createLabelsFromTemplate(template, orgID) @@ -98,7 +98,7 @@ export const createDashboardFromTemplate = async ( throw new Error(getResp.data.message) } - const dashboard = addDashboardDefaults(getResp.data) + const dashboard = addDashboardDefaults(getResp.data as Dashboard) return dashboard } diff --git a/ui/src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.tsx b/ui/src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.tsx index 1458e9d70c..6d5a69ae9e 100644 --- a/ui/src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.tsx +++ b/ui/src/templates/components/createFromTemplateOverlay/CreateFromTemplateOverlay.tsx @@ -2,7 +2,7 @@ import React, {PureComponent} from 'react' import {withRouter, WithRouterProps} from 'react-router' import {connect} from 'react-redux' -import _ from 'lodash' +import {sortBy} from 'lodash' // Components import { @@ -16,7 +16,7 @@ import TemplateBrowserEmpty from 'src/templates/components/createFromTemplateOve import GetResources from 'src/shared/components/GetResources' // Actions -import {createDashboardFromTemplate as createDashboardFromTemplateAction} from 'src/dashboards/actions' +import {createDashboardFromTemplate as createDashboardFromTemplateAction} from 'src/dashboards/actions/thunks' import {getTemplateByID} from 'src/templates/actions' // Constants @@ -191,7 +191,7 @@ const mstp = ({templates: {items, status}}: AppState): StateProps => { t => !t.meta.type || t.meta.type === TemplateType.Dashboard ) - const templates = _.sortBy(filteredTemplates, item => + const templates = sortBy(filteredTemplates, item => item.meta.name.toLocaleLowerCase() ) diff --git a/ui/src/types/resources.ts b/ui/src/types/resources.ts index 0961b513ad..ee8594962e 100644 --- a/ui/src/types/resources.ts +++ b/ui/src/types/resources.ts @@ -1,5 +1,6 @@ import { Bucket, + Dashboard, Authorization, Organization, Member, @@ -13,6 +14,7 @@ import { export enum ResourceType { Authorizations = 'tokens', Buckets = 'buckets', + Cells = 'cells', Checks = 'checks', Dashboards = 'dashboards', Labels = 'labels', @@ -54,4 +56,5 @@ export interface ResourceState { [ResourceType.Scrapers]: NormalizedState [ResourceType.Tasks]: TasksState [ResourceType.Variables]: VariablesState + [ResourceType.Dashboards]: NormalizedState } diff --git a/ui/src/types/schemas.ts b/ui/src/types/schemas.ts index c430cc0527..bbe375d403 100644 --- a/ui/src/types/schemas.ts +++ b/ui/src/types/schemas.ts @@ -1,6 +1,7 @@ // Types import { Task, + Dashboard, Variable, Telegraf, Member, @@ -26,6 +27,14 @@ export interface BucketEntities { } } +// DashboardEntities defines the result of normalizr's normalization +// of the "dashboards" resource +export interface DashboardEntities { + dashboards: { + [uuid: string]: Dashboard + } +} + // MemberEntities defines the result of normalizr's normalization // of the "members" resource export interface MemberEntities { diff --git a/ui/src/types/stores.ts b/ui/src/types/stores.ts index b830427c20..eabfde18dd 100644 --- a/ui/src/types/stores.ts +++ b/ui/src/types/stores.ts @@ -20,7 +20,6 @@ import {TemplatesState} from 'src/templates/reducers' import {RangeState} from 'src/dashboards/reducers/ranges' import {ViewsState} from 'src/dashboards/reducers/views' import {UserSettingsState} from 'src/userSettings/reducers' -import {DashboardsState} from 'src/dashboards/reducers/dashboards' import {OverlayState} from 'src/overlays/reducers/overlays' import {AutoRefreshState} from 'src/shared/reducers/autoRefresh' import {LimitsState} from 'src/cloud/reducers/limits' @@ -37,7 +36,6 @@ export interface AppState { autoRefresh: AutoRefreshState checks: ChecksState cloud: {limits: LimitsState} - dashboards: DashboardsState dataLoading: DataLoadingState endpoints: NotificationEndpointsState labels: LabelsState