mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { clearAppNotification, notifyApp } from '../reducers/appNotification';
|
import { clearAppNotification, notifyApp } from '../reducers/appNotification';
|
||||||
import { updateLocation } from '../reducers/location';
|
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 };
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { reducerTester } from '../../../test/core/redux/reducerTester';
|
import { reducerTester } from '../../../test/core/redux/reducerTester';
|
||||||
import { initialState, navIndexReducer, updateNavIndex } from './navModel';
|
import { navIndexReducer, updateNavIndex, updateConfigurationSubtitle } from './navModel';
|
||||||
import { NavIndex } from '@grafana/data';
|
import { NavIndex } from '@grafana/data';
|
||||||
|
|
||||||
describe('applicationReducer', () => {
|
describe('navModelReducer', () => {
|
||||||
describe('when updateNavIndex is dispatched', () => {
|
describe('when updateNavIndex is dispatched', () => {
|
||||||
it('then state should be correct', () => {
|
it('then state should be correct', () => {
|
||||||
reducerTester<NavIndex>()
|
reducerTester<NavIndex>()
|
||||||
.givenReducer(navIndexReducer, { ...initialState })
|
.givenReducer(navIndexReducer, {})
|
||||||
.whenActionIsDispatched(
|
.whenActionIsDispatched(
|
||||||
updateNavIndex({
|
updateNavIndex({
|
||||||
id: 'parent',
|
id: 'parent',
|
||||||
@@ -20,7 +20,6 @@ describe('applicationReducer', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
...initialState,
|
|
||||||
child: {
|
child: {
|
||||||
id: 'child',
|
id: 'child',
|
||||||
text: '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 initialState: NavIndex = {};
|
||||||
|
|
||||||
export const updateNavIndex = createAction<NavModelItem>('navIndex/updateNavIndex');
|
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.
|
// 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
|
// 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 };
|
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;
|
return state;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NavModel, NavModelItem, NavIndex } from '@grafana/data';
|
import { NavModel, NavModelItem, NavIndex } from '@grafana/data';
|
||||||
|
|
||||||
function getNotFoundModel(): NavModel {
|
const getNotFoundModel = (): NavModel => {
|
||||||
const node: NavModelItem = {
|
const node: NavModelItem = {
|
||||||
id: 'not-found',
|
id: 'not-found',
|
||||||
text: 'Page not found',
|
text: 'Page not found',
|
||||||
@@ -13,9 +13,9 @@ function getNotFoundModel(): NavModel {
|
|||||||
node: node,
|
node: node,
|
||||||
main: 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]) {
|
if (navIndex[id]) {
|
||||||
const node = navIndex[id];
|
const node = navIndex[id];
|
||||||
|
|
||||||
@@ -36,8 +36,8 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: node,
|
node,
|
||||||
main: main,
|
main,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return getNotFoundModel();
|
return getNotFoundModel();
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getTitleFromNavModel = (navModel: NavModel) => {
|
export const getTitleFromNavModel = (navModel: NavModel) => {
|
||||||
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : ''}`;
|
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : ''}`;
|
||||||
|
|||||||
40
public/app/features/org/state/actions.test.ts
Normal file
40
public/app/features/org/state/actions.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,22 +1,30 @@
|
|||||||
import { ThunkResult } from 'app/types';
|
import { ThunkResult } from 'app/types';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { organizationLoaded } from './reducers';
|
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 => {
|
return async dispatch => {
|
||||||
const organizationResponse = await getBackendSrv().get('/api/org');
|
const organizationResponse = await dependencies.getBackendSrv().get('/api/org');
|
||||||
dispatch(organizationLoaded(organizationResponse));
|
dispatch(organizationLoaded(organizationResponse));
|
||||||
|
|
||||||
return organizationResponse;
|
return organizationResponse;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateOrganization(): ThunkResult<any> {
|
export function updateOrganization(
|
||||||
|
dependencies: OrganizationDependencies = { getBackendSrv: getBackendSrv }
|
||||||
|
): ThunkResult<any> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
const organization = getStore().organization.organization;
|
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));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user