Commit bf3dcc80 by Jake Krammer Committed by GitHub

Admin: Fix updating organization name not updating configuration subtitle (#26315)

* Fix updating organization name not updating configuration subtitle

* PR feedback: Remove unnecessary square brackets

* Refactor code to update redux state in a safer way, add org action test

* Refactor updateConfigurationSubtitle test, remove jest.mock usage

* Consolidate dependency type
parent eed313f3
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 };
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<NavIndex>()
.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<NavIndex>()
.givenReducer(navIndexReducer, { ...initialState })
.whenActionIsDispatched(updateConfigurationSubtitle(newOrgName))
.thenStateShouldEqual(expectedState);
});
});
});
......@@ -41,6 +41,17 @@ function buildWarningNav(text: string, subTitle?: string): NavModel {
export const initialState: NavIndex = {};
export const updateNavIndex = createAction<NavModelItem>('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<string>('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;
......
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 : ''}`;
......
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);
});
});
});
import { ThunkResult } from 'app/types';
import { getBackendSrv } from '@grafana/runtime';
import { organizationLoaded } from './reducers';
import { updateConfigurationSubtitle } from 'app/core/actions';
export function loadOrganization(): ThunkResult<any> {
type OrganizationDependencies = { getBackendSrv: typeof getBackendSrv };
export function loadOrganization(
dependencies: OrganizationDependencies = { getBackendSrv: getBackendSrv }
): ThunkResult<any> {
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<any> {
export function updateOrganization(
dependencies: OrganizationDependencies = { getBackendSrv: getBackendSrv }
): ThunkResult<any> {
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));
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment