From b6b5b4d9a7c69a6b04e6ac8eff2d87835665d5bb Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 3 Feb 2020 16:55:40 -0800 Subject: [PATCH] refactor: normalization checks (#16707) * refactor(checks): action types * feat: ive done too much * fix: tsc errors * fix: jest tests * chore: change Status to activeStatus * refactor: getResourceAtID --- ui/src/alerting/actions/alertBuilder.ts | 6 +- .../components/builder/MatchingRuleCard.tsx | 4 +- ui/src/alerting/constants/index.ts | 14 +- ui/src/alerting/reducers/alertBuilder.test.ts | 34 +- ui/src/alerting/reducers/alertBuilder.ts | 18 +- ui/src/alerting/utils/customCheck.test.ts | 8 +- ui/src/checks/actions/creators.ts | 63 ++++ ui/src/checks/actions/index.ts | 353 ------------------ ui/src/checks/actions/thunks.ts | 319 ++++++++++++++++ ui/src/checks/components/CheckCard.tsx | 40 +- ui/src/checks/components/CheckEOHeader.tsx | 6 +- ui/src/checks/components/CheckHistory.tsx | 3 +- .../components/CheckMatchingRulesCard.tsx | 22 +- ui/src/checks/components/ChecksColumn.tsx | 3 +- ui/src/checks/components/EditCheckEO.tsx | 20 +- .../checks/components/NewDeadmanCheckEO.tsx | 16 +- .../checks/components/NewThresholdCheckEO.tsx | 16 +- ui/src/checks/reducers/checks.test.ts | 108 ++++-- ui/src/checks/reducers/index.ts | 81 ++-- ui/src/checks/selectors/index.ts | 7 +- ui/src/checks/utils/index.ts | 125 +++++++ ui/src/cloud/actions/limits.ts | 27 +- ui/src/dashboards/selectors/index.ts | 4 +- .../notifications/endpoints/actions/thunks.ts | 20 +- .../endpoints/components/EndpointCard.tsx | 52 +-- .../components/EndpointOverlayContents.tsx | 2 +- .../notifications/endpoints/reducers/index.ts | 34 +- ui/src/notifications/endpoints/utils/index.ts | 13 + ui/src/notifications/rules/actions/thunks.ts | 6 +- .../rules/components/RuleCard.tsx | 48 ++- ui/src/notifications/rules/reducers/index.ts | 35 +- .../rules/reducers/rules.test.ts | 21 +- ui/src/notifications/rules/utils/index.ts | 13 +- ui/src/resources/components/GetResources.tsx | 2 +- ui/src/resources/reducers/helpers.ts | 15 +- .../resources/selectors/getResourcesStatus.ts | 1 + ui/src/schemas/index.ts | 38 +- ui/src/store/configureStore.ts | 2 +- ui/src/types/alerting.ts | 112 ++++-- ui/src/types/resources.ts | 8 +- ui/src/types/schemas.ts | 9 + ui/src/types/stores.ts | 2 - 42 files changed, 992 insertions(+), 738 deletions(-) create mode 100644 ui/src/checks/actions/creators.ts delete mode 100644 ui/src/checks/actions/index.ts create mode 100644 ui/src/checks/actions/thunks.ts create mode 100644 ui/src/checks/utils/index.ts create mode 100644 ui/src/notifications/endpoints/utils/index.ts diff --git a/ui/src/alerting/actions/alertBuilder.ts b/ui/src/alerting/actions/alertBuilder.ts index e18293991a6..a8f1061aae9 100644 --- a/ui/src/alerting/actions/alertBuilder.ts +++ b/ui/src/alerting/actions/alertBuilder.ts @@ -49,9 +49,9 @@ export const setAlertBuilderCheck = (check: Check) => ({ payload: {check}, }) -export const setAlertBuilderCheckStatus = (checkStatus: RemoteDataState) => ({ - type: 'SET_ALERT_BUILER_CHECK_STATUS' as 'SET_ALERT_BUILER_CHECK_STATUS', - payload: {checkStatus}, +export const setAlertBuilderCheckStatus = (status: RemoteDataState) => ({ + type: 'SET_ALERT_BUILDER_STATUS' as 'SET_ALERT_BUILDER_STATUS', + payload: {status}, }) export const changeCheckType = (toType: CheckType) => ({ diff --git a/ui/src/alerting/components/builder/MatchingRuleCard.tsx b/ui/src/alerting/components/builder/MatchingRuleCard.tsx index ca710b3c39f..634d89836e7 100644 --- a/ui/src/alerting/components/builder/MatchingRuleCard.tsx +++ b/ui/src/alerting/components/builder/MatchingRuleCard.tsx @@ -7,7 +7,7 @@ import {ResourceCard} from '@influxdata/clockface' // Types import { - NotificationRule, + NotificationRuleDraft, AppState, NotificationEndpoint, ResourceType, @@ -17,7 +17,7 @@ import { import {getAll} from 'src/resources/selectors' interface OwnProps { - rule: NotificationRule + rule: NotificationRuleDraft } interface StateProps { diff --git a/ui/src/alerting/constants/index.ts b/ui/src/alerting/constants/index.ts index a6a04ea5634..704fe23cafc 100644 --- a/ui/src/alerting/constants/index.ts +++ b/ui/src/alerting/constants/index.ts @@ -54,7 +54,7 @@ export const LEVEL_COMPONENT_COLORS = { export const DEFAULT_THRESHOLD_CHECK: Partial = { name: DEFAULT_CHECK_NAME, type: 'threshold', - status: 'active', + activeStatus: 'active', thresholds: [], every: DEFAULT_CHECK_EVERY, offset: DEFAULT_CHECK_OFFSET, @@ -79,11 +79,11 @@ export const DEFAULT_ENDPOINT_URLS = { export const NEW_ENDPOINT_DRAFT: NotificationEndpoint = { name: 'Name this Endpoint', description: '', - status: 'active', + activeStatus: 'active', type: 'slack', token: '', url: DEFAULT_ENDPOINT_URLS['slack'], - loadingStatus: RemoteDataState.Done, + status: RemoteDataState.Done, } export const NEW_ENDPOINT_FIXTURES: NotificationEndpoint[] = [ @@ -93,11 +93,11 @@ export const NEW_ENDPOINT_FIXTURES: NotificationEndpoint[] = [ userID: '1', description: 'interrupt everyone at work', name: 'Slack', - status: 'active', + activeStatus: 'active', type: 'slack', url: 'insert.slack.url.here', token: 'plerps', - loadingStatus: RemoteDataState.Done, + status: RemoteDataState.Done, }, { id: '3', @@ -105,10 +105,10 @@ export const NEW_ENDPOINT_FIXTURES: NotificationEndpoint[] = [ userID: '1', description: 'interrupt someone by all means known to man', name: 'PagerDuty', - status: 'active', + activeStatus: 'active', type: 'pagerduty', clientURL: 'insert.pagerduty.client.url.here', routingKey: 'plerps', - loadingStatus: RemoteDataState.Done, + status: RemoteDataState.Done, }, ] diff --git a/ui/src/alerting/reducers/alertBuilder.test.ts b/ui/src/alerting/reducers/alertBuilder.test.ts index 1f9ee830a4e..b5ac17a8644 100644 --- a/ui/src/alerting/reducers/alertBuilder.test.ts +++ b/ui/src/alerting/reducers/alertBuilder.test.ts @@ -19,12 +19,24 @@ import { updateThresholds, } from 'src/alerting/actions/alertBuilder' import {CHECK_FIXTURE_1, CHECK_FIXTURE_3} from 'src/checks/reducers/checks.test' -import {RemoteDataState, Threshold} from 'src/types' +import {RemoteDataState, Threshold, TaskStatusType} from 'src/types' + +const check_1 = { + ...CHECK_FIXTURE_1, + activeStatus: 'inactive' as TaskStatusType, + status: RemoteDataState.Done, +} + +const check_3 = { + ...CHECK_FIXTURE_3, + activeStatus: 'active' as TaskStatusType, + status: RemoteDataState.Done, +} const mockState = (): AlertBuilderState => ({ id: '3', type: 'deadman', - status: 'active', + activeStatus: 'active', name: 'just', every: '1m', offset: '2m', @@ -35,7 +47,7 @@ const mockState = (): AlertBuilderState => ({ staleTime: '2m', level: 'OK', thresholds: [], - checkStatus: RemoteDataState.Done, + status: RemoteDataState.Done, }) describe('alertBuilderReducer', () => { @@ -52,10 +64,10 @@ describe('alertBuilderReducer', () => { it('Loads threshold check properties in to alert builder', () => { const actual = alertBuilderReducer( initialState(), - setAlertBuilderCheck(CHECK_FIXTURE_1) + setAlertBuilderCheck(check_1) ) - const expected = CHECK_FIXTURE_1 + const expected = check_1 expect(actual.type).toEqual(expected.type) expect(actual.name).toEqual(expected.name) @@ -66,16 +78,16 @@ describe('alertBuilderReducer', () => { ) expect(actual.tags).toEqual(expected.tags) expect(actual.thresholds).toEqual(expected.thresholds) - expect(actual.checkStatus).toEqual(RemoteDataState.Done) + expect(actual.status).toEqual(RemoteDataState.Done) }) it('Loads deadman check properties in to alert builder', () => { const actual = alertBuilderReducer( initialState(), - setAlertBuilderCheck(CHECK_FIXTURE_3) + setAlertBuilderCheck(check_3) ) - const expected = CHECK_FIXTURE_3 + const expected = check_3 expect(actual.type).toEqual(expected.type) expect(actual.name).toEqual(expected.name) @@ -90,13 +102,13 @@ describe('alertBuilderReducer', () => { expect(actual.staleTime).toEqual(expected.staleTime) expect(actual.reportZero).toEqual(expected.reportZero) expect(actual.level).toEqual(expected.level) - expect(actual.checkStatus).toEqual(RemoteDataState.Done) + expect(actual.status).toEqual(RemoteDataState.Done) }) }) describe('setAlertBuilderCheckStatus', () => { it('check status is initialized to Not Started', () => { - expect(initialState().checkStatus).toEqual(RemoteDataState.NotStarted) + expect(initialState().status).toEqual(RemoteDataState.NotStarted) }) it('sets check status', () => { const newStatus = RemoteDataState.Error @@ -104,7 +116,7 @@ describe('alertBuilderReducer', () => { initialState(), setAlertBuilderCheckStatus(newStatus) ) - expect(actual.checkStatus).toEqual(newStatus) + expect(actual.status).toEqual(newStatus) }) }) diff --git a/ui/src/alerting/reducers/alertBuilder.ts b/ui/src/alerting/reducers/alertBuilder.ts index 7b3fd0629c1..8b1dd7931b3 100644 --- a/ui/src/alerting/reducers/alertBuilder.ts +++ b/ui/src/alerting/reducers/alertBuilder.ts @@ -16,7 +16,9 @@ import { DEFAULT_CHECK_TAGS, } from 'src/alerting/constants' -type FromBase = Required> +type FromBase = Required< + Pick +> type FromThreshold = Required< Pick< @@ -34,12 +36,12 @@ export interface AlertBuilderState FromThreshold, FromDeadman { type: CheckType - checkStatus: RemoteDataState } export const initialState = (): AlertBuilderState => ({ id: null, - status: 'active', + activeStatus: 'active', + status: RemoteDataState.NotStarted, type: 'threshold', name: DEFAULT_CHECK_NAME, every: DEFAULT_CHECK_EVERY, @@ -51,7 +53,6 @@ export const initialState = (): AlertBuilderState => ({ staleTime: '10m', level: DEFAULT_DEADMAN_LEVEL, thresholds: [], - checkStatus: RemoteDataState.NotStarted, }) export default ( @@ -67,7 +68,7 @@ export default ( return { ...initialState(), type: action.payload.type, - checkStatus: RemoteDataState.Done, + status: RemoteDataState.Done, } } @@ -80,7 +81,7 @@ export default ( const newState = { ...initialState(), - checkStatus: RemoteDataState.Done, + status: RemoteDataState.Done, id, name, query, @@ -90,6 +91,7 @@ export default ( if (action.payload.check.type === 'custom') { return newState } + if (action.payload.check.type === 'threshold') { const { every, @@ -137,8 +139,8 @@ export default ( ) } - case 'SET_ALERT_BUILER_CHECK_STATUS': { - return {...state, checkStatus: action.payload.checkStatus} + case 'SET_ALERT_BUILDER_STATUS': { + return {...state, status: action.payload.status} } case 'SET_ALERT_BUILDER_EVERY': { diff --git a/ui/src/alerting/utils/customCheck.test.ts b/ui/src/alerting/utils/customCheck.test.ts index b5756e747cd..f6e82199494 100644 --- a/ui/src/alerting/utils/customCheck.test.ts +++ b/ui/src/alerting/utils/customCheck.test.ts @@ -9,8 +9,8 @@ const bc1: BuilderConfig = { } const ab1: AlertBuilderState = { - status: 'active', - checkStatus: RemoteDataState.Done, + activeStatus: 'active', + status: RemoteDataState.Done, statusMessageTemplate: 'this is staus message', tags: [{key: 'k1', value: 'v1'}], id: '2', @@ -30,8 +30,8 @@ const ab1: AlertBuilderState = { } const ab2: AlertBuilderState = { - status: 'active', - checkStatus: RemoteDataState.Done, + activeStatus: 'active', + status: RemoteDataState.Done, statusMessageTemplate: 'this is staus message', tags: [{key: 'k1', value: 'v1'}], id: '2', diff --git a/ui/src/checks/actions/creators.ts b/ui/src/checks/actions/creators.ts new file mode 100644 index 00000000000..f48cbd7aa98 --- /dev/null +++ b/ui/src/checks/actions/creators.ts @@ -0,0 +1,63 @@ +// Types +import {RemoteDataState, Label, CheckEntities} from 'src/types' +import {NormalizedSchema} from 'normalizr' + +export type Action = + | ReturnType + | ReturnType + | ReturnType + | ReturnType + | ReturnType + +export const SET_CHECKS = 'SET_CHECKS' +export const SET_CHECK = 'SET_CHECK' +export const REMOVE_CHECK = 'REMOVE_CHECK' +export const ADD_LABEL_TO_CHECK = 'ADD_LABEL_TO_CHECK' +export const REMOVE_LABEL_FROM_CHECK = 'REMOVE_LABEL_FROM_CHECK' + +type ChecksSchema = NormalizedSchema< + CheckEntities, + R +> + +export const setChecks = ( + status: RemoteDataState, + schema?: ChecksSchema +) => + ({ + type: SET_CHECKS, + status, + schema, + } as const) + +export const setCheck = ( + id: string, + status: RemoteDataState, + schema?: ChecksSchema +) => + ({ + type: SET_CHECK, + id, + status, + schema, + } as const) + +export const removeCheck = (id: string) => + ({ + type: REMOVE_CHECK, + id, + } as const) + +export const addLabelToCheck = (checkID: string, label: Label) => + ({ + type: ADD_LABEL_TO_CHECK, + checkID, + label, + } as const) + +export const removeLabelFromCheck = (checkID: string, labelID: string) => + ({ + type: REMOVE_LABEL_FROM_CHECK, + checkID, + labelID, + } as const) diff --git a/ui/src/checks/actions/index.ts b/ui/src/checks/actions/index.ts deleted file mode 100644 index f643c0473bb..00000000000 --- a/ui/src/checks/actions/index.ts +++ /dev/null @@ -1,353 +0,0 @@ -// Libraries -import {Dispatch} from 'react' -import {push} from 'react-router-redux' -import {get} from 'lodash' - -// Constants -import * as copy from 'src/shared/copy/notifications' - -// APIs -import * as api from 'src/client' - -// Utils -import {getActiveTimeMachine} from 'src/timeMachine/selectors' -import {incrementCloneName} from 'src/utils/naming' -import {reportError} from 'src/shared/utils/errors' -import {isDurationParseable} from 'src/shared/utils/duration' -import {checkThresholdsValid} from '../utils/checkValidate' -import {createView} from 'src/views/helpers' -import {getOrg} from 'src/organizations/selectors' - -// Actions -import { - notify, - Action as NotificationAction, -} from 'src/shared/actions/notifications' -import { - Action as TimeMachineAction, - setActiveTimeMachine, -} from 'src/timeMachine/actions' -import { - Action as AlertBuilderAction, - setAlertBuilderCheck, - setAlertBuilderCheckStatus, - resetAlertBuilder, -} from 'src/alerting/actions/alertBuilder' -import {checkChecksLimits} from 'src/cloud/actions/limits' - -// Types -import { - Check, - GetState, - RemoteDataState, - CheckViewProperties, - Label, - PostCheck, - CheckPatch, - ThresholdCheck, - DeadmanCheck, -} from 'src/types' - -export type Action = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - -export const setAllChecks = (status: RemoteDataState, checks?: Check[]) => ({ - type: 'SET_ALL_CHECKS' as 'SET_ALL_CHECKS', - payload: {status, checks}, -}) - -export const setCheck = (check: Check) => ({ - type: 'SET_CHECK' as 'SET_CHECK', - payload: {check}, -}) - -export const removeCheck = (checkID: string) => ({ - type: 'REMOVE_CHECK' as 'REMOVE_CHECK', - payload: {checkID}, -}) - -export const addLabelToCheck = (checkID: string, label: Label) => ({ - type: 'ADD_LABEL_TO_CHECK' as 'ADD_LABEL_TO_CHECK', - payload: {checkID, label}, -}) - -export const removeLabelFromCheck = (checkID: string, label: Label) => ({ - type: 'REMOVE_LABEL_FROM_CHECK' as 'REMOVE_LABEL_FROM_CHECK', - payload: {checkID, label}, -}) - -export const getChecks = () => async ( - dispatch: Dispatch< - Action | NotificationAction | ReturnType - >, - getState: GetState -) => { - try { - dispatch(setAllChecks(RemoteDataState.Loading)) - const {id: orgID} = getOrg(getState()) - - const resp = await api.getChecks({query: {orgID}}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - dispatch(setAllChecks(RemoteDataState.Done, resp.data.checks)) - dispatch(checkChecksLimits()) - } catch (e) { - console.error(e) - dispatch(setAllChecks(RemoteDataState.Error)) - dispatch(notify(copy.getChecksFailed(e.message))) - } -} - -export const getCheckForTimeMachine = (checkID: string) => async ( - dispatch: Dispatch< - TimeMachineAction | NotificationAction | AlertBuilderAction - >, - getState: GetState -) => { - const org = getOrg(getState()) - try { - dispatch(setAlertBuilderCheckStatus(RemoteDataState.Loading)) - - const resp = await api.getCheck({checkID}) - - if (resp.status !== 200) { - throw new Error(resp.data.message) - } - - const check = resp.data - - const view = createView(check.type) - - view.properties.queries = [check.query] - - dispatch( - setActiveTimeMachine('alerting', { - view, - activeTab: check.type === 'custom' ? 'customCheckQuery' : 'alerting', - }) - ) - dispatch(setAlertBuilderCheck(check)) - } catch (e) { - console.error(e) - dispatch(push(`/orgs/${org.id}/alerting`)) - dispatch(setAlertBuilderCheckStatus(RemoteDataState.Error)) - dispatch(notify(copy.getCheckFailed(e.message))) - } -} - -export const saveCheckFromTimeMachine = () => async ( - dispatch: Dispatch, - getState: GetState -) => { - try { - const state = getState() - const { - alertBuilder: { - type, - id, - status, - name, - every, - offset, - tags, - statusMessageTemplate, - timeSince, - reportZero, - staleTime, - level, - thresholds, - }, - } = state - const {id: orgID} = getOrg(state) - - const {draftQueries} = getActiveTimeMachine(state) - - let check = { - id, - type, - status, - name, - query: draftQueries[0], - orgID, - } as Check - - if (check.type === 'threshold') { - check = { - ...check, - every, - offset, - statusMessageTemplate, - tags, - thresholds, - } as ThresholdCheck - checkThresholdsValid(thresholds) - } else if (check.type === 'deadman') { - check = { - ...check, - every, - level, - offset, - reportZero, - staleTime, - statusMessageTemplate, - tags, - timeSince, - } as DeadmanCheck - if (!isDurationParseable(timeSince) || !isDurationParseable(staleTime)) { - throw new Error('Duration fields must contain valid duration') - } - } - - if (!isDurationParseable(offset)) { - throw new Error('Check offset must be a valid duration') - } - if (!isDurationParseable(every)) { - throw new Error('Check every must be a valid duration') - } - - const resp = check.id - ? await updateCheckFromTimeMachine(check) - : await api.postCheck({data: check}) - - if (resp.status === 200 || resp.status === 201) { - dispatch(setCheck(resp.data)) - dispatch(checkChecksLimits()) - - dispatch(push(`/orgs/${orgID}/alerting`)) - dispatch(resetAlertBuilder()) - } else { - throw new Error(resp.data.message) - } - } catch (error) { - console.error(error) - dispatch(notify(copy.createCheckFailed(error.message))) - reportError(error, { - context: {state: getState()}, - name: 'saveCheckFromTimeMachine function', - }) - } -} - -export const updateCheckDisplayProperties = ( - checkID: string, - update: CheckPatch -) => async (dispatch: Dispatch) => { - const resp = await api.patchCheck({checkID, data: update}) - - if (resp.status === 200) { - dispatch(setCheck(resp.data)) - } else { - throw new Error(resp.data.message) - } - dispatch(setCheck(resp.data)) -} - -const updateCheckFromTimeMachine = async (check: Check) => { - // todo: refactor after https://github.com/influxdata/influxdb/issues/16317 - const getCheckResponse = await api.getCheck({checkID: check.id}) - - if (getCheckResponse.status !== 200) { - throw new Error(getCheckResponse.data.message) - } - - return api.putCheck({ - checkID: check.id, - data: {...check, ownerID: getCheckResponse.data.ownerID}, - }) -} - -export const deleteCheck = (checkID: string) => async ( - dispatch: Dispatch -) => { - try { - const resp = await api.deleteCheck({checkID}) - - if (resp.status === 204) { - dispatch(removeCheck(checkID)) - } else { - throw new Error(resp.data.message) - } - - dispatch(removeCheck(checkID)) - dispatch(checkChecksLimits()) - } catch (e) { - console.error(e) - dispatch(notify(copy.deleteCheckFailed(e.message))) - } -} - -export const addCheckLabel = (checkID: string, label: Label) => async ( - dispatch: Dispatch -) => { - try { - const resp = await api.postChecksLabel({checkID, data: {labelID: label.id}}) - - if (resp.status !== 201) { - throw new Error(resp.data.message) - } - - dispatch(addLabelToCheck(checkID, label)) - } catch (e) { - console.error(e) - } -} - -export const deleteCheckLabel = (checkID: string, label: Label) => async ( - dispatch: Dispatch -) => { - try { - const resp = await api.deleteChecksLabel({ - checkID, - labelID: label.id, - }) - - if (resp.status !== 204) { - throw new Error(resp.data.message) - } - - dispatch(removeLabelFromCheck(checkID, label)) - } catch (e) { - console.error(e) - } -} - -export const cloneCheck = (check: Check) => async ( - dispatch: Dispatch< - Action | NotificationAction | ReturnType - >, - getState: GetState -): Promise => { - try { - const { - checks: {list}, - } = getState() - - const allCheckNames = list.map(c => c.name) - - const clonedName = incrementCloneName(allCheckNames, check.name) - const labels = get(check, 'labels', []) as Label[] - const data = { - ...check, - name: clonedName, - labels: labels.map(l => l.id), - } as PostCheck - const resp = await api.postCheck({data}) - - if (resp.status !== 201) { - throw new Error(resp.data.message) - } - - dispatch(setCheck(resp.data)) - dispatch(checkChecksLimits()) - } catch (error) { - console.error(error) - dispatch(notify(copy.createCheckFailed(error.message))) - } -} diff --git a/ui/src/checks/actions/thunks.ts b/ui/src/checks/actions/thunks.ts new file mode 100644 index 00000000000..0bba6636666 --- /dev/null +++ b/ui/src/checks/actions/thunks.ts @@ -0,0 +1,319 @@ +// Libraries +import {Dispatch} from 'react' +import {push} from 'react-router-redux' +import {normalize} from 'normalizr' + +// Constants +import * as copy from 'src/shared/copy/notifications' + +// APIs +import * as api from 'src/client' +import {checkSchema, arrayOfChecks} from 'src/schemas' + +// Utils +import {incrementCloneName} from 'src/utils/naming' +import {reportError} from 'src/shared/utils/errors' +import {createView} from 'src/views/helpers' +import {getOrg} from 'src/organizations/selectors' +import {toPostCheck, builderToPostCheck} from 'src/checks/utils' +import {getAll} from 'src/resources/selectors' + +// Actions +import { + notify, + Action as NotificationAction, +} from 'src/shared/actions/notifications' +import { + Action as TimeMachineAction, + setActiveTimeMachine, +} from 'src/timeMachine/actions' +import { + Action as AlertBuilderAction, + setAlertBuilderCheck, + setAlertBuilderCheckStatus, + resetAlertBuilder, +} from 'src/alerting/actions/alertBuilder' +import { + Action, + setChecks, + setCheck, + removeCheck, + addLabelToCheck, + removeLabelFromCheck, +} from 'src/checks/actions/creators' +import {checkChecksLimits} from 'src/cloud/actions/limits' + +// Types +import { + Check, + GetState, + RemoteDataState, + CheckViewProperties, + Label, + CheckPatch, + CheckEntities, + ResourceType, +} from 'src/types' + +export const getChecks = () => async ( + dispatch: Dispatch< + Action | NotificationAction | ReturnType + >, + getState: GetState +) => { + try { + dispatch(setChecks(RemoteDataState.Loading)) + const {id: orgID} = getOrg(getState()) + + const resp = await api.getChecks({query: {orgID}}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const checks = normalize( + resp.data.checks, + arrayOfChecks + ) + + dispatch(setChecks(RemoteDataState.Done, checks)) + dispatch(checkChecksLimits()) + } catch (e) { + console.error(e) + dispatch(setChecks(RemoteDataState.Error)) + dispatch(notify(copy.getChecksFailed(e.message))) + } +} + +export const getCheckForTimeMachine = (checkID: string) => async ( + dispatch: Dispatch< + TimeMachineAction | NotificationAction | AlertBuilderAction + >, + getState: GetState +) => { + const org = getOrg(getState()) + try { + dispatch(setAlertBuilderCheckStatus(RemoteDataState.Loading)) + + const resp = await api.getCheck({checkID}) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const check = resp.data + + const view = createView(check.type) + + view.properties.queries = [check.query] + + dispatch( + setActiveTimeMachine('alerting', { + view, + activeTab: check.type === 'custom' ? 'customCheckQuery' : 'alerting', + }) + ) + + const normCheck = normalize( + resp.data, + checkSchema + ) + + const builderCheck = normCheck.entities.checks[normCheck.result] + + dispatch(setAlertBuilderCheck(builderCheck)) + } catch (error) { + console.error(error) + dispatch(push(`/orgs/${org.id}/alerting`)) + dispatch(setAlertBuilderCheckStatus(RemoteDataState.Error)) + dispatch(notify(copy.getCheckFailed(error.message))) + } +} + +type SendToTimeMachineAction = + | ReturnType + | ReturnType + | ReturnType + | NotificationAction + +export const createCheckFromTimeMachine = () => async ( + dispatch: Dispatch, + getState: GetState +): Promise => { + try { + const state = getState() + const check = builderToPostCheck(state) + const resp = await api.postCheck({data: check}) + if (resp.status !== 201) { + throw new Error(resp.data.message) + } + + const normCheck = normalize( + resp.data, + checkSchema + ) + + dispatch(setCheck(resp.data.id, RemoteDataState.Done, normCheck)) + dispatch(checkChecksLimits()) + + dispatch(push(`/orgs/${check.orgID}/alerting`)) + dispatch(resetAlertBuilder()) + } catch (error) { + console.error(error) + dispatch(notify(copy.createCheckFailed(error.message))) + reportError(error, { + context: {state: getState()}, + name: 'saveCheckFromTimeMachine function', + }) + } +} + +export const updateCheckFromTimeMachine = () => async ( + dispatch: Dispatch, + getState: GetState +) => { + const state = getState() + const check = builderToPostCheck(state) + // todo: refactor after https://github.com/influxdata/influxdb/issues/16317 + try { + const getCheckResponse = await api.getCheck({checkID: check.id}) + + if (getCheckResponse.status !== 200) { + throw new Error(getCheckResponse.data.message) + } + + const resp = await api.putCheck({ + checkID: check.id, + data: {...check, ownerID: getCheckResponse.data.ownerID}, + }) + + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const normCheck = normalize( + resp.data, + checkSchema + ) + + dispatch(setCheck(resp.data.id, RemoteDataState.Done, normCheck)) + dispatch(checkChecksLimits()) + + dispatch(push(`/orgs/${check.orgID}/alerting`)) + dispatch(resetAlertBuilder()) + } catch (error) { + console.error(error) + dispatch(notify(copy.updateCheckFailed(error.message))) + reportError(error, { + context: {state: getState()}, + name: 'saveCheckFromTimeMachine function', + }) + } +} + +export const updateCheckDisplayProperties = ( + checkID: string, + update: CheckPatch +) => async (dispatch: Dispatch) => { + try { + const resp = await api.patchCheck({checkID, data: update}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + + const check = normalize( + resp.data, + checkSchema + ) + + dispatch(setCheck(checkID, RemoteDataState.Done, check)) + } catch (error) { + console.error(error) + } +} + +export const deleteCheck = (checkID: string) => async ( + dispatch: Dispatch +) => { + try { + const resp = await api.deleteCheck({checkID}) + + if (resp.status !== 204) { + throw new Error(resp.data.message) + } + + dispatch(removeCheck(checkID)) + dispatch(checkChecksLimits()) + } catch (error) { + console.error(error) + dispatch(notify(copy.deleteCheckFailed(error.message))) + } +} + +export const addCheckLabel = (checkID: string, label: Label) => async ( + dispatch: Dispatch +) => { + try { + const resp = await api.postChecksLabel({checkID, data: {labelID: label.id}}) + + if (resp.status !== 201) { + throw new Error(resp.data.message) + } + + dispatch(addLabelToCheck(checkID, label)) + } catch (error) { + console.error(error) + } +} + +export const deleteCheckLabel = (checkID: string, labelID: string) => async ( + dispatch: Dispatch +) => { + try { + const resp = await api.deleteChecksLabel({ + checkID, + labelID, + }) + + if (resp.status !== 204) { + throw new Error(resp.data.message) + } + + dispatch(removeLabelFromCheck(checkID, labelID)) + } catch (error) { + console.error(error) + } +} + +export const cloneCheck = (check: Check) => async ( + dispatch: Dispatch< + Action | NotificationAction | ReturnType + >, + getState: GetState +): Promise => { + try { + const state = getState() + const checks = getAll(state, ResourceType.Checks) + const allCheckNames = checks.map(c => c.name) + const clonedName = incrementCloneName(allCheckNames, check.name) + + const data = toPostCheck({...check, name: clonedName}) + + const resp = await api.postCheck({data}) + + if (resp.status !== 201) { + throw new Error(resp.data.message) + } + + const normCheck = normalize( + resp.data, + checkSchema + ) + + dispatch(setCheck(resp.data.id, RemoteDataState.Done, normCheck)) + dispatch(checkChecksLimits()) + } catch (error) { + console.error(error) + dispatch(notify(copy.createCheckFailed(error.message))) + } +} diff --git a/ui/src/checks/components/CheckCard.tsx b/ui/src/checks/components/CheckCard.tsx index db5e681475f..48b88758f3a 100644 --- a/ui/src/checks/components/CheckCard.tsx +++ b/ui/src/checks/components/CheckCard.tsx @@ -1,5 +1,5 @@ // Libraries -import React, {FunctionComponent} from 'react' +import React, {FC} from 'react' import {connect} from 'react-redux' import {withRouter, WithRouterProps} from 'react-router' @@ -20,7 +20,7 @@ import { addCheckLabel, deleteCheckLabel, cloneCheck, -} from 'src/checks/actions' +} from 'src/checks/actions/thunks' import {viewableLabels} from 'src/labels/selectors' import {notify} from 'src/shared/actions/notifications' import {updateCheckFailed} from 'src/shared/copy/notifications' @@ -50,7 +50,7 @@ interface OwnProps { type Props = OwnProps & DispatchProps & WithRouterProps & StateProps -const CheckCard: FunctionComponent = ({ +const CheckCard: FC = ({ onRemoveCheckLabel, onAddCheckLabel, onCloneCheck, @@ -62,11 +62,13 @@ const CheckCard: FunctionComponent = ({ labels, router, }) => { + const {id, activeStatus, name, description} = check + const onUpdateName = (name: string) => { try { onUpdateCheckDisplayProperties(check.id, {name}) - } catch (e) { - onNotify(updateCheckFailed(e.message)) + } catch (error) { + onNotify(updateCheckFailed(error.message)) } } @@ -87,38 +89,38 @@ const CheckCard: FunctionComponent = ({ } const onToggle = () => { - const status = check.status === 'active' ? 'inactive' : 'active' + const status = activeStatus === 'active' ? 'inactive' : 'active' try { - onUpdateCheckDisplayProperties(check.id, {status}) - } catch (e) { - onNotify(updateCheckFailed(e.message)) + onUpdateCheckDisplayProperties(id, {status}) + } catch (error) { + onNotify(updateCheckFailed(error.message)) } } const onCheckClick = () => { - router.push(`/orgs/${orgID}/alerting/checks/${check.id}/edit`) + router.push(`/orgs/${orgID}/alerting/checks/${id}/edit`) } const onView = () => { const queryParams = new URLSearchParams({ - [SEARCH_QUERY_PARAM]: `"checkID" == "${check.id}"`, + [SEARCH_QUERY_PARAM]: `"checkID" == "${id}"`, }) - router.push(`/orgs/${orgID}/checks/${check.id}/?${queryParams}`) + router.push(`/orgs/${orgID}/checks/${id}/?${queryParams}`) } const handleAddCheckLabel = (label: Label) => { - onAddCheckLabel(check.id, label) + onAddCheckLabel(id, label) } const handleRemoveCheckLabel = (label: Label) => { - onRemoveCheckLabel(check.id, label) + onRemoveCheckLabel(id, label.id) } return ( = ({ } toggle={ = ({ description={ } labels={ @@ -154,7 +156,7 @@ const CheckCard: FunctionComponent = ({ onRemoveLabel={handleRemoveCheckLabel} /> } - disabled={check.status === 'inactive'} + disabled={activeStatus === 'inactive'} contextMenu={ void onCancel: () => void - onSave: () => void + onSave: typeof createCheckFromTimeMachine | typeof updateCheckFromTimeMachine } interface StateProps { diff --git a/ui/src/checks/components/CheckHistory.tsx b/ui/src/checks/components/CheckHistory.tsx index 16f4241019c..589a174bc9c 100644 --- a/ui/src/checks/components/CheckHistory.tsx +++ b/ui/src/checks/components/CheckHistory.tsx @@ -24,6 +24,7 @@ import {getCheckIDs} from 'src/checks/selectors' // Types import {ResourceIDs} from 'src/checks/reducers' import {AppState, Check, TimeZone, ResourceType} from 'src/types' +import {getByID} from 'src/resources/selectors' interface OwnProps { params: {orgID: string; checkID: string} @@ -96,9 +97,9 @@ const CheckHistory: FC = ({ } const mstp = (state: AppState, props: OwnProps) => { - const check = state.checks.list.find(({id}) => id === props.params.checkID) const timeZone = state.app.persisted.timeZone const checkIDs = getCheckIDs(state) + const check = getByID(state, ResourceType.Checks, props.params.checkID) const resourceIDs = { checkIDs, diff --git a/ui/src/checks/components/CheckMatchingRulesCard.tsx b/ui/src/checks/components/CheckMatchingRulesCard.tsx index 8a5cf3d33af..4a157b69bda 100644 --- a/ui/src/checks/components/CheckMatchingRulesCard.tsx +++ b/ui/src/checks/components/CheckMatchingRulesCard.tsx @@ -14,20 +14,25 @@ import { AlignItems, } from '@influxdata/clockface' -// Selectors +// Selectors & Utils import {getOrg} from 'src/organizations/selectors' +import {ruleToDraftRule} from 'src/notifications/rules/utils' // API import {getNotificationRules as apiGetNotificationRules} from 'src/client' //Types -import {NotificationRule, AppState, CheckTagSet} from 'src/types' +import { + NotificationRule, + AppState, + CheckTagSet, + GenRule, + NotificationRuleDraft, +} from 'src/types' import {EmptyState, ComponentSize, RemoteDataState} from '@influxdata/clockface' import BuilderCard from 'src/timeMachine/components/builderCard/BuilderCard' import {getActiveTimeMachine} from 'src/timeMachine/selectors' -// Selectors - interface StateProps { tags: CheckTagSet[] orgID: string @@ -75,10 +80,9 @@ const CheckMatchingRulesCard: FC = ({ return } - const matchingRules = resp.data.notificationRules.map(r => ({ - ...r, - loadingStatus: RemoteDataState.Done, - })) + const matchingRules: NotificationRuleDraft[] = resp.data.notificationRules.map( + (r: GenRule) => ruleToDraftRule(r) + ) setMatchingRules({ matchingRules, @@ -87,7 +91,7 @@ const CheckMatchingRulesCard: FC = ({ } const [{matchingRules, status}, setMatchingRules] = useState<{ - matchingRules: NotificationRule[] + matchingRules: NotificationRuleDraft[] status: RemoteDataState }>({matchingRules: [], status: RemoteDataState.NotStarted}) diff --git a/ui/src/checks/components/ChecksColumn.tsx b/ui/src/checks/components/ChecksColumn.tsx index 4a351899e62..6d9491d1902 100644 --- a/ui/src/checks/components/ChecksColumn.tsx +++ b/ui/src/checks/components/ChecksColumn.tsx @@ -94,10 +94,11 @@ const ChecksColumn: FunctionComponent = ({ const mstp = (state: AppState) => { const { - checks: {list: checks}, labels: {list: labels}, } = state + const checks = getAll(state, ResourceType.Checks) + const endpoints = getAll( state, ResourceType.NotificationEndpoints diff --git a/ui/src/checks/components/EditCheckEO.tsx b/ui/src/checks/components/EditCheckEO.tsx index 38d0299a9b7..5b83121442a 100644 --- a/ui/src/checks/components/EditCheckEO.tsx +++ b/ui/src/checks/components/EditCheckEO.tsx @@ -14,9 +14,9 @@ import {getActiveTimeMachine} from 'src/timeMachine/selectors' // Actions import { - saveCheckFromTimeMachine, + updateCheckFromTimeMachine, getCheckForTimeMachine, -} from 'src/checks/actions' +} from 'src/checks/actions/thunks' import {executeQueries} from 'src/timeMachine/actions/queries' import {resetAlertBuilder, updateName} from 'src/alerting/actions/alertBuilder' @@ -24,7 +24,7 @@ import {resetAlertBuilder, updateName} from 'src/alerting/actions/alertBuilder' import {AppState, RemoteDataState, TimeMachineID, QueryView} from 'src/types' interface DispatchProps { - onSaveCheckFromTimeMachine: typeof saveCheckFromTimeMachine + onSaveCheckFromTimeMachine: typeof updateCheckFromTimeMachine onGetCheckForTimeMachine: typeof getCheckForTimeMachine onExecuteQueries: typeof executeQueries onResetAlertBuilder: typeof resetAlertBuilder @@ -33,7 +33,7 @@ interface DispatchProps { interface StateProps { view: QueryView | null - checkStatus: RemoteDataState + status: RemoteDataState activeTimeMachineID: TimeMachineID loadedCheckID: string checkName: string @@ -48,7 +48,7 @@ const EditCheckEditorOverlay: FunctionComponent = ({ onExecuteQueries, onGetCheckForTimeMachine, activeTimeMachineID, - checkStatus, + status, router, params: {checkID, orgID}, checkName, @@ -70,11 +70,11 @@ const EditCheckEditorOverlay: FunctionComponent = ({ let loadingStatus = RemoteDataState.Loading - if (checkStatus === RemoteDataState.Error) { + if (status === RemoteDataState.Error) { loadingStatus = RemoteDataState.Error } if ( - checkStatus === RemoteDataState.Done && + status === RemoteDataState.Done && activeTimeMachineID === 'alerting' && loadedCheckID === checkID ) { @@ -106,7 +106,7 @@ const EditCheckEditorOverlay: FunctionComponent = ({ const mstp = (state: AppState): StateProps => { const { timeMachines: {activeTimeMachineID}, - alertBuilder: {checkStatus, name, id}, + alertBuilder: {status, name, id}, } = state const {view} = getActiveTimeMachine(state) @@ -114,7 +114,7 @@ const mstp = (state: AppState): StateProps => { return { loadedCheckID: id, checkName: name, - checkStatus, + status, activeTimeMachineID, view, } @@ -122,7 +122,7 @@ const mstp = (state: AppState): StateProps => { const mdtp: DispatchProps = { onGetCheckForTimeMachine: getCheckForTimeMachine, - onSaveCheckFromTimeMachine: saveCheckFromTimeMachine, + onSaveCheckFromTimeMachine: updateCheckFromTimeMachine, onExecuteQueries: executeQueries, onResetAlertBuilder: resetAlertBuilder, onUpdateAlertBuilderName: updateName, diff --git a/ui/src/checks/components/NewDeadmanCheckEO.tsx b/ui/src/checks/components/NewDeadmanCheckEO.tsx index 5361b6cbaeb..6090f92d81f 100644 --- a/ui/src/checks/components/NewDeadmanCheckEO.tsx +++ b/ui/src/checks/components/NewDeadmanCheckEO.tsx @@ -9,7 +9,7 @@ import CheckEOHeader from 'src/checks/components/CheckEOHeader' import TimeMachine from 'src/timeMachine/components/TimeMachine' // Actions -import {saveCheckFromTimeMachine} from 'src/checks/actions' +import {createCheckFromTimeMachine} from 'src/checks/actions/thunks' import {setActiveTimeMachine} from 'src/timeMachine/actions' import { resetAlertBuilder, @@ -25,7 +25,7 @@ import {AppState, RemoteDataState, CheckViewProperties} from 'src/types' interface DispatchProps { onSetActiveTimeMachine: typeof setActiveTimeMachine - onSaveCheckFromTimeMachine: typeof saveCheckFromTimeMachine + onSaveCheckFromTimeMachine: typeof createCheckFromTimeMachine onResetAlertBuilder: typeof resetAlertBuilder onUpdateAlertBuilderName: typeof updateName onInitializeAlertBuilder: typeof initializeAlertBuilder @@ -33,14 +33,14 @@ interface DispatchProps { interface StateProps { checkName: string - checkStatus: RemoteDataState + status: RemoteDataState } type Props = DispatchProps & StateProps & WithRouterProps const NewCheckOverlay: FunctionComponent = ({ params: {orgID}, - checkStatus, + status, checkName, router, onSaveCheckFromTimeMachine, @@ -67,7 +67,7 @@ const NewCheckOverlay: FunctionComponent = ({
} - loading={checkStatus || RemoteDataState.Loading} + loading={status || RemoteDataState.Loading} > = ({ ) } -const mstp = ({alertBuilder: {name, checkStatus}}: AppState): StateProps => { - return {checkName: name, checkStatus} +const mstp = ({alertBuilder: {name, status}}: AppState): StateProps => { + return {checkName: name, status} } const mdtp: DispatchProps = { onSetActiveTimeMachine: setActiveTimeMachine, - onSaveCheckFromTimeMachine: saveCheckFromTimeMachine, + onSaveCheckFromTimeMachine: createCheckFromTimeMachine, onResetAlertBuilder: resetAlertBuilder, onUpdateAlertBuilderName: updateName, onInitializeAlertBuilder: initializeAlertBuilder, diff --git a/ui/src/checks/components/NewThresholdCheckEO.tsx b/ui/src/checks/components/NewThresholdCheckEO.tsx index 0807ed136d6..939dbd1f632 100644 --- a/ui/src/checks/components/NewThresholdCheckEO.tsx +++ b/ui/src/checks/components/NewThresholdCheckEO.tsx @@ -9,7 +9,7 @@ import CheckEOHeader from 'src/checks/components/CheckEOHeader' import TimeMachine from 'src/timeMachine/components/TimeMachine' // Actions -import {saveCheckFromTimeMachine} from 'src/checks/actions' +import {createCheckFromTimeMachine} from 'src/checks/actions/thunks' import {setActiveTimeMachine} from 'src/timeMachine/actions' import { resetAlertBuilder, @@ -25,7 +25,7 @@ import {AppState, RemoteDataState, CheckViewProperties} from 'src/types' interface DispatchProps { onSetActiveTimeMachine: typeof setActiveTimeMachine - onSaveCheckFromTimeMachine: typeof saveCheckFromTimeMachine + onSaveCheckFromTimeMachine: typeof createCheckFromTimeMachine onResetAlertBuilder: typeof resetAlertBuilder onUpdateAlertBuilderName: typeof updateName onInitializeAlertBuilder: typeof initializeAlertBuilder @@ -33,13 +33,13 @@ interface DispatchProps { interface StateProps { checkName: string - checkStatus: RemoteDataState + status: RemoteDataState } type Props = DispatchProps & StateProps & WithRouterProps const NewCheckOverlay: FunctionComponent = ({ - checkStatus, + status, params: {orgID}, checkName, router, @@ -67,7 +67,7 @@ const NewCheckOverlay: FunctionComponent = ({
} - loading={checkStatus || RemoteDataState.Loading} + loading={status || RemoteDataState.Loading} > = ({ ) } -const mstp = ({alertBuilder: {name, checkStatus}}: AppState): StateProps => { - return {checkName: name, checkStatus} +const mstp = ({alertBuilder: {name, status}}: AppState): StateProps => { + return {checkName: name, status} } const mdtp: DispatchProps = { onSetActiveTimeMachine: setActiveTimeMachine, - onSaveCheckFromTimeMachine: saveCheckFromTimeMachine, + onSaveCheckFromTimeMachine: createCheckFromTimeMachine, onResetAlertBuilder: resetAlertBuilder, onUpdateAlertBuilderName: updateName, onInitializeAlertBuilder: initializeAlertBuilder, diff --git a/ui/src/checks/reducers/checks.test.ts b/ui/src/checks/reducers/checks.test.ts index 690acbcbdb1..9fe110db00f 100644 --- a/ui/src/checks/reducers/checks.test.ts +++ b/ui/src/checks/reducers/checks.test.ts @@ -1,10 +1,19 @@ +// Libraries +import {normalize} from 'normalizr' + +// Schemas +import {arrayOfChecks, checkSchema} from 'src/schemas' + import checksReducer, {defaultChecksState} from 'src/checks/reducers' -import {setAllChecks, setCheck, removeCheck} from 'src/checks/actions' + +import {setChecks, setCheck, removeCheck} from 'src/checks/actions/creators' import { RemoteDataState, DashboardQuery, - ThresholdCheck, - DeadmanCheck, + CheckEntities, + Check, + GenThresholdCheck, + GenDeadmanCheck, } from 'src/types' const CHECK_QUERY_FIXTURE: DashboardQuery = { @@ -14,7 +23,7 @@ const CHECK_QUERY_FIXTURE: DashboardQuery = { name: 'great q', } -export const CHECK_FIXTURE_1: ThresholdCheck = { +export const CHECK_FIXTURE_1: GenThresholdCheck = { id: '1', type: 'threshold', name: 'Amoozing check', @@ -34,9 +43,10 @@ export const CHECK_FIXTURE_1: ThresholdCheck = { value: 10, }, ], + labels: [], } -export const CHECK_FIXTURE_2: ThresholdCheck = { +export const CHECK_FIXTURE_2: GenThresholdCheck = { id: '2', type: 'threshold', name: 'Another check', @@ -56,9 +66,10 @@ export const CHECK_FIXTURE_2: ThresholdCheck = { value: 10, }, ], + labels: [], } -export const CHECK_FIXTURE_3: DeadmanCheck = { +export const CHECK_FIXTURE_3: GenDeadmanCheck = { id: '2', type: 'deadman', name: 'Another check', @@ -66,7 +77,6 @@ export const CHECK_FIXTURE_3: DeadmanCheck = { createdAt: '2019-12-17T00:00', updatedAt: '2019-05-17T00:00', query: CHECK_QUERY_FIXTURE, - status: 'active', every: '2d', offset: '1m', tags: [{key: 'a', value: 'b'}], @@ -75,71 +85,89 @@ export const CHECK_FIXTURE_3: DeadmanCheck = { staleTime: '10m', reportZero: false, level: 'INFO', + labels: [], + status: 'active', } describe('checksReducer', () => { - describe('setAllChecks', () => { + describe('setChecks', () => { it('sets list and status properties of state.', () => { const initialState = defaultChecksState + const cid_1 = CHECK_FIXTURE_1.id + const cid_2 = CHECK_FIXTURE_2.id + + const checks = [CHECK_FIXTURE_1, CHECK_FIXTURE_2] + + const normChecks = normalize( + checks, + arrayOfChecks + ) const actual = checksReducer( initialState, - setAllChecks(RemoteDataState.Done, [CHECK_FIXTURE_1, CHECK_FIXTURE_2]) + setChecks(RemoteDataState.Done, normChecks) ) - const expected = { - ...defaultChecksState, - list: [CHECK_FIXTURE_1, CHECK_FIXTURE_2], + expect(actual.byID[cid_1]).toEqual({ + ...CHECK_FIXTURE_1, + activeStatus: 'active', status: RemoteDataState.Done, - } - - expect(actual).toEqual(expected) + }) + expect(actual.byID[cid_2]).toEqual({ + ...CHECK_FIXTURE_2, + activeStatus: 'active', + status: RemoteDataState.Done, + }) + expect(actual.allIDs).toEqual([cid_1, cid_2]) + expect(actual.status).toBe(RemoteDataState.Done) }) }) + describe('setCheck', () => { it('adds check to list if it is new', () => { const initialState = defaultChecksState + const id = CHECK_FIXTURE_2.id - const actual = checksReducer(initialState, setCheck(CHECK_FIXTURE_2)) - - const expected = { - ...defaultChecksState, - list: [CHECK_FIXTURE_2], - } + const check = normalize( + CHECK_FIXTURE_2, + checkSchema + ) - expect(actual).toEqual(expected) - }) - it('updates check in list if it exists', () => { - const initialState = defaultChecksState - initialState.list = [CHECK_FIXTURE_1] const actual = checksReducer( initialState, - setCheck({...CHECK_FIXTURE_1, name: CHECK_FIXTURE_2.name}) + setCheck(id, RemoteDataState.Done, check) ) - const expected = { - ...defaultChecksState, - list: [{...CHECK_FIXTURE_1, name: CHECK_FIXTURE_2.name}], - } + expect(actual.byID[id]).toEqual({ + ...CHECK_FIXTURE_2, + status: RemoteDataState.Done, + activeStatus: 'active', + }) - expect(actual).toEqual(expected) + expect(actual.allIDs).toEqual([id]) }) }) + describe('removeCheck', () => { - it('removes check from list', () => { + it('removes check from state', () => { const initialState = defaultChecksState - initialState.list = [CHECK_FIXTURE_1] + const id = CHECK_FIXTURE_1.id + + initialState.byID[id] = { + ...CHECK_FIXTURE_1, + status: RemoteDataState.Done, + activeStatus: 'active', + } + + initialState.allIDs = [id] + const actual = checksReducer( initialState, removeCheck(CHECK_FIXTURE_1.id) ) - const expected = { - ...defaultChecksState, - list: [], - } - - expect(actual).toEqual(expected) + expect(actual.byID).toEqual({}) + expect(actual.allIDs).toEqual([]) }) }) }) diff --git a/ui/src/checks/reducers/index.ts b/ui/src/checks/reducers/index.ts index 26528e3ee1a..2de9e0fbb2a 100644 --- a/ui/src/checks/reducers/index.ts +++ b/ui/src/checks/reducers/index.ts @@ -2,17 +2,27 @@ import {produce} from 'immer' // Types -import {RemoteDataState, Check} from 'src/types' -import {Action} from 'src/checks/actions' +import {Check, RemoteDataState, ResourceState, ResourceType} from 'src/types' +import { + Action, + SET_CHECKS, + SET_CHECK, + REMOVE_CHECK, + ADD_LABEL_TO_CHECK, + REMOVE_LABEL_FROM_CHECK, +} from 'src/checks/actions/creators' +import { + setResource, + setResourceAtID, + removeResource, +} from 'src/resources/reducers/helpers' -export interface ChecksState { - status: RemoteDataState - list: Check[] -} +export type ChecksState = ResourceState['checks'] export const defaultChecksState: ChecksState = { status: RemoteDataState.NotStarted, - list: [], + byID: {}, + allIDs: [], } export interface ResourceIDs { @@ -27,48 +37,39 @@ export default ( ): ChecksState => produce(state, draftState => { switch (action.type) { - case 'SET_ALL_CHECKS': - const {status, checks} = action.payload - draftState.status = status - if (checks) { - draftState.list = checks - } + case SET_CHECKS: { + setResource(draftState, action, ResourceType.Checks) + return + } - case 'SET_CHECK': - const newCheck = action.payload.check - const checkIndex = state.list.findIndex(c => c.id == newCheck.id) + case SET_CHECK: { + setResourceAtID(draftState, action, ResourceType.Checks) - if (checkIndex == -1) { - draftState.list.push(newCheck) - } else { - draftState.list[checkIndex] = newCheck - } return + } + + case REMOVE_CHECK: { + removeResource(draftState, action) - case 'REMOVE_CHECK': - const {checkID} = action.payload - draftState.list = draftState.list.filter(c => c.id != checkID) return + } + + case ADD_LABEL_TO_CHECK: { + const {checkID, label} = action + + const labels = draftState.byID[checkID].labels + draftState.byID[checkID].labels = [...labels, label] - case 'ADD_LABEL_TO_CHECK': - draftState.list = draftState.list.map(c => { - if (c.id === action.payload.checkID) { - c.labels = [...c.labels, action.payload.label] - } - return c - }) return + } + + case REMOVE_LABEL_FROM_CHECK: { + const {checkID, labelID} = action + const labels = draftState.byID[checkID].labels + draftState.byID[checkID].labels = labels.filter(l => l.id !== labelID) - case 'REMOVE_LABEL_FROM_CHECK': - draftState.list = draftState.list.map(c => { - if (c.id === action.payload.checkID) { - c.labels = c.labels.filter( - label => label.id !== action.payload.label.id - ) - } - return c - }) return + } } }) diff --git a/ui/src/checks/selectors/index.ts b/ui/src/checks/selectors/index.ts index 551bf8b4232..f396e6b95ad 100644 --- a/ui/src/checks/selectors/index.ts +++ b/ui/src/checks/selectors/index.ts @@ -1,13 +1,12 @@ import {AppState, Check} from 'src/types' export const getCheck = (state: AppState, id: string): Check => { - const checksList = state.checks.list - return checksList.find(c => c.id === id) + return state.resources.checks.byID[id] || null } export const getCheckIDs = (state: AppState): {[x: string]: boolean} => { - return state.checks.list.reduce( - (acc, check) => ({...acc, [check.id]: true}), + return state.resources.checks.allIDs.reduce( + (acc, id) => ({...acc, [id]: true}), {} ) } diff --git a/ui/src/checks/utils/index.ts b/ui/src/checks/utils/index.ts new file mode 100644 index 00000000000..e401f2e5d02 --- /dev/null +++ b/ui/src/checks/utils/index.ts @@ -0,0 +1,125 @@ +// Types +import {AppState, Check, ThresholdCheck, DeadmanCheck} from 'src/types' +import {PostCheck} from 'src/client' + +// Utils +import {checkThresholdsValid} from './checkValidate' +import {isDurationParseable} from 'src/shared/utils/duration' +import {getActiveTimeMachine} from 'src/timeMachine/selectors' +import {getOrg} from 'src/organizations/selectors' + +type AlertBuilder = AppState['alertBuilder'] + +export const toPostCheck = (check: Check): PostCheck => { + // TODO: type PostCheck properly github.com/influxdata/influxdb/issues/16704 + const status = check.activeStatus + + delete check.activeStatus + + return { + ...check, + status, + labels: (check.labels || []).map(l => l.id), + } as PostCheck +} + +export const builderToPostCheck = (state: AppState) => { + const {alertBuilder} = state + const check = genCheckBase(state) + + validateBuilder(alertBuilder) + + if (check.type === 'threshold') { + return toThresholdPostCheck(alertBuilder, check) + } + + if (check.type === 'deadman') { + return toDeadManPostCheck(alertBuilder, check) + } +} + +const toDeadManPostCheck = ( + alertBuilder: AlertBuilder, + check: DeadmanCheck +): PostCheck => { + const { + every, + level, + offset, + reportZero, + staleTime, + statusMessageTemplate, + tags, + timeSince, + activeStatus, + } = alertBuilder + + if (!isDurationParseable(timeSince) || !isDurationParseable(staleTime)) { + throw new Error('Duration fields must contain valid duration') + } + + return { + ...check, + every, + level, + offset, + reportZero, + staleTime, + statusMessageTemplate, + tags, + timeSince, + status: activeStatus, + } +} + +const toThresholdPostCheck = ( + alertBuilder: AlertBuilder, + check: ThresholdCheck +): PostCheck => { + const { + activeStatus, + every, + offset, + statusMessageTemplate, + tags, + thresholds, + } = alertBuilder + + checkThresholdsValid(thresholds) + + return { + ...check, + every, + offset, + statusMessageTemplate, + tags, + thresholds, + status: activeStatus, + } +} + +const validateBuilder = (alertBuilder: AlertBuilder) => { + if (!isDurationParseable(alertBuilder.offset)) { + throw new Error('Check offset must be a valid duration') + } + + if (!isDurationParseable(alertBuilder.every)) { + throw new Error('Check every must be a valid duration') + } +} + +const genCheckBase = (state: AppState) => { + const {type, id, status, activeStatus, name} = state.alertBuilder + const {draftQueries} = getActiveTimeMachine(state) + const {id: orgID} = getOrg(state) + + return { + id, + type, + status, + activeStatus, + name, + query: draftQueries[0], + orgID, + } as Check +} diff --git a/ui/src/cloud/actions/limits.ts b/ui/src/cloud/actions/limits.ts index 53619dc59e5..1d062461771 100644 --- a/ui/src/cloud/actions/limits.ts +++ b/ui/src/cloud/actions/limits.ts @@ -242,7 +242,9 @@ export const getReadWriteCardinalityLimits = () => async ( } else { dispatch(setCardinalityLimitStatus(LimitStatus.OK)) } - } catch (e) {} + } catch (error) { + console.error(error) + } } export const getAssetLimits = () => async (dispatch, getState: GetState) => { @@ -253,7 +255,8 @@ export const getAssetLimits = () => async (dispatch, getState: GetState) => { const limits = await getLimitsAJAX(org.id) dispatch(setLimits(limits)) dispatch(setLimitsStatus(RemoteDataState.Done)) - } catch (e) { + } catch (error) { + console.error(error) dispatch(setLimitsStatus(RemoteDataState.Error)) } } @@ -297,8 +300,8 @@ export const checkBucketLimits = () => (dispatch, getState: GetState) => { } else { dispatch(setBucketLimitStatus(LimitStatus.OK)) } - } catch (e) { - console.error(e) + } catch (error) { + console.error(error) } } @@ -316,27 +319,27 @@ export const checkTaskLimits = () => (dispatch, getState: GetState) => { } else { dispatch(setTaskLimitStatus(LimitStatus.OK)) } - } catch (e) { - console.error(e) + } catch (error) { + console.error(error) } } export const checkChecksLimits = () => (dispatch, getState: GetState) => { try { const { - checks: {list: checksList}, + resources, cloud: {limits}, } = getState() const checksMax = extractChecksMax(limits) - const checksCount = checksList.length + const checksCount = resources.checks.allIDs.length if (checksCount >= checksMax) { dispatch(setChecksLimitStatus(LimitStatus.EXCEEDED)) } else { dispatch(setChecksLimitStatus(LimitStatus.OK)) } - } catch (e) { - console.error(e) + } catch (error) { + console.error(error) } } @@ -355,8 +358,8 @@ export const checkRulesLimits = () => (dispatch, getState: GetState) => { } else { dispatch(setRulesLimitStatus(LimitStatus.OK)) } - } catch (e) { - console.error(e) + } catch (error) { + console.error(error) } } diff --git a/ui/src/dashboards/selectors/index.ts b/ui/src/dashboards/selectors/index.ts index 6dc76da44c3..c678616adba 100644 --- a/ui/src/dashboards/selectors/index.ts +++ b/ui/src/dashboards/selectors/index.ts @@ -23,9 +23,7 @@ export const getCheckForView = ( const viewType: ViewType = get(view, 'properties.type') const checkID = get(view, 'properties.checkID') - return viewType === 'check' - ? state.checks.list.find(c => c.id === checkID) - : null + return viewType === 'check' ? state.resources.checks.byID[checkID] : null } interface DropdownValues { diff --git a/ui/src/notifications/endpoints/actions/thunks.ts b/ui/src/notifications/endpoints/actions/thunks.ts index 30ac5161a0a..441fdc6933a 100644 --- a/ui/src/notifications/endpoints/actions/thunks.ts +++ b/ui/src/notifications/endpoints/actions/thunks.ts @@ -27,6 +27,7 @@ import * as api from 'src/client' import {incrementCloneName} from 'src/utils/naming' import {getOrg} from 'src/organizations/selectors' import {getAll} from 'src/resources/selectors' +import {toPostNotificationEndpoint} from 'src/notifications/endpoints/utils' import * as copy from 'src/shared/copy/notifications' // Types @@ -35,7 +36,6 @@ import { GetState, Label, NotificationEndpointUpdate, - PostNotificationEndpoint, RemoteDataState, EndpointEntities, ResourceType, @@ -80,12 +80,7 @@ export const createEndpoint = (endpoint: NotificationEndpoint) => async ( Action | NotificationAction | ReturnType > ) => { - const labels = endpoint.labels || [] - - const data = { - ...endpoint, - labels: labels.map(l => l.id), - } as PostNotificationEndpoint + const data = toPostNotificationEndpoint(endpoint) try { const resp = await api.postNotificationEndpoint({data}) @@ -111,11 +106,12 @@ export const updateEndpoint = (endpoint: NotificationEndpoint) => async ( dispatch: Dispatch ) => { dispatch(setEndpoint(endpoint.id, RemoteDataState.Loading)) + const data = toPostNotificationEndpoint(endpoint) try { const resp = await api.putNotificationEndpoint({ endpointID: endpoint.id, - data: endpoint, + data, }) if (resp.status !== 200) { @@ -138,6 +134,7 @@ export const updateEndpointProperties = ( endpointID: string, properties: NotificationEndpointUpdate ) => async (dispatch: Dispatch) => { + dispatch(setEndpoint(endpointID, RemoteDataState.Loading)) try { const resp = await api.patchNotificationEndpoint({ endpointID, @@ -235,14 +232,11 @@ export const cloneEndpoint = (endpoint: NotificationEndpoint) => async ( const clonedName = incrementCloneName(allEndpointNames, endpoint.name) - const labels = endpoint.labels || [] - const resp = await api.postNotificationEndpoint({ data: { - ...endpoint, + ...toPostNotificationEndpoint(endpoint), name: clonedName, - labels: labels.map(l => l.id), - } as PostNotificationEndpoint, + }, }) if (resp.status !== 201) { diff --git a/ui/src/notifications/endpoints/components/EndpointCard.tsx b/ui/src/notifications/endpoints/components/EndpointCard.tsx index 4904481e130..7bd3bf3b496 100644 --- a/ui/src/notifications/endpoints/components/EndpointCard.tsx +++ b/ui/src/notifications/endpoints/components/EndpointCard.tsx @@ -14,13 +14,7 @@ import { } from 'src/notifications/endpoints/actions/thunks' // Components -import { - SlideToggle, - ComponentSize, - ResourceCard, - SpinnerContainer, - TechnoSpinner, -} from '@influxdata/clockface' +import {SlideToggle, ComponentSize, ResourceCard} from '@influxdata/clockface' import EndpointCardMenu from 'src/notifications/endpoints/components/EndpointCardMenu' import InlineLabels from 'src/shared/components/inlineLabels/InlineLabels' @@ -79,13 +73,14 @@ const EndpointCard: FC = ({ onAddEndpointLabel, onRemoveEndpointLabel, }) => { - const {id, name, status, description} = endpoint + const {id, name, description, activeStatus} = endpoint const handleUpdateName = (name: string) => { onUpdateEndpointProperties(id, {name}) } + const handleClick = () => { - router.push(`orgs/${orgID}/alerting/endpoints/${endpoint.id}/edit`) + router.push(`orgs/${orgID}/alerting/endpoints/${id}/edit`) } const nameComponent = ( @@ -102,13 +97,13 @@ const EndpointCard: FC = ({ ) const handleToggle = () => { - const toStatus = status === 'active' ? 'inactive' : 'active' + const toStatus = activeStatus === 'active' ? 'inactive' : 'active' onUpdateEndpointProperties(id, {status: toStatus}) } const toggle = ( = ({ const queryParams = new URLSearchParams({ [HISTORY_TYPE_QUERY_PARAM]: historyType, - [SEARCH_QUERY_PARAM]: `"notificationEndpointID" == "${endpoint.id}"`, + [SEARCH_QUERY_PARAM]: `"notificationEndpointID" == "${id}"`, }) router.push(`/orgs/${orgID}/alert-history?${queryParams}`) @@ -167,26 +162,19 @@ const EndpointCard: FC = ({ ) return ( - } - loading={endpoint.loadingStatus} - > - - {relativeTimestampFormatter(endpoint.updatedAt, 'Last updated ')} - , - ]} - testID={`endpoint-card ${name}`} - /> - + {relativeTimestampFormatter(endpoint.updatedAt, 'Last updated ')}, + ]} + testID={`endpoint-card ${name}`} + /> ) } diff --git a/ui/src/notifications/endpoints/components/EndpointOverlayContents.tsx b/ui/src/notifications/endpoints/components/EndpointOverlayContents.tsx index e831e7162c1..d820ed23fc9 100644 --- a/ui/src/notifications/endpoints/components/EndpointOverlayContents.tsx +++ b/ui/src/notifications/endpoints/components/EndpointOverlayContents.tsx @@ -57,7 +57,7 @@ const EndpointOverlayContents: FC = ({ const handleSelectType = (type: NotificationEndpointType) => { dispatch({ type: 'UPDATE_ENDPOINT_TYPE', - endpoint: {...endpoint, type}, + endpoint: {...endpoint, type} as NotificationEndpoint, }) } diff --git a/ui/src/notifications/endpoints/reducers/index.ts b/ui/src/notifications/endpoints/reducers/index.ts index 9efec3d7e19..de842bca21b 100644 --- a/ui/src/notifications/endpoints/reducers/index.ts +++ b/ui/src/notifications/endpoints/reducers/index.ts @@ -1,6 +1,5 @@ // Libraries import produce from 'immer' -import {get} from 'lodash' // Types import { @@ -19,7 +18,11 @@ import { } from 'src/notifications/endpoints/actions/creators' // Helpers -import {setResource, removeResource} from 'src/resources/reducers/helpers' +import { + setResource, + removeResource, + setResourceAtID, +} from 'src/resources/reducers/helpers' type EndpointsState = ResourceState['endpoints'] @@ -46,28 +49,11 @@ export default ( } case SET_ENDPOINT: { - const {schema, status, id} = action - - const endpoint: NotificationEndpoint = get(schema, [ - 'entities', - ResourceType.NotificationEndpoints, - id, - ]) - - if (!endpoint) { - draftState.byID[id] = ({ - id, - loadingStatus: status, - } as unknown) as NotificationEndpoint - - return - } - - if (!draftState.allIDs.includes(id)) { - draftState.allIDs.push(id) - } - - draftState.byID[id] = {...endpoint, loadingStatus: status} + setResourceAtID( + draftState, + action, + ResourceType.NotificationEndpoints + ) return } diff --git a/ui/src/notifications/endpoints/utils/index.ts b/ui/src/notifications/endpoints/utils/index.ts new file mode 100644 index 00000000000..584fd9f61c7 --- /dev/null +++ b/ui/src/notifications/endpoints/utils/index.ts @@ -0,0 +1,13 @@ +import {NotificationEndpoint, PostNotificationEndpoint} from 'src/types' + +export const toPostNotificationEndpoint = ( + endpoint: NotificationEndpoint +): PostNotificationEndpoint => { + const labels = endpoint.labels || [] + + return { + ...endpoint, + status: endpoint.activeStatus, + labels: labels.map(l => l.id), + } +} diff --git a/ui/src/notifications/rules/actions/thunks.ts b/ui/src/notifications/rules/actions/thunks.ts index acbd1d95526..fa19eefba90 100644 --- a/ui/src/notifications/rules/actions/thunks.ts +++ b/ui/src/notifications/rules/actions/thunks.ts @@ -152,7 +152,7 @@ export const updateRule = (rule: NotificationRuleDraft) => async ( schemas.rule ) - dispatch(setRule(resp.data.id, RemoteDataState.Done, normRule)) + dispatch(setRule(rule.id, RemoteDataState.Done, normRule)) } catch (error) { console.error(error) } @@ -162,8 +162,6 @@ export const updateRuleProperties = ( ruleID: string, properties: NotificationRuleUpdate ) => async (dispatch: Dispatch) => { - dispatch(setRule(ruleID, RemoteDataState.Loading)) - try { const resp = await api.patchNotificationRule({ ruleID, @@ -179,7 +177,7 @@ export const updateRuleProperties = ( schemas.rule ) - dispatch(setRule(ruleID, RemoteDataState.Loading, rule)) + dispatch(setRule(ruleID, RemoteDataState.Done, rule)) } catch (error) { console.error(error) } diff --git a/ui/src/notifications/rules/components/RuleCard.tsx b/ui/src/notifications/rules/components/RuleCard.tsx index 2ede235c4f9..f788b2e6160 100644 --- a/ui/src/notifications/rules/components/RuleCard.tsx +++ b/ui/src/notifications/rules/components/RuleCard.tsx @@ -66,16 +66,26 @@ const RuleCard: FC = ({ params: {orgID}, router, }) => { + const { + id, + activeStatus, + name, + lastRunError, + lastRunStatus, + description, + latestCompleted, + } = rule + const onUpdateName = (name: string) => { - onUpdateRuleProperties(rule.id, {name}) + onUpdateRuleProperties(id, {name}) } const onUpdateDescription = (description: string) => { - onUpdateRuleProperties(rule.id, {description}) + onUpdateRuleProperties(id, {description}) } const onDelete = () => { - deleteNotificationRule(rule.id) + deleteNotificationRule(id) } const onClone = () => { @@ -83,13 +93,13 @@ const RuleCard: FC = ({ } const onToggle = () => { - const status = rule.status === 'active' ? 'inactive' : 'active' + const status = activeStatus === 'active' ? 'inactive' : 'active' - onUpdateRuleProperties(rule.id, {status}) + onUpdateRuleProperties(id, {status}) } const onRuleClick = () => { - router.push(`/orgs/${orgID}/alerting/rules/${rule.id}/edit`) + router.push(`/orgs/${orgID}/alerting/rules/${id}/edit`) } const onView = () => { @@ -97,29 +107,29 @@ const RuleCard: FC = ({ const queryParams = new URLSearchParams({ [HISTORY_TYPE_QUERY_PARAM]: historyType, - [SEARCH_QUERY_PARAM]: `"notificationRuleID" == "${rule.id}"`, + [SEARCH_QUERY_PARAM]: `"notificationRuleID" == "${id}"`, }) router.push(`/orgs/${orgID}/alert-history?${queryParams}`) } const handleAddRuleLabel = (label: Label) => { - onAddRuleLabel(rule.id, label) + onAddRuleLabel(id, label) } const handleRemoveRuleLabel = (label: Label) => { - onRemoveRuleLabel(rule.id, label.id) + onRemoveRuleLabel(id, label.id) } return ( = ({ } toggle={ = ({ description={ } labels={ @@ -149,7 +159,7 @@ const RuleCard: FC = ({ onRemoveLabel={handleRemoveRuleLabel} /> } - disabled={rule.status === 'inactive'} + disabled={activeStatus === 'inactive'} contextMenu={ = ({ /> } metaData={[ - <>Last completed at {rule.latestCompleted}, + <>Last completed at {latestCompleted}, <>{relativeTimestampFormatter(rule.updatedAt, 'Last updated ')}, , ]} /> diff --git a/ui/src/notifications/rules/reducers/index.ts b/ui/src/notifications/rules/reducers/index.ts index 2457d7e6b12..ac6d6adf5a0 100644 --- a/ui/src/notifications/rules/reducers/index.ts +++ b/ui/src/notifications/rules/reducers/index.ts @@ -1,6 +1,5 @@ // Libraries import {produce} from 'immer' -import {get} from 'lodash' // Types import { @@ -18,7 +17,11 @@ import { ADD_LABEL_TO_RULE, REMOVE_LABEL_FROM_RULE, } from 'src/notifications/rules/actions/creators' -import {setResource, removeResource} from 'src/resources/reducers/helpers' +import { + setResource, + removeResource, + setResourceAtID, +} from 'src/resources/reducers/helpers' export const defaultNotificationRulesState: RulesState = { status: RemoteDataState.NotStarted, @@ -44,29 +47,11 @@ export default ( } case SET_RULE: { - const {schema, status, id} = action - - const rule: NotificationRule = get(schema, [ - 'entities', - ResourceType.NotificationRules, - id, - ]) - - if (!rule) { - draftState.byID[id] = ({ - id, - loadingStatus: status, - } as unknown) as NotificationRule - - return - } - - if (!draftState.allIDs.includes(id)) { - draftState.allIDs.push(id) - } - - draftState.byID[id] = {...rule} - draftState.byID[id].loadingStatus = status + setResourceAtID( + draftState, + action, + ResourceType.NotificationRules + ) return } diff --git a/ui/src/notifications/rules/reducers/rules.test.ts b/ui/src/notifications/rules/reducers/rules.test.ts index 246c07bc976..4dde8d3fe61 100644 --- a/ui/src/notifications/rules/reducers/rules.test.ts +++ b/ui/src/notifications/rules/reducers/rules.test.ts @@ -18,17 +18,24 @@ import { import {initRuleDraft} from 'src/notifications/rules/utils' -import {RemoteDataState, RuleEntities, NotificationRule} from 'src/types' +import { + GenRule, + RemoteDataState, + RuleEntities, + NotificationRule, +} from 'src/types' const ruleID = '1' -const NEW_RULE_DRAFT = { +const NEW_RULE_DRAFT: GenRule = { ...initRuleDraft(''), id: ruleID, + status: 'active', statusRules: [], + tagRules: [], } describe('rulesReducer', () => { - describe('setAllNotificationRules', () => { + describe('setRules', () => { it('sets list and status properties of state.', () => { const initialState = defaultNotificationRulesState @@ -44,7 +51,7 @@ describe('rulesReducer', () => { const expected = { ...NEW_RULE_DRAFT, - loadingStatus: RemoteDataState.Done, + status: RemoteDataState.Done, } expect(actual.status).toEqual(RemoteDataState.Done) @@ -69,7 +76,7 @@ describe('rulesReducer', () => { const expected = { ...NEW_RULE_DRAFT, - loadingStatus: RemoteDataState.Done, + status: RemoteDataState.Done, } expect(actual.byID[ruleID]).toEqual(expected) @@ -92,7 +99,7 @@ describe('rulesReducer', () => { const expected = { ...rule, - loadingStatus: RemoteDataState.Done, + status: RemoteDataState.Done, } expect(actual.byID[ruleID]).toEqual(expected) @@ -126,7 +133,7 @@ describe('rulesReducer', () => { expect(actual.current.status).toEqual(RemoteDataState.Done) expect(actual.current.rule).toEqual({ ...NEW_RULE_DRAFT, - loadingStatus: RemoteDataState.Done, + status: RemoteDataState.Done, }) }) }) diff --git a/ui/src/notifications/rules/utils/index.ts b/ui/src/notifications/rules/utils/index.ts index e5b41ca134e..dded3e17141 100644 --- a/ui/src/notifications/rules/utils/index.ts +++ b/ui/src/notifications/rules/utils/index.ts @@ -9,11 +9,11 @@ import { SMTPNotificationRuleBase, PagerDutyNotificationRuleBase, NotificationEndpoint, - NotificationRule, NotificationRuleDraft, HTTPNotificationRuleBase, RuleStatusLevel, PostNotificationRule, + GenRule, } from 'src/types' import {RemoteDataState} from '@influxdata/clockface' @@ -86,7 +86,8 @@ export const initRuleDraft = (orgID: string): NotificationRuleDraft => ({ url: '', orgID, name: '', - status: 'active', + activeStatus: 'active', + status: RemoteDataState.NotStarted, endpointID: '', tagRules: [], labels: [], @@ -101,7 +102,6 @@ export const initRuleDraft = (orgID: string): NotificationRuleDraft => ({ }, ], description: '', - loadingStatus: RemoteDataState.NotStarted, }) // Prepare a newly created rule for persistence @@ -110,6 +110,7 @@ export const draftRuleToPostRule = ( ): PostNotificationRule => { return { ...draftRule, + status: draftRule.activeStatus, statusRules: draftRule.statusRules.map(r => r.value), tagRules: draftRule.tagRules .map(r => r.value) @@ -127,13 +128,13 @@ export const newTagRuleDraft = () => ({ }) // Prepare a persisted rule for editing -export const ruleToDraftRule = ( - rule: NotificationRule -): NotificationRuleDraft => { +export const ruleToDraftRule = (rule: GenRule): NotificationRuleDraft => { const statusRules = rule.statusRules || [] const tagRules = rule.tagRules || [] return { ...rule, + status: RemoteDataState.Done, + activeStatus: rule.status, offset: rule.offset || '', statusRules: statusRules.map(value => ({cid: uuid.v4(), value})), tagRules: tagRules.map(value => ({cid: uuid.v4(), value})), diff --git a/ui/src/resources/components/GetResources.tsx b/ui/src/resources/components/GetResources.tsx index 05751f3097c..f92d7c761f0 100644 --- a/ui/src/resources/components/GetResources.tsx +++ b/ui/src/resources/components/GetResources.tsx @@ -5,7 +5,7 @@ import {connect} from 'react-redux' // Actions import {getAuthorizations} from 'src/authorizations/actions/thunks' import {getBuckets} from 'src/buckets/actions/thunks' -import {getChecks} from 'src/checks/actions' +import {getChecks} from 'src/checks/actions/thunks' import {getDashboards} from 'src/dashboards/actions/thunks' import {getEndpoints} from 'src/notifications/endpoints/actions/thunks' import {getLabels} from 'src/labels/actions' diff --git a/ui/src/resources/reducers/helpers.ts b/ui/src/resources/reducers/helpers.ts index 0ee09e4df12..10ebc14f19b 100644 --- a/ui/src/resources/reducers/helpers.ts +++ b/ui/src/resources/reducers/helpers.ts @@ -9,23 +9,16 @@ export const setResourceAtID = ( action, resource: ResourceType ) => { - const {schema} = action + const {schema, status, id} = action - const status: RemoteDataState = action.status - const id: string = action.id - const r: R = get(schema, ['entities', resource, id]) - - if (!r) { - draftState.byID[id] = ({id, status} as unknown) as R - return - } + const prevResource = get(draftState, ['byID', id], {}) + const currentResource = get(schema, ['entities', resource, id], {}) if (!draftState.allIDs.includes(id)) { draftState.allIDs.push(id) } - draftState.byID[id] = {...r, status} - draftState.byID[id].status = status + draftState.byID[id] = {...prevResource, ...currentResource, status} } export const setResource = ( diff --git a/ui/src/resources/selectors/getResourcesStatus.ts b/ui/src/resources/selectors/getResourcesStatus.ts index cc03d55f62f..d490c02edb6 100644 --- a/ui/src/resources/selectors/getResourcesStatus.ts +++ b/ui/src/resources/selectors/getResourcesStatus.ts @@ -10,6 +10,7 @@ export const getResourcesStatus = ( // Normalized resource statuses case ResourceType.Authorizations: case ResourceType.Buckets: + case ResourceType.Checks: case ResourceType.Dashboards: case ResourceType.Members: case ResourceType.NotificationEndpoints: diff --git a/ui/src/schemas/index.ts b/ui/src/schemas/index.ts index 682b8d1d79d..dd83437d9d2 100644 --- a/ui/src/schemas/index.ts +++ b/ui/src/schemas/index.ts @@ -14,7 +14,11 @@ import { Variable, View, NotificationEndpoint, - NotificationRule, + GenCheck, + Check, + GenEndpoint, + GenRule, + NotificationRuleDraft, } from 'src/types' import {CellsWithViewProperties} from 'src/client' @@ -77,6 +81,26 @@ export const cell = new schema.Entity( ) export const arrayOfCells = [cell] +/* Checks */ + +// Defines the schema for the "checks" resource +export const checkSchema = new schema.Entity( + ResourceType.Checks, + {}, + { + processStrategy: (check: GenCheck): Check => { + return { + ...check, + status: RemoteDataState.Done, + activeStatus: check.status, + labels: addLabels(check), + } + }, + } +) + +export const arrayOfChecks = [checkSchema] + /* Dashboards */ // Defines the schema for the "dashboards" resource @@ -127,13 +151,12 @@ export const endpoint = new schema.Entity( export const arrayOfEndpoints = [endpoint] -const addEndpointDefaults = ( - point: NotificationEndpoint -): NotificationEndpoint => { +const addEndpointDefaults = (point: GenEndpoint): NotificationEndpoint => { return { ...point, - loadingStatus: RemoteDataState.Done, - labels: point.labels.map(addLabelDefaults), + status: RemoteDataState.Done, + activeStatus: point.status, + labels: addLabels(point), } } @@ -154,10 +177,9 @@ export const rule = new schema.Entity( ResourceType.NotificationRules, {}, { - processStrategy: (rule: NotificationRule) => ({ + processStrategy: (rule: GenRule): NotificationRuleDraft => ({ ...ruleToDraftRule(rule), labels: addLabels(rule), - loadingStatus: RemoteDataState.Done, }), } ) diff --git a/ui/src/store/configureStore.ts b/ui/src/store/configureStore.ts index 3c37f5e38df..f959d60ca76 100644 --- a/ui/src/store/configureStore.ts +++ b/ui/src/store/configureStore.ts @@ -54,7 +54,6 @@ export const rootReducer = combineReducers({ ...sharedReducers, autoRefresh: autoRefreshReducer, alertBuilder: alertBuilderReducer, - checks: checksReducer, cloud: combineReducers<{limits: LimitsState}>({limits: limitsReducer}), dataLoading: dataLoadingReducer, labels: labelsReducer, @@ -68,6 +67,7 @@ export const rootReducer = combineReducers({ resources: combineReducers({ buckets: bucketsReducer, cells: cellsReducer, + checks: checksReducer, dashboards: dashboardsReducer, endpoints: endpointsReducer, members: membersReducer, diff --git a/ui/src/types/alerting.ts b/ui/src/types/alerting.ts index 985a30a6b1e..850db712447 100644 --- a/ui/src/types/alerting.ts +++ b/ui/src/types/alerting.ts @@ -2,18 +2,26 @@ import { StatusRule, NotificationRuleBase, TagRule, + SlackNotificationEndpoint, + PagerDutyNotificationEndpoint, + HTTPNotificationEndpoint, SlackNotificationRuleBase, SMTPNotificationRuleBase, PagerDutyNotificationRuleBase, HTTPNotificationRuleBase, Label, - ThresholdCheck, - DeadmanCheck, - CustomCheck, + Check as GenCheck, + ThresholdCheck as GenThresholdCheck, + DeadmanCheck as GenDeadmanCheck, + CustomCheck as GenCustomCheck, NotificationRule as GenRule, NotificationEndpoint as GenEndpoint, - NotificationEndpointBase as GenBaseEndpoint, + TaskStatusType, + Threshold, + CheckBase as GenCheckBase, + NotificationEndpointBase as GenEndpointBase, } from 'src/client' +import {RemoteDataState} from 'src/types' type Omit = Pick> type Overwrite = Omit & U @@ -23,17 +31,25 @@ interface WithClientID { value: T } -export type NotificationEndpoint = GenEndpoint & { - loadingStatus: RemoteDataState +/* Endpoints */ +type EndpointOverrides = { + status: RemoteDataState + activeStatus: TaskStatusType } +// GenEndpoint is the shape of a NotificationEndpoint from the server -- before any UI specific fields are or modified +export type GenEndpoint = GenEndpoint +export type NotificationEndpoint = + | Omit & EndpointOverrides + | Omit & EndpointOverrides + | Omit & EndpointOverrides +export type NotificationEndpointBase = GenEndpointBase & EndpointOverrides -export type NotificationEndpointBase = GenBaseEndpoint & { - loadingStatus: RemoteDataState -} +/* Rule */ +type RuleOverrides = {status: RemoteDataState; activeStatus: TaskStatusType} -export type NotificationRule = GenRule & { - loadingStatus: RemoteDataState -} +// GenRule is the shape of a NotificationRule from the server -- before any UI specific fields are added or modified +export type GenRule = GenRule +export type NotificationRule = GenRule & RuleOverrides export type StatusRuleDraft = WithClientID @@ -43,6 +59,8 @@ export type NotificationRuleBaseDraft = Overwrite< NotificationRuleBase, { id?: string + status: RemoteDataState + activeStatus: TaskStatusType statusRules: StatusRuleDraft[] tagRules: TagRuleDraft[] labels?: Label[] @@ -51,12 +69,23 @@ export type NotificationRuleBaseDraft = Overwrite< type RuleDraft = SlackRule | SMTPRule | PagerDutyRule | HTTPRule -export type NotificationRuleDraft = RuleDraft & {loadingStatus: RemoteDataState} +export type NotificationRuleDraft = RuleDraft -type SlackRule = NotificationRuleBaseDraft & SlackNotificationRuleBase -type SMTPRule = NotificationRuleBaseDraft & SMTPNotificationRuleBase -type PagerDutyRule = NotificationRuleBaseDraft & PagerDutyNotificationRuleBase -type HTTPRule = NotificationRuleBaseDraft & HTTPNotificationRuleBase +type SlackRule = NotificationRuleBaseDraft & + SlackNotificationRuleBase & + RuleOverrides + +type SMTPRule = NotificationRuleBaseDraft & + SMTPNotificationRuleBase & + RuleOverrides + +type PagerDutyRule = NotificationRuleBaseDraft & + PagerDutyNotificationRuleBase & + RuleOverrides + +type HTTPRule = NotificationRuleBaseDraft & + HTTPNotificationRuleBase & + RuleOverrides export type LowercaseCheckStatusLevel = | 'crit' @@ -87,9 +116,36 @@ export interface NotificationRow { sent: 'true' | 'false' // See https://github.com/influxdata/idpe/issues/4645 } +/* Checks */ +type CheckOverrides = {status: RemoteDataState; activeStatus: TaskStatusType} +export type CheckBase = Omit & CheckOverrides + +// GenCheck is the shape of a Check from the server -- before UI specific properties are added +export type GenCheck = GenCheck +export type GenThresholdCheck = GenThresholdCheck +export type GenDeadmanCheck = GenDeadmanCheck + +export type ThresholdCheck = Omit & CheckOverrides + +export type DeadmanCheck = Omit & CheckOverrides + +export type CustomCheck = Omit & CheckOverrides + +export type Check = ThresholdCheck | DeadmanCheck | CustomCheck + +export type CheckType = Check['type'] + +export type ThresholdType = Threshold['type'] + +export type CheckTagSet = ThresholdCheck['tags'][0] + +export type AlertHistoryType = 'statuses' | 'notifications' + +export type HTTPMethodType = HTTPNotificationEndpoint['method'] +export type HTTPAuthMethodType = HTTPNotificationEndpoint['authMethod'] + export { Threshold, - CheckBase, StatusRule, TagRule, PostCheck, @@ -98,9 +154,6 @@ export { GreaterThreshold, LesserThreshold, RangeThreshold, - ThresholdCheck, - DeadmanCheck, - CustomCheck, PostNotificationEndpoint, NotificationRuleBase, NotificationRuleUpdate, @@ -119,20 +172,5 @@ export { NotificationEndpointUpdate, PostNotificationRule, CheckPatch, + TaskStatusType, } from '../client' - -import {Threshold, HTTPNotificationEndpoint} from '../client' -import {RemoteDataState} from '@influxdata/clockface' - -export type Check = ThresholdCheck | DeadmanCheck | CustomCheck - -export type CheckType = Check['type'] - -export type ThresholdType = Threshold['type'] - -export type CheckTagSet = ThresholdCheck['tags'][0] - -export type AlertHistoryType = 'statuses' | 'notifications' - -export type HTTPMethodType = HTTPNotificationEndpoint['method'] -export type HTTPAuthMethodType = HTTPNotificationEndpoint['authMethod'] diff --git a/ui/src/types/resources.ts b/ui/src/types/resources.ts index dca39877d3e..013ec422318 100644 --- a/ui/src/types/resources.ts +++ b/ui/src/types/resources.ts @@ -2,18 +2,19 @@ import { Authorization, Bucket, Cell, + Check, Dashboard, Member, + NotificationEndpoint, + NotificationRule, Organization, RemoteDataState, Scraper, - View, TasksState, Telegraf, TemplatesState, VariablesState, - NotificationEndpoint, - NotificationRule, + View, } from 'src/types' export enum ResourceType { @@ -75,4 +76,5 @@ export interface ResourceState { [ResourceType.Views]: NormalizedState [ResourceType.NotificationEndpoints]: NormalizedState [ResourceType.NotificationRules]: RulesState + [ResourceType.Checks]: NormalizedState } diff --git a/ui/src/types/schemas.ts b/ui/src/types/schemas.ts index 3800ea1fa2b..43a4ae75fdd 100644 --- a/ui/src/types/schemas.ts +++ b/ui/src/types/schemas.ts @@ -3,6 +3,7 @@ import { Authorization, Bucket, Cell, + Check, Dashboard, Member, Organization, @@ -40,6 +41,14 @@ export interface CellEntities { } } +// CheckEntities defines the result of normalizr's normalization of +// the "checks" resource +export interface CheckEntities { + checks: { + [uuid: string]: Check + } +} + // DashboardEntities defines the result of normalizr's normalization // of the "dashboards" resource export interface DashboardEntities { diff --git a/ui/src/types/stores.ts b/ui/src/types/stores.ts index be29af4d80b..48725cbd4a8 100644 --- a/ui/src/types/stores.ts +++ b/ui/src/types/stores.ts @@ -21,7 +21,6 @@ import {UserSettingsState} from 'src/userSettings/reducers' import {OverlayState} from 'src/overlays/reducers/overlays' import {AutoRefreshState} from 'src/shared/reducers/autoRefresh' import {LimitsState} from 'src/cloud/reducers/limits' -import {ChecksState} from 'src/checks/reducers' import {AlertBuilderState} from 'src/alerting/reducers/alertBuilder' import {ResourceState} from 'src/types' @@ -30,7 +29,6 @@ export interface AppState { alertBuilder: AlertBuilderState app: AppPresentationState autoRefresh: AutoRefreshState - checks: ChecksState cloud: {limits: LimitsState} dataLoading: DataLoadingState labels: LabelsState