Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Workspace] feat: Add workspace navigation for default route #7785

Merged
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 @@
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 @@
enrichBreadcrumbsWithWorkspace,
filterWorkspaceConfigurableApps,
getFirstUseCaseOfFeatureConfigs,
getUseCaseUrl,
isAppAccessibleInWorkspace,
isNavGroupInFeatureConfigs,
} from './utils';
Expand Down Expand Up @@ -375,6 +377,32 @@
workspaceAvailability: WorkspaceAvailability.outsideWorkspace,
});

const registeredUseCases$ = this.registeredUseCases$;
// register workspace navigation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have a more context about this change

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();

Check warning on line 390 in src/plugins/workspace/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L388-L390

Added lines #L388 - L390 were not covered by tests
if (workspace) {
const availableUseCases = registeredUseCases$.getValue();
const currentUseCase = availableUseCases.find(

Check warning on line 393 in src/plugins/workspace/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L392-L393

Added lines #L392 - L393 were not covered by tests
(useCase) => useCase.id === getFirstUseCaseOfFeatureConfigs(workspace?.features ?? [])
);
const useCaseUrl = getUseCaseUrl(currentUseCase, workspace, application, http);
application.navigateToUrl(useCaseUrl);

Check warning on line 397 in src/plugins/workspace/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L396-L397

Added lines #L396 - L397 were not covered by tests
} else {
application.navigateToApp('home');

Check warning on line 399 in src/plugins/workspace/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L399

Added line #L399 was not covered by tests
}
return () => {};

Check warning on line 401 in src/plugins/workspace/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L401

Added line #L401 was not covered by tests
},
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}`,
SuZhou-Joe marked this conversation as resolved.
Show resolved Hide resolved
},
});
}
// 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` },
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
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
Loading