diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 4ee3d55a769..6b48b66a501 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,5 +1,5 @@ import { clearAppNotification, notifyApp } from '../reducers/appNotification'; import { updateLocation } from '../reducers/location'; -import { updateNavIndex } from '../reducers/navModel'; +import { updateNavIndex, updateConfigurationSubtitle } from '../reducers/navModel'; -export { updateLocation, updateNavIndex, notifyApp, clearAppNotification }; +export { updateLocation, updateNavIndex, updateConfigurationSubtitle, notifyApp, clearAppNotification }; diff --git a/public/app/core/reducers/navModel.test.ts b/public/app/core/reducers/navModel.test.ts index 5d8f9caffc3..b13874baa1f 100644 --- a/public/app/core/reducers/navModel.test.ts +++ b/public/app/core/reducers/navModel.test.ts @@ -1,12 +1,12 @@ import { reducerTester } from '../../../test/core/redux/reducerTester'; -import { initialState, navIndexReducer, updateNavIndex } from './navModel'; +import { navIndexReducer, updateNavIndex, updateConfigurationSubtitle } from './navModel'; import { NavIndex } from '@grafana/data'; -describe('applicationReducer', () => { +describe('navModelReducer', () => { describe('when updateNavIndex is dispatched', () => { it('then state should be correct', () => { reducerTester() - .givenReducer(navIndexReducer, { ...initialState }) + .givenReducer(navIndexReducer, {}) .whenActionIsDispatched( updateNavIndex({ id: 'parent', @@ -20,7 +20,6 @@ describe('applicationReducer', () => { }) ) .thenStateShouldEqual({ - ...initialState, child: { id: 'child', text: 'Child', @@ -38,4 +37,44 @@ describe('applicationReducer', () => { }); }); }); + + describe('when updateConfigurationSubtitle is dispatched', () => { + it('then state should be correct', () => { + const originalCfg = { id: 'cfg', subTitle: 'Organization: Org 1', text: 'Configuration' }; + const datasources = { id: 'datasources', text: 'Data Sources' }; + const users = { id: 'users', text: 'Users' }; + const teams = { id: 'teams', text: 'Teams' }; + const plugins = { id: 'plugins', text: 'Plugins' }; + const orgsettings = { id: 'org-settings', text: 'Preferences' }; + const apikeys = { id: 'apikeys', text: 'API Keys' }; + + const initialState = { + cfg: { ...originalCfg, children: [datasources, users, teams, plugins, orgsettings, apikeys] }, + datasources: { ...datasources, parentItem: originalCfg }, + users: { ...users, parentItem: originalCfg }, + teams: { ...teams, parentItem: originalCfg }, + plugins: { ...plugins, parentItem: originalCfg }, + 'org-settings': { ...orgsettings, parentItem: originalCfg }, + apikeys: { ...apikeys, parentItem: originalCfg }, + }; + + const newOrgName = 'Org 2'; + const subTitle = `Organization: ${newOrgName}`; + const newCfg = { ...originalCfg, subTitle }; + const expectedState = { + cfg: { ...newCfg, children: [datasources, users, teams, plugins, orgsettings, apikeys] }, + datasources: { ...datasources, parentItem: newCfg }, + users: { ...users, parentItem: newCfg }, + teams: { ...teams, parentItem: newCfg }, + plugins: { ...plugins, parentItem: newCfg }, + 'org-settings': { ...orgsettings, parentItem: newCfg }, + apikeys: { ...apikeys, parentItem: newCfg }, + }; + + reducerTester() + .givenReducer(navIndexReducer, { ...initialState }) + .whenActionIsDispatched(updateConfigurationSubtitle(newOrgName)) + .thenStateShouldEqual(expectedState); + }); + }); }); diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 3af34c8fde9..0e8ca795b84 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -41,6 +41,17 @@ function buildWarningNav(text: string, subTitle?: string): NavModel { export const initialState: NavIndex = {}; export const updateNavIndex = createAction('navIndex/updateNavIndex'); +// Since the configuration subtitle includes the organization name, we include this action to update the org name if it changes. +export const updateConfigurationSubtitle = createAction('navIndex/updateConfigurationSubtitle'); + +export const getItemWithNewSubTitle = (item: NavModelItem, subTitle: string): NavModelItem => ({ + ...item, + parentItem: { + ...item.parentItem, + text: item.parentItem?.text ?? '', + subTitle, + }, +}); // Redux Toolkit uses ImmerJs as part of their solution to ensure that state objects are not mutated. // ImmerJs has an autoFreeze option that freezes objects from change which means this reducer can't be migrated to createSlice @@ -60,6 +71,19 @@ export const navIndexReducer = (state: NavIndex = initialState, action: AnyActio } return { ...state, ...newPages }; + } else if (updateConfigurationSubtitle.match(action)) { + const subTitle = `Organization: ${action.payload}`; + + return { + ...state, + cfg: { ...state.cfg, subTitle }, + datasources: getItemWithNewSubTitle(state.datasources, subTitle), + users: getItemWithNewSubTitle(state.users, subTitle), + teams: getItemWithNewSubTitle(state.teams, subTitle), + plugins: getItemWithNewSubTitle(state.plugins, subTitle), + 'org-settings': getItemWithNewSubTitle(state['org-settings'], subTitle), + apikeys: getItemWithNewSubTitle(state.apikeys, subTitle), + }; } return state; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 6ce18154ba6..3fc5ea7329f 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,6 +1,6 @@ import { NavModel, NavModelItem, NavIndex } from '@grafana/data'; -function getNotFoundModel(): NavModel { +const getNotFoundModel = (): NavModel => { const node: NavModelItem = { id: 'not-found', text: 'Page not found', @@ -13,9 +13,9 @@ function getNotFoundModel(): NavModel { node: node, main: node, }; -} +}; -export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel, onlyChild = false): NavModel { +export const getNavModel = (navIndex: NavIndex, id: string, fallback?: NavModel, onlyChild = false): NavModel => { if (navIndex[id]) { const node = navIndex[id]; @@ -36,8 +36,8 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel, } return { - node: node, - main: main, + node, + main, }; } @@ -46,7 +46,7 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel, } return getNotFoundModel(); -} +}; export const getTitleFromNavModel = (navModel: NavModel) => { return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : ''}`; diff --git a/public/app/features/org/state/actions.test.ts b/public/app/features/org/state/actions.test.ts new file mode 100644 index 00000000000..9d207f1b1a4 --- /dev/null +++ b/public/app/features/org/state/actions.test.ts @@ -0,0 +1,40 @@ +import { updateOrganization } from './actions'; +import { updateConfigurationSubtitle } from 'app/core/actions'; +import { thunkTester } from 'test/core/thunk/thunkTester'; + +const setup = () => { + const initialState = { + organization: { + organization: { + id: 1, + name: 'New Org Name', + }, + }, + }; + + return { + initialState, + }; +}; + +describe('updateOrganization', () => { + describe('when updateOrganization thunk is dispatched', () => { + const getMock = jest.fn().mockResolvedValue({ id: 1, name: 'New Org Name' }); + const putMock = jest.fn().mockResolvedValue({ id: 1, name: 'New Org Name' }); + const backendSrvMock: any = { + get: getMock, + put: putMock, + }; + + it('then it should dispatch updateConfigurationSubtitle', async () => { + const { initialState } = setup(); + + const dispatchedActions = await thunkTester(initialState) + .givenThunk(updateOrganization) + .whenThunkIsDispatched({ getBackendSrv: () => backendSrvMock }); + + expect(dispatchedActions[0].type).toEqual(updateConfigurationSubtitle.type); + expect(dispatchedActions[0].payload).toEqual(initialState.organization.organization.name); + }); + }); +}); diff --git a/public/app/features/org/state/actions.ts b/public/app/features/org/state/actions.ts index f94cd4d1cc5..4fe67f010c4 100644 --- a/public/app/features/org/state/actions.ts +++ b/public/app/features/org/state/actions.ts @@ -1,22 +1,30 @@ import { ThunkResult } from 'app/types'; import { getBackendSrv } from '@grafana/runtime'; import { organizationLoaded } from './reducers'; +import { updateConfigurationSubtitle } from 'app/core/actions'; -export function loadOrganization(): ThunkResult { +type OrganizationDependencies = { getBackendSrv: typeof getBackendSrv }; + +export function loadOrganization( + dependencies: OrganizationDependencies = { getBackendSrv: getBackendSrv } +): ThunkResult { return async dispatch => { - const organizationResponse = await getBackendSrv().get('/api/org'); + const organizationResponse = await dependencies.getBackendSrv().get('/api/org'); dispatch(organizationLoaded(organizationResponse)); return organizationResponse; }; } -export function updateOrganization(): ThunkResult { +export function updateOrganization( + dependencies: OrganizationDependencies = { getBackendSrv: getBackendSrv } +): ThunkResult { return async (dispatch, getStore) => { const organization = getStore().organization.organization; - await getBackendSrv().put('/api/org', { name: organization.name }); + await dependencies.getBackendSrv().put('/api/org', { name: organization.name }); - dispatch(loadOrganization()); + dispatch(updateConfigurationSubtitle(organization.name)); + dispatch(loadOrganization(dependencies)); }; }