Skip to content

Commit

Permalink
[Workspace] feat: Add workspace navigation for default route (#7785) (#…
Browse files Browse the repository at this point in the history
…7824)

* add workspace navigation



* Changeset file for PR #7785 created/updated

* optimize the code



* optimize the code



* optimize the test code and add newHomePage check



* delete useless code



---------



(cherry picked from commit e1e8169)

Signed-off-by: yubonluo <[email protected]>
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>
  • Loading branch information
3 people authored Aug 26, 2024
1 parent a128621 commit ac5ed85
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 19 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/7785.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- [Workspace] Add workspace navigation for default route ([#7785](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7785))
2 changes: 2 additions & 0 deletions src/plugins/workspace/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const WORKSPACE_CREATE_APP_ID = 'workspace_create';
export const WORKSPACE_LIST_APP_ID = 'workspace_list';
export const WORKSPACE_DETAIL_APP_ID = 'workspace_detail';
export const WORKSPACE_INITIAL_APP_ID = 'workspace_initial';
export const WORKSPACE_NAVIGATION_APP_ID = 'workspace_navigation';

/**
* Since every workspace always have overview and update page, these features will be selected by default
* and can't be changed in the workspace form feature selector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,8 @@ const overview = i18n.translate('workspace.detail.overview', {
});

function getOwners(currentWorkspace: WorkspaceAttributeWithPermission) {
if (currentWorkspace.permissions) {
const { groups = [], users = [] } = currentWorkspace.permissions.write;
return [...groups, ...users];
}
return [];
const { groups = [], users = [] } = currentWorkspace?.permissions?.write || {};
return [...groups, ...users];
}

interface WorkspaceDetailPanelProps {
Expand Down
22 changes: 19 additions & 3 deletions src/plugins/workspace/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { workspaceClientMock, WorkspaceClientMock } from './workspace_client.moc
import { WorkspacePlugin, WorkspacePluginStartDeps } from './plugin';
import { contentManagementPluginMocks } from '../../content_management/public';

// Expect 6 app registrations: create, fatal error, detail, initial, navigation, and list apps.
const registrationAppNumber = 6;

describe('Workspace plugin', () => {
const mockDependencies: WorkspacePluginStartDeps = {
contentManagement: contentManagementPluginMocks.createStartContract(),
Expand All @@ -40,7 +43,7 @@ describe('Workspace plugin', () => {
savedObjectsManagement: savedObjectManagementSetupMock,
management: managementPluginMock.createSetupContract(),
});
expect(setupMock.application.register).toBeCalledTimes(5);
expect(setupMock.application.register).toBeCalledTimes(registrationAppNumber);
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(savedObjectManagementSetupMock.columns.register).toBeCalledTimes(1);
});
Expand All @@ -53,7 +56,7 @@ describe('Workspace plugin', () => {
workspacePlugin.start(coreStart, mockDependencies);
coreStart.workspaces.currentWorkspaceId$.next('foo');
expect(coreStart.savedObjects.client.setCurrentWorkspace).toHaveBeenCalledWith('foo');
expect(setupMock.application.register).toBeCalledTimes(5);
expect(setupMock.application.register).toBeCalledTimes(registrationAppNumber);
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0);
});
Expand Down Expand Up @@ -90,7 +93,7 @@ describe('Workspace plugin', () => {
await workspacePlugin.setup(setupMock, {
management: managementPluginMock.createSetupContract(),
});
expect(setupMock.application.register).toBeCalledTimes(5);
expect(setupMock.application.register).toBeCalledTimes(registrationAppNumber);
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId');
expect(setupMock.getStartServices).toBeCalledTimes(2);
Expand Down Expand Up @@ -215,6 +218,19 @@ describe('Workspace plugin', () => {
);
});

it('#setup should register workspace navigation with a visible application', async () => {
const setupMock = coreMock.createSetup();
const workspacePlugin = new WorkspacePlugin();
await workspacePlugin.setup(setupMock, {});

expect(setupMock.application.register).toHaveBeenCalledWith(
expect.objectContaining({
id: 'workspace_navigation',
navLinkStatus: AppNavLinkStatus.hidden,
})
);
});

it('#start add workspace detail page to breadcrumbs when start', async () => {
const startMock = coreMock.createStart();
const workspaceObject = {
Expand Down
28 changes: 28 additions & 0 deletions src/plugins/workspace/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
WORKSPACE_LIST_APP_ID,
WORKSPACE_USE_CASES,
WORKSPACE_INITIAL_APP_ID,
WORKSPACE_NAVIGATION_APP_ID,
} from '../common/constants';
import { getWorkspaceIdFromUrl } from '../../../core/public/utils';
import { Services, WorkspaceUseCase } from './types';
Expand All @@ -45,6 +46,7 @@ import {
enrichBreadcrumbsWithWorkspace,
filterWorkspaceConfigurableApps,
getFirstUseCaseOfFeatureConfigs,
getUseCaseUrl,
isAppAccessibleInWorkspace,
isNavGroupInFeatureConfigs,
} from './utils';
Expand Down Expand Up @@ -374,6 +376,32 @@ export class WorkspacePlugin
workspaceAvailability: WorkspaceAvailability.outsideWorkspace,
});

const registeredUseCases$ = this.registeredUseCases$;
// register workspace navigation
core.application.register({
id: WORKSPACE_NAVIGATION_APP_ID,
title: '',
chromeless: true,
navLinkStatus: AppNavLinkStatus.hidden,
async mount() {
const [coreStart] = await core.getStartServices();
const { application, http, workspaces } = coreStart;
const workspace = workspaces.currentWorkspace$.getValue();
if (workspace) {
const availableUseCases = registeredUseCases$.getValue();
const currentUseCase = availableUseCases.find(
(useCase) => useCase.id === getFirstUseCaseOfFeatureConfigs(workspace?.features ?? [])
);
const useCaseUrl = getUseCaseUrl(currentUseCase, workspace, application, http);
application.navigateToUrl(useCaseUrl);
} else {
application.navigateToApp('home');
}
return () => {};
},
workspaceAvailability: WorkspaceAvailability.insideWorkspace,
});

// workspace list
core.application.register({
id: WORKSPACE_LIST_APP_ID,
Expand Down
145 changes: 137 additions & 8 deletions src/plugins/workspace/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
*/

import { OnPostAuthHandler, OnPreRoutingHandler } from 'src/core/server';
import { coreMock, httpServerMock } from '../../../core/server/mocks';
import { coreMock, httpServerMock, uiSettingsServiceMock } from '../../../core/server/mocks';
import { WorkspacePlugin } from './plugin';
import { getWorkspaceState, updateWorkspaceState } from '../../../core/server/utils';
import * as utilsExports from './utils';
import { SavedObjectsPermissionControl } from './permission_control/client';

describe('Workspace server plugin', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('#setup', async () => {
let value;
const capabilities = {} as any;
Expand Down Expand Up @@ -178,6 +182,24 @@ describe('Workspace server plugin', () => {

describe('#setUpRedirectPage', () => {
const setupMock = coreMock.createSetup();
const uiSettingsMock = uiSettingsServiceMock.createClient();
setupMock.getStartServices.mockResolvedValue([
{
...coreMock.createStart(),
uiSettings: {
asScopedToClient: () => ({
...uiSettingsMock,
get: jest.fn().mockImplementation((key) => {
if (key === 'home:useNewHomePage') {
return Promise.resolve(true);
}
}),
}),
},
},
{},
{},
]);
const initializerContextConfigMock = coreMock.createPluginInitializerContext({
enabled: true,
permission: {
Expand All @@ -192,7 +214,18 @@ describe('Workspace server plugin', () => {
const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock);
const response = httpServerMock.createResponseFactory();

it('with / request path', async () => {
it('without / request path', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/foo',
});
await workspacePlugin.setup(setupMock);
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(request, response, toolKitMock);
expect(toolKitMock.next).toBeCalledTimes(1);
});

it('with / request path and no workspaces', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/',
});
Expand All @@ -205,29 +238,125 @@ describe('Workspace server plugin', () => {
});
});

it('without / request path', async () => {
it('with / request path and one workspace', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/foo',
path: '/',
});
const workspaceSetup = await workspacePlugin.setup(setupMock);
const client = workspaceSetup.client;
jest.spyOn(client, 'list').mockResolvedValue({
success: true,
result: {
total: 1,
per_page: 100,
page: 1,
workspaces: [{ id: 'workspace-1', name: 'workspace-1' }],
},
});
await workspacePlugin.setup(setupMock);
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(request, response, toolKitMock);
expect(toolKitMock.next).toBeCalledTimes(1);
expect(response.redirected).toBeCalledWith({
headers: {
location: '/mock-server-basepath/w/workspace-1/app/workspace_navigation',
},
});
});

it('with / request path and more than one workspaces', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/',
});
const workspaceSetup = await workspacePlugin.setup(setupMock);
const client = workspaceSetup.client;
jest.spyOn(client, 'list').mockResolvedValue({
success: true,
result: {
total: 2,
per_page: 100,
page: 1,
workspaces: [
{ id: 'workspace-1', name: 'workspace-1' },
{ id: 'workspace-2', name: 'workspace-2' },
],
},
});
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(request, response, toolKitMock);
expect(response.redirected).toBeCalledWith({
headers: {
location: '/mock-server-basepath/app/home',
},
});
});

it('with more than one workspace', async () => {
it('with / request path and default workspace', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/',
});
setupMock.getStartServices.mockResolvedValue([
{
...coreMock.createStart(),
uiSettings: {
asScopedToClient: () => ({
...uiSettingsMock,
get: jest.fn().mockImplementation((key) => {
if (key === 'defaultWorkspace') {
return Promise.resolve('defaultWorkspace');
} else if (key === 'home:useNewHomePage') {
return Promise.resolve('true');
}
}),
}),
},
},
{},
{},
]);
const workspaceSetup = await workspacePlugin.setup(setupMock);
const client = workspaceSetup.client;
jest.spyOn(client, 'list').mockResolvedValue({
success: true,
result: { total: 1 },
result: {
total: 2,
per_page: 100,
page: 1,
workspaces: [
{ id: 'defaultWorkspace', name: 'default-workspace' },
{ id: 'workspace-2', name: 'workspace-2' },
],
},
});
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(request, response, toolKitMock);
expect(response.redirected).toBeCalledWith({
headers: {
location: '/mock-server-basepath/w/defaultWorkspace/app/workspace_navigation',
},
});
});

it('with / request path and home:useNewHomePage is false', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/',
});
setupMock.getStartServices.mockResolvedValue([
{
...coreMock.createStart(),
uiSettings: {
asScopedToClient: () => ({
...uiSettingsMock,
get: jest.fn().mockResolvedValue(false),
}),
},
},
{},
{},
]);
const toolKitMock = httpServerMock.createToolkit();

await registerOnPostAuthFn(request, response, toolKitMock);
expect(toolKitMock.next).toBeCalledTimes(1);
});
Expand Down
44 changes: 41 additions & 3 deletions src/plugins/workspace/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
WORKSPACE_UI_SETTINGS_CLIENT_WRAPPER_ID,
PRIORITY_FOR_WORKSPACE_UI_SETTINGS_WRAPPER,
WORKSPACE_INITIAL_APP_ID,
WORKSPACE_NAVIGATION_APP_ID,
} from '../common/constants';
import { IWorkspaceClientImpl, WorkspacePluginSetup, WorkspacePluginStart } from './types';
import { WorkspaceClient } from './workspace_client';
Expand Down Expand Up @@ -113,14 +114,51 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
core.http.registerOnPostAuth(async (request, response, toolkit) => {
const path = request.url.pathname;
if (path === '/') {
const [coreStart] = await core.getStartServices();
const uiSettings = coreStart.uiSettings.asScopedToClient(
coreStart.savedObjects.getScopedClient(request)
);
const useNewHomePage = await uiSettings.get('home:useNewHomePage');
if (!useNewHomePage) {
return toolkit.next();
}

const workspaceListResponse = await this.client?.list(
{ request, logger: this.logger },
{ page: 1, perPage: 1 }
{ page: 1, perPage: 100 }
);
const basePath = core.http.basePath.serverBasePath;

if (workspaceListResponse?.success && workspaceListResponse.result.total > 0) {
return toolkit.next();
const workspaceList = workspaceListResponse.result.workspaces;
// If user only has one workspace, go to overview page of that workspace
if (workspaceList.length === 1) {
return response.redirected({
headers: {
location: `${basePath}/w/${workspaceList[0].id}/app/${WORKSPACE_NAVIGATION_APP_ID}`,
},
});
}
// Temporarily use defaultWorkspace as a placeholder
const defaultWorkspaceId = await uiSettings.get('defaultWorkspace');
const defaultWorkspace = workspaceList.find(
(workspace) => workspace.id === defaultWorkspaceId
);
// If user has a default workspace configured, go to overview page of that workspace
// If user has more than one workspaces, go to homepage
if (defaultWorkspace) {
return response.redirected({
headers: {
location: `${basePath}/w/${defaultWorkspace.id}/app/${WORKSPACE_NAVIGATION_APP_ID}`,
},
});
} else {
return response.redirected({
headers: { location: `${basePath}/app/home` },
});
}
}
const basePath = core.http.basePath.serverBasePath;
// If user has no workspaces, go to initial page
return response.redirected({
headers: { location: `${basePath}/app/${WORKSPACE_INITIAL_APP_ID}` },
});
Expand Down

0 comments on commit ac5ed85

Please sign in to comment.