mirror of https://github.com/grafana/grafana
Alerting: Prevents creating alerts from unsupported queries (#19250)
* Refactor: Makes PanelEditor use state and shows validation message on AlerTab * Refactor: Makes validation message nicer looking * Refactor: Changes imports * Refactor: Removes conditional props * Refactor: Changes after feedback from PR review * Refactor: Removes unused actionpull/19317/head
parent
68d6da77da
commit
9bd6ed887c
@ -0,0 +1,148 @@ |
|||||||
|
import { DataSourceSrv } from '@grafana/runtime'; |
||||||
|
import { DataSourceApi, PluginMeta } from '@grafana/ui'; |
||||||
|
import { DataTransformerConfig } from '@grafana/data'; |
||||||
|
|
||||||
|
import { ElasticsearchQuery } from '../../plugins/datasource/elasticsearch/types'; |
||||||
|
import { getAlertingValidationMessage } from './getAlertingValidationMessage'; |
||||||
|
|
||||||
|
describe('getAlertingValidationMessage', () => { |
||||||
|
describe('when called with some targets containing template variables', () => { |
||||||
|
it('then it should return false', async () => { |
||||||
|
let call = 0; |
||||||
|
const datasource: DataSourceApi = ({ |
||||||
|
meta: ({ alerting: true } as any) as PluginMeta, |
||||||
|
targetContainsTemplate: () => { |
||||||
|
if (call === 0) { |
||||||
|
call++; |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
}, |
||||||
|
name: 'some name', |
||||||
|
} as any) as DataSourceApi; |
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource); |
||||||
|
const datasourceSrv: DataSourceSrv = { |
||||||
|
get: getMock, |
||||||
|
}; |
||||||
|
const targets: ElasticsearchQuery[] = [ |
||||||
|
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false }, |
||||||
|
{ refId: 'B', query: '@instance:instance', isLogsQuery: false }, |
||||||
|
]; |
||||||
|
const transformations: DataTransformerConfig[] = []; |
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); |
||||||
|
|
||||||
|
expect(result).toBe(''); |
||||||
|
expect(getMock).toHaveBeenCalledTimes(2); |
||||||
|
expect(getMock).toHaveBeenCalledWith(datasource.name); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called with some targets using a datasource that does not support alerting', () => { |
||||||
|
it('then it should return false', async () => { |
||||||
|
const alertingDatasource: DataSourceApi = ({ |
||||||
|
meta: ({ alerting: true } as any) as PluginMeta, |
||||||
|
targetContainsTemplate: () => false, |
||||||
|
name: 'alertingDatasource', |
||||||
|
} as any) as DataSourceApi; |
||||||
|
const datasource: DataSourceApi = ({ |
||||||
|
meta: ({ alerting: false } as any) as PluginMeta, |
||||||
|
targetContainsTemplate: () => false, |
||||||
|
name: 'datasource', |
||||||
|
} as any) as DataSourceApi; |
||||||
|
|
||||||
|
const datasourceSrv: DataSourceSrv = { |
||||||
|
get: (name: string) => { |
||||||
|
if (name === datasource.name) { |
||||||
|
return Promise.resolve(datasource); |
||||||
|
} |
||||||
|
|
||||||
|
return Promise.resolve(alertingDatasource); |
||||||
|
}, |
||||||
|
}; |
||||||
|
const targets: any[] = [ |
||||||
|
{ refId: 'A', query: 'some query', datasource: 'alertingDatasource' }, |
||||||
|
{ refId: 'B', query: 'some query', datasource: 'datasource' }, |
||||||
|
]; |
||||||
|
const transformations: DataTransformerConfig[] = []; |
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); |
||||||
|
|
||||||
|
expect(result).toBe(''); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called with all targets containing template variables', () => { |
||||||
|
it('then it should return false', async () => { |
||||||
|
const datasource: DataSourceApi = ({ |
||||||
|
meta: ({ alerting: true } as any) as PluginMeta, |
||||||
|
targetContainsTemplate: () => true, |
||||||
|
name: 'some name', |
||||||
|
} as any) as DataSourceApi; |
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource); |
||||||
|
const datasourceSrv: DataSourceSrv = { |
||||||
|
get: getMock, |
||||||
|
}; |
||||||
|
const targets: ElasticsearchQuery[] = [ |
||||||
|
{ refId: 'A', query: '@hostname:$hostname', isLogsQuery: false }, |
||||||
|
{ refId: 'B', query: '@instance:$instance', isLogsQuery: false }, |
||||||
|
]; |
||||||
|
const transformations: DataTransformerConfig[] = []; |
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); |
||||||
|
|
||||||
|
expect(result).toBe('Template variables are not supported in alert queries'); |
||||||
|
expect(getMock).toHaveBeenCalledTimes(2); |
||||||
|
expect(getMock).toHaveBeenCalledWith(datasource.name); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called with all targets using a datasource that does not support alerting', () => { |
||||||
|
it('then it should return false', async () => { |
||||||
|
const datasource: DataSourceApi = ({ |
||||||
|
meta: ({ alerting: false } as any) as PluginMeta, |
||||||
|
targetContainsTemplate: () => false, |
||||||
|
name: 'some name', |
||||||
|
} as any) as DataSourceApi; |
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource); |
||||||
|
const datasourceSrv: DataSourceSrv = { |
||||||
|
get: getMock, |
||||||
|
}; |
||||||
|
const targets: ElasticsearchQuery[] = [ |
||||||
|
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false }, |
||||||
|
{ refId: 'B', query: '@instance:instance', isLogsQuery: false }, |
||||||
|
]; |
||||||
|
const transformations: DataTransformerConfig[] = []; |
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); |
||||||
|
|
||||||
|
expect(result).toBe('The datasource does not support alerting queries'); |
||||||
|
expect(getMock).toHaveBeenCalledTimes(2); |
||||||
|
expect(getMock).toHaveBeenCalledWith(datasource.name); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called with transformations', () => { |
||||||
|
it('then it should return false', async () => { |
||||||
|
const datasource: DataSourceApi = ({ |
||||||
|
meta: ({ alerting: true } as any) as PluginMeta, |
||||||
|
targetContainsTemplate: () => false, |
||||||
|
name: 'some name', |
||||||
|
} as any) as DataSourceApi; |
||||||
|
const getMock = jest.fn().mockResolvedValue(datasource); |
||||||
|
const datasourceSrv: DataSourceSrv = { |
||||||
|
get: getMock, |
||||||
|
}; |
||||||
|
const targets: ElasticsearchQuery[] = [ |
||||||
|
{ refId: 'A', query: '@hostname:hostname', isLogsQuery: false }, |
||||||
|
{ refId: 'B', query: '@instance:instance', isLogsQuery: false }, |
||||||
|
]; |
||||||
|
const transformations: DataTransformerConfig[] = [{ id: 'A', options: null }]; |
||||||
|
|
||||||
|
const result = await getAlertingValidationMessage(transformations, targets, datasourceSrv, datasource.name); |
||||||
|
|
||||||
|
expect(result).toBe('Transformations are not supported in alert queries'); |
||||||
|
expect(getMock).toHaveBeenCalledTimes(0); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,49 @@ |
|||||||
|
import { DataQuery } from '@grafana/ui'; |
||||||
|
import { DataSourceSrv } from '@grafana/runtime'; |
||||||
|
import { DataTransformerConfig } from '@grafana/data'; |
||||||
|
|
||||||
|
export const getDefaultCondition = () => ({ |
||||||
|
type: 'query', |
||||||
|
query: { params: ['A', '5m', 'now'] }, |
||||||
|
reducer: { type: 'avg', params: [] as any[] }, |
||||||
|
evaluator: { type: 'gt', params: [null] as any[] }, |
||||||
|
operator: { type: 'and' }, |
||||||
|
}); |
||||||
|
|
||||||
|
export const getAlertingValidationMessage = async ( |
||||||
|
transformations: DataTransformerConfig[], |
||||||
|
targets: DataQuery[], |
||||||
|
datasourceSrv: DataSourceSrv, |
||||||
|
datasourceName: string |
||||||
|
): Promise<string> => { |
||||||
|
if (targets.length === 0) { |
||||||
|
return 'Could not find any metric queries'; |
||||||
|
} |
||||||
|
|
||||||
|
if (transformations && transformations.length) { |
||||||
|
return 'Transformations are not supported in alert queries'; |
||||||
|
} |
||||||
|
|
||||||
|
let alertingNotSupported = 0; |
||||||
|
let templateVariablesNotSupported = 0; |
||||||
|
|
||||||
|
for (const target of targets) { |
||||||
|
const dsName = target.datasource || datasourceName; |
||||||
|
const ds = await datasourceSrv.get(dsName); |
||||||
|
if (!ds.meta.alerting) { |
||||||
|
alertingNotSupported++; |
||||||
|
} else if (ds.targetContainsTemplate && ds.targetContainsTemplate(target)) { |
||||||
|
templateVariablesNotSupported++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (alertingNotSupported === targets.length) { |
||||||
|
return 'The datasource does not support alerting queries'; |
||||||
|
} |
||||||
|
|
||||||
|
if (templateVariablesNotSupported === targets.length) { |
||||||
|
return 'Template variables are not supported in alert queries'; |
||||||
|
} |
||||||
|
|
||||||
|
return ''; |
||||||
|
}; |
@ -0,0 +1,127 @@ |
|||||||
|
import { thunkTester } from '../../../../../test/core/thunk/thunkTester'; |
||||||
|
import { initialState, getPanelEditorTab, PanelEditorTabIds } from './reducers'; |
||||||
|
import { refreshPanelEditor, panelEditorInitCompleted, changePanelEditorTab } from './actions'; |
||||||
|
import { updateLocation } from '../../../../core/actions'; |
||||||
|
|
||||||
|
describe('refreshPanelEditor', () => { |
||||||
|
describe('when called and there is no activeTab in state', () => { |
||||||
|
it('then the dispatched action should default the activeTab to PanelEditorTabIds.Queries', async () => { |
||||||
|
const activeTab = PanelEditorTabIds.Queries; |
||||||
|
const tabs = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert), |
||||||
|
]; |
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState, activeTab: null } }) |
||||||
|
.givenThunk(refreshPanelEditor) |
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true }); |
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1); |
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs })); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called and there is already an activeTab in state', () => { |
||||||
|
it('then the dispatched action should include activeTab from state', async () => { |
||||||
|
const activeTab = PanelEditorTabIds.Visualization; |
||||||
|
const tabs = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert), |
||||||
|
]; |
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState, activeTab } }) |
||||||
|
.givenThunk(refreshPanelEditor) |
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true }); |
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1); |
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs })); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called and plugin has no queries tab', () => { |
||||||
|
it('then the dispatched action should not include Queries tab and default the activeTab to PanelEditorTabIds.Visualization', async () => { |
||||||
|
const activeTab = PanelEditorTabIds.Visualization; |
||||||
|
const tabs = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert), |
||||||
|
]; |
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } }) |
||||||
|
.givenThunk(refreshPanelEditor) |
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: false, alertingEnabled: true, usesGraphPlugin: true }); |
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1); |
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs })); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called and alerting is enabled and the visualization is the graph plugin', () => { |
||||||
|
it('then the dispatched action should include the alert tab', async () => { |
||||||
|
const activeTab = PanelEditorTabIds.Queries; |
||||||
|
const tabs = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Alert), |
||||||
|
]; |
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } }) |
||||||
|
.givenThunk(refreshPanelEditor) |
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true }); |
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1); |
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs })); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called and alerting is not enabled', () => { |
||||||
|
it('then the dispatched action should not include the alert tab', async () => { |
||||||
|
const activeTab = PanelEditorTabIds.Queries; |
||||||
|
const tabs = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
]; |
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } }) |
||||||
|
.givenThunk(refreshPanelEditor) |
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: false, usesGraphPlugin: true }); |
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1); |
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs })); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when called and the visualization is not the graph plugin', () => { |
||||||
|
it('then the dispatched action should not include the alert tab', async () => { |
||||||
|
const activeTab = PanelEditorTabIds.Queries; |
||||||
|
const tabs = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
]; |
||||||
|
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } }) |
||||||
|
.givenThunk(refreshPanelEditor) |
||||||
|
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: false }); |
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1); |
||||||
|
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs })); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('changePanelEditorTab', () => { |
||||||
|
describe('when called', () => { |
||||||
|
it('then it should dispatch correct actions', async () => { |
||||||
|
const activeTab = getPanelEditorTab(PanelEditorTabIds.Visualization); |
||||||
|
const dispatchedActions = await thunkTester({}) |
||||||
|
.givenThunk(changePanelEditorTab) |
||||||
|
.whenThunkIsDispatched(activeTab); |
||||||
|
|
||||||
|
expect(dispatchedActions.length).toBe(1); |
||||||
|
expect(dispatchedActions).toEqual([ |
||||||
|
updateLocation({ query: { tab: activeTab.id, openVizPicker: null }, partial: true }), |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,54 @@ |
|||||||
|
import { actionCreatorFactory, noPayloadActionCreatorFactory } from '../../../../core/redux'; |
||||||
|
import { PanelEditorTabIds, PanelEditorTab, getPanelEditorTab } from './reducers'; |
||||||
|
import { ThunkResult } from '../../../../types'; |
||||||
|
import { updateLocation } from '../../../../core/actions'; |
||||||
|
|
||||||
|
export interface PanelEditorInitCompleted { |
||||||
|
activeTab: PanelEditorTabIds; |
||||||
|
tabs: PanelEditorTab[]; |
||||||
|
} |
||||||
|
|
||||||
|
export const panelEditorInitCompleted = actionCreatorFactory<PanelEditorInitCompleted>( |
||||||
|
'PANEL_EDITOR_INIT_COMPLETED' |
||||||
|
).create(); |
||||||
|
|
||||||
|
export const panelEditorCleanUp = noPayloadActionCreatorFactory('PANEL_EDITOR_CLEAN_UP').create(); |
||||||
|
|
||||||
|
export const refreshPanelEditor = (props: { |
||||||
|
hasQueriesTab?: boolean; |
||||||
|
usesGraphPlugin?: boolean; |
||||||
|
alertingEnabled?: boolean; |
||||||
|
}): ThunkResult<void> => { |
||||||
|
return async (dispatch, getState) => { |
||||||
|
let activeTab = getState().panelEditor.activeTab || PanelEditorTabIds.Queries; |
||||||
|
const { hasQueriesTab, usesGraphPlugin, alertingEnabled } = props; |
||||||
|
|
||||||
|
const tabs: PanelEditorTab[] = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
]; |
||||||
|
|
||||||
|
// handle panels that do not have queries tab
|
||||||
|
if (!hasQueriesTab) { |
||||||
|
// remove queries tab
|
||||||
|
tabs.shift(); |
||||||
|
// switch tab
|
||||||
|
if (activeTab === PanelEditorTabIds.Queries) { |
||||||
|
activeTab = PanelEditorTabIds.Visualization; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (alertingEnabled && usesGraphPlugin) { |
||||||
|
tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert)); |
||||||
|
} |
||||||
|
|
||||||
|
dispatch(panelEditorInitCompleted({ activeTab, tabs })); |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export const changePanelEditorTab = (activeTab: PanelEditorTab): ThunkResult<void> => { |
||||||
|
return async dispatch => { |
||||||
|
dispatch(updateLocation({ query: { tab: activeTab.id, openVizPicker: null }, partial: true })); |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,35 @@ |
|||||||
|
import { reducerTester } from '../../../../../test/core/redux/reducerTester'; |
||||||
|
import { initialState, panelEditorReducer, PanelEditorTabIds, PanelEditorTab, getPanelEditorTab } from './reducers'; |
||||||
|
import { panelEditorInitCompleted, panelEditorCleanUp } from './actions'; |
||||||
|
|
||||||
|
describe('panelEditorReducer', () => { |
||||||
|
describe('when panelEditorInitCompleted is dispatched', () => { |
||||||
|
it('then state should be correct', () => { |
||||||
|
const activeTab = PanelEditorTabIds.Alert; |
||||||
|
const tabs: PanelEditorTab[] = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
]; |
||||||
|
reducerTester() |
||||||
|
.givenReducer(panelEditorReducer, initialState) |
||||||
|
.whenActionIsDispatched(panelEditorInitCompleted({ activeTab, tabs })) |
||||||
|
.thenStateShouldEqual({ activeTab, tabs }); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('when panelEditorCleanUp is dispatched', () => { |
||||||
|
it('then state should be intialState', () => { |
||||||
|
const activeTab = PanelEditorTabIds.Alert; |
||||||
|
const tabs: PanelEditorTab[] = [ |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Queries), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Visualization), |
||||||
|
getPanelEditorTab(PanelEditorTabIds.Advanced), |
||||||
|
]; |
||||||
|
reducerTester() |
||||||
|
.givenReducer(panelEditorReducer, { activeTab, tabs }) |
||||||
|
.whenActionIsDispatched(panelEditorCleanUp()) |
||||||
|
.thenStateShouldEqual(initialState); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,56 @@ |
|||||||
|
import { reducerFactory } from '../../../../core/redux'; |
||||||
|
import { panelEditorCleanUp, panelEditorInitCompleted } from './actions'; |
||||||
|
|
||||||
|
export interface PanelEditorTab { |
||||||
|
id: string; |
||||||
|
text: string; |
||||||
|
} |
||||||
|
|
||||||
|
export enum PanelEditorTabIds { |
||||||
|
Queries = 'queries', |
||||||
|
Visualization = 'visualization', |
||||||
|
Advanced = 'advanced', |
||||||
|
Alert = 'alert', |
||||||
|
} |
||||||
|
|
||||||
|
export const panelEditorTabTexts = { |
||||||
|
[PanelEditorTabIds.Queries]: 'Queries', |
||||||
|
[PanelEditorTabIds.Visualization]: 'Visualization', |
||||||
|
[PanelEditorTabIds.Advanced]: 'General', |
||||||
|
[PanelEditorTabIds.Alert]: 'Alert', |
||||||
|
}; |
||||||
|
|
||||||
|
export const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => { |
||||||
|
return { |
||||||
|
id: tabId, |
||||||
|
text: panelEditorTabTexts[tabId], |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export interface PanelEditorState { |
||||||
|
activeTab: PanelEditorTabIds; |
||||||
|
tabs: PanelEditorTab[]; |
||||||
|
} |
||||||
|
|
||||||
|
export const initialState: PanelEditorState = { |
||||||
|
activeTab: null, |
||||||
|
tabs: [], |
||||||
|
}; |
||||||
|
|
||||||
|
export const panelEditorReducer = reducerFactory<PanelEditorState>(initialState) |
||||||
|
.addMapper({ |
||||||
|
filter: panelEditorInitCompleted, |
||||||
|
mapper: (state, action): PanelEditorState => { |
||||||
|
const { activeTab, tabs } = action.payload; |
||||||
|
return { |
||||||
|
...state, |
||||||
|
activeTab, |
||||||
|
tabs, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}) |
||||||
|
.addMapper({ |
||||||
|
filter: panelEditorCleanUp, |
||||||
|
mapper: (): PanelEditorState => initialState, |
||||||
|
}) |
||||||
|
.create(); |
Loading…
Reference in new issue