Skip to content

Commit d525a1c

Browse files
opensearch-trigger-bot[bot]github-actions[bot]opensearch-changeset-bot[bot]
authored
[UI setting] feat: keep backward compatibility for UI setting client. (#9854) (#9924)
* feat: keep backward compatibility for scope * Changeset file for PR #9854 created/updated * feat: update warning messages * feat: update logic to keep backward compatibility * fix: UT failure * feat: optimize code * fix: bootstrap * fix: boostrap * fix: bootstrap error --------- (cherry picked from commit 027baef) Signed-off-by: SuZhou-Joe <suzhou@amazon.com> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent 646eb48 commit d525a1c

File tree

6 files changed

+215
-47
lines changed

6 files changed

+215
-47
lines changed

changelogs/fragments/9854.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Keep backward compatibility for UI setting client ([#9854](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/9854))

src/core/server/ui_settings/ui_settings_client.test.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -137,19 +137,39 @@ describe('ui settings', () => {
137137
);
138138
});
139139

140-
it('shall throw error when trying to update multi-scope settings without passing a scope', async () => {
140+
it('shall give deprecation warnings when trying to update multi-scope settings without passing a scope', async () => {
141141
const value = chance.word();
142142
const defaults = {
143143
user: { value, scope: 'user' },
144144
workspace: { value, scope: ['workspace', 'user'] },
145+
globalAndWorkspace: { value, scope: ['workspace', 'global'] },
145146
};
146-
const { uiSettings } = setup({ defaults });
147+
const { uiSettings, savedObjectsClient } = setup({ defaults });
147148

148-
try {
149-
await uiSettings.setMany({ one: 'value', user: 'val', workspace: 'value' });
150-
} catch (error) {
151-
expect(error.message).toBe('Unable to update "workspace", because it has multiple scopes');
152-
}
149+
await expect(
150+
uiSettings.setMany({
151+
one: 'value',
152+
user: 'val',
153+
workspace: 'value',
154+
globalAndWorkspace: 'value',
155+
})
156+
).resolves.not.toThrow();
157+
158+
expect(savedObjectsClient.update).toBeCalledWith('config', ID, {
159+
globalAndWorkspace: 'value',
160+
one: 'value',
161+
});
162+
expect(savedObjectsClient.update).toBeCalledWith('config', `<current_workspace>_${ID}`, {
163+
workspace: 'value',
164+
});
165+
166+
expect(logger.warn).toBeCalledWith(
167+
'Deprecation warning: The setting "workspace" has multiple scopes. Please specify a scope when updating it.'
168+
);
169+
170+
expect(logger.warn).toBeCalledWith(
171+
'Deprecation warning: The setting "globalAndWorkspace" has multiple scopes. Please specify a scope when updating it.'
172+
);
153173
});
154174

155175
it('automatically creates the savedConfig if it is missing', async () => {

src/core/server/ui_settings/ui_settings_client.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -326,27 +326,45 @@ export class UiSettingsClient implements IUiSettingsClient {
326326
* @returns [global, user, workspace]
327327
*/
328328
private groupChanges(changes: Record<string, any>) {
329-
const userChanges = {} as Record<string, any>;
330-
const globalChanges = {} as Record<string, any>;
331-
const adminSettingChanges = {} as Record<string, any>;
332-
const workspaceChanges = {} as Record<string, any>;
329+
const groupedChanges: Record<UiSettingScope, any> = {
330+
[UiSettingScope.GLOBAL]: {},
331+
[UiSettingScope.USER]: {},
332+
[UiSettingScope.WORKSPACE]: {},
333+
[UiSettingScope.DASHBOARD_ADMIN]: {},
334+
};
333335

334336
Object.entries(changes).forEach(([key, val]) => {
335337
if (Array.isArray(this.defaults[key]?.scope) && (this.defaults[key].scope?.length ?? 0) > 1) {
336-
// if this setting has more than one scope, we should not update this setting without specifying scope
337-
throw new Error(`Unable to update "${key}", because it has multiple scopes`);
338+
const keyScopes = Array<UiSettingScope>().concat(this.defaults[key].scope || []);
339+
// If this setting applies to multiple scopes including global
340+
// categorize it as global to maintain backward compatibility.
341+
if (keyScopes.includes(UiSettingScope.GLOBAL)) {
342+
groupedChanges[UiSettingScope.GLOBAL][key] = val;
343+
} else {
344+
// else we use first scope as fallback
345+
groupedChanges[keyScopes[0]][key] = val;
346+
}
347+
348+
this.log.warn(
349+
`Deprecation warning: The setting "${key}" has multiple scopes. Please specify a scope when updating it.`
350+
);
338351
} else if (this.userLevelSettingsKeys.includes(key)) {
339-
userChanges[key] = val;
352+
groupedChanges[UiSettingScope.USER][key] = val;
340353
} else if (this.adminUiSettingsKeys.includes(key)) {
341-
adminSettingChanges[key] = val;
354+
groupedChanges[UiSettingScope.DASHBOARD_ADMIN][key] = val;
342355
} else if (this.workspaceLevelSettingsKeys.includes(key)) {
343-
workspaceChanges[key] = val;
356+
groupedChanges[UiSettingScope.WORKSPACE][key] = val;
344357
} else {
345-
globalChanges[key] = val;
358+
groupedChanges[UiSettingScope.GLOBAL][key] = val;
346359
}
347360
});
348361

349-
return [globalChanges, userChanges, workspaceChanges, adminSettingChanges];
362+
return [
363+
groupedChanges[UiSettingScope.GLOBAL],
364+
groupedChanges[UiSettingScope.USER],
365+
groupedChanges[UiSettingScope.WORKSPACE],
366+
groupedChanges[UiSettingScope.DASHBOARD_ADMIN],
367+
];
350368
}
351369

352370
private async write({

src/plugins/workspace/server/plugin.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
7070
private workspaceSavedObjectsClientWrapper?: WorkspaceSavedObjectsClientWrapper;
7171
private workspaceUiSettingsClientWrapper?: WorkspaceUiSettingsClientWrapper;
7272
private workspaceConfig$: Observable<ConfigSchema>;
73+
private env: PluginInitializerContext['env'];
7374

7475
private proxyWorkspaceTrafficToRealHandler(setupDeps: CoreSetup) {
7576
/**
@@ -218,6 +219,7 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
218219
this.logger = initializerContext.logger.get();
219220
this.globalConfig$ = initializerContext.config.legacy.globalConfig$;
220221
this.workspaceConfig$ = initializerContext.config.create();
222+
this.env = initializerContext.env;
221223
}
222224

223225
public async setup(core: CoreSetup, deps: WorkspacePluginDependencies) {
@@ -245,7 +247,10 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
245247
);
246248
this.proxyWorkspaceTrafficToRealHandler(core);
247249

248-
const workspaceUiSettingsClientWrapper = new WorkspaceUiSettingsClientWrapper(this.logger);
250+
const workspaceUiSettingsClientWrapper = new WorkspaceUiSettingsClientWrapper(
251+
this.logger,
252+
this.env
253+
);
249254
this.workspaceUiSettingsClientWrapper = workspaceUiSettingsClientWrapper;
250255
core.savedObjects.addClientWrapper(
251256
PRIORITY_FOR_WORKSPACE_UI_SETTINGS_WRAPPER,
@@ -303,6 +308,9 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
303308
this.workspaceConflictControl?.setSerializer(core.savedObjects.createSerializer());
304309
this.workspaceSavedObjectsClientWrapper?.setScopedClient(core.savedObjects.getScopedClient);
305310
this.workspaceUiSettingsClientWrapper?.setScopedClient(core.savedObjects.getScopedClient);
311+
this.workspaceUiSettingsClientWrapper?.setAsScopedUISettingsClient(
312+
core.uiSettings.asScopedToClient
313+
);
306314

307315
return {
308316
client: this.client as IWorkspaceClientImpl,

src/plugins/workspace/server/saved_objects/workspace_ui_settings_client_wrapper.test.ts

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@
44
*/
55

66
import { loggerMock } from '@osd/logging/target/mocks';
7-
import { httpServerMock, savedObjectsClientMock, coreMock } from '../../../../core/server/mocks';
7+
import {
8+
httpServerMock,
9+
savedObjectsClientMock,
10+
coreMock,
11+
uiSettingsServiceMock,
12+
} from '../../../../core/server/mocks';
813
import { WorkspaceUiSettingsClientWrapper } from './workspace_ui_settings_client_wrapper';
914
import {
1015
WORKSPACE_TYPE,
1116
CURRENT_WORKSPACE_PLACEHOLDER,
1217
SavedObjectsErrorHelpers,
18+
PackageInfo,
19+
UiSettingScope,
1320
} from '../../../../core/server';
1421
import {
1522
DEFAULT_DATA_SOURCE_UI_SETTINGS_ID,
@@ -19,13 +26,34 @@ import * as utils from '../../../../core/server/utils';
1926

2027
jest.mock('../../../../core/server/utils');
2128

29+
const WORKSPACE_SCOPE_SETTING_WITHOUT_VALUE_ID = 'workspace_scope_setting_without_value';
30+
const GLOBAL_SCOPE_SETTING_ID = 'global_scope_setting';
31+
2232
describe('WorkspaceUiSettingsClientWrapper', () => {
2333
const createWrappedClient = () => {
2434
const clientMock = savedObjectsClientMock.create();
2535
const getClientMock = jest.fn().mockReturnValue(clientMock);
2636
const requestHandlerContext = coreMock.createRequestHandlerContext();
2737
const requestMock = httpServerMock.createOpenSearchDashboardsRequest();
2838
const logger = loggerMock.create();
39+
const uiSettingsMock = coreMock.createStart().uiSettings;
40+
const uiSettingsClientMock = uiSettingsServiceMock.createClient();
41+
uiSettingsMock.asScopedToClient.mockReturnValue(uiSettingsClientMock);
42+
const pluginInitializerContext = coreMock.createPluginInitializerContext();
43+
(pluginInitializerContext.env.packageInfo as PackageInfo).version = '3.0.0';
44+
45+
uiSettingsClientMock.getRegistered.mockReturnValue({
46+
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: {
47+
scope: [UiSettingScope.GLOBAL, UiSettingScope.WORKSPACE],
48+
},
49+
[DEFAULT_INDEX_PATTERN_UI_SETTINGS_ID]: {
50+
scope: UiSettingScope.WORKSPACE,
51+
},
52+
[WORKSPACE_SCOPE_SETTING_WITHOUT_VALUE_ID]: {
53+
scope: UiSettingScope.WORKSPACE,
54+
},
55+
[GLOBAL_SCOPE_SETTING_ID]: {},
56+
});
2957

3058
clientMock.get.mockImplementation(async (type, id) => {
3159
if (type === 'config') {
@@ -55,8 +83,20 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
5583
return Promise.reject();
5684
});
5785

58-
const wrapper = new WorkspaceUiSettingsClientWrapper(logger);
86+
clientMock.update.mockResolvedValue({
87+
id: '3.0.0',
88+
references: [],
89+
type: 'config',
90+
attributes: {
91+
defaultDashboard: 'default-dashboard-global',
92+
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: 'default-ds-global',
93+
[DEFAULT_INDEX_PATTERN_UI_SETTINGS_ID]: 'default-index-global',
94+
},
95+
});
96+
97+
const wrapper = new WorkspaceUiSettingsClientWrapper(logger, pluginInitializerContext.env);
5998
wrapper.setScopedClient(getClientMock);
99+
wrapper.setAsScopedUISettingsClient(uiSettingsMock.asScopedToClient);
60100

61101
return {
62102
wrappedClient: wrapper.wrapperFactory({
@@ -94,14 +134,18 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
94134

95135
const { wrappedClient } = createWrappedClient();
96136

97-
const result = await wrappedClient.get(`config`, `${CURRENT_WORKSPACE_PLACEHOLDER}_3.0.0`);
98-
expect(result).toEqual({
137+
const result = await wrappedClient.get<{
138+
[key: string]: unknown;
139+
}>(`config`, `${CURRENT_WORKSPACE_PLACEHOLDER}_3.0.0`);
140+
expect(result).toStrictEqual({
99141
id: '3.0.0',
100142
references: [],
101143
type: 'config',
102144
attributes: {
103145
defaultDashboard: 'default-dashboard-workspace',
104146
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: 'default-ds-workspace',
147+
[DEFAULT_INDEX_PATTERN_UI_SETTINGS_ID]: undefined,
148+
[WORKSPACE_SCOPE_SETTING_WITHOUT_VALUE_ID]: undefined,
105149
},
106150
});
107151
});
@@ -182,7 +226,7 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
182226
expect(error.message).toEqual('Bad Request');
183227
});
184228

185-
it('should update global ui settings', async () => {
229+
it('should update global ui settings when out of workspace', async () => {
186230
// Currently NOT in a workspace
187231
jest.spyOn(utils, 'getWorkspaceState').mockReturnValue({});
188232

@@ -199,4 +243,28 @@ describe('WorkspaceUiSettingsClientWrapper', () => {
199243
{}
200244
);
201245
});
246+
247+
it('should update workspace settings when inside workspace and config id equals global setting', async () => {
248+
jest.spyOn(utils, 'getWorkspaceState').mockReturnValue({ requestWorkspaceId: 'workspace-id' });
249+
250+
const { wrappedClient, clientMock, logger } = createWrappedClient();
251+
252+
await wrappedClient.update('config', '3.0.0', {
253+
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: 'data_source_id',
254+
});
255+
256+
expect(clientMock.update).toHaveBeenCalledWith(
257+
WORKSPACE_TYPE,
258+
'workspace-id',
259+
{
260+
uiSettings: expect.objectContaining({
261+
[DEFAULT_DATA_SOURCE_UI_SETTINGS_ID]: 'data_source_id',
262+
}),
263+
},
264+
{}
265+
);
266+
expect(logger.warn).toBeCalledWith(
267+
'Deprecation warning: updating workspace settings through global scope will no longer be supported.'
268+
);
269+
});
202270
});

0 commit comments

Comments
 (0)