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

[Backport 2.x] [Workspace] feat: Add workspace navigation for default route #7824

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -374,6 +376,32 @@
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();

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

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L387-L389

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L391 - L392 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 396 in src/plugins/workspace/public/plugin.ts

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L400 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}`,
},
});
}
// 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
Loading