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 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
20 changes: 20 additions & 0 deletions src/plugins/workspace/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { WorkspaceCreatorProps } from './components/workspace_creator/workspace_
import { WorkspaceDetailApp } from './components/workspace_detail_app';
import { WorkspaceDetailProps } from './components/workspace_detail/workspace_detail';
import { WorkspaceInitialApp } from './components/workspace_initial_app';
import {
WorkspaceNavigationApp,
WorkspaceNavigationAppProps,
} from './components/workspace_navigation_app';

export const renderCreatorApp = (
{ element }: AppMountParameters,
Expand Down Expand Up @@ -102,3 +106,19 @@ export const renderInitialApp = ({}: AppMountParameters, services: Services) =>
ReactDOM.unmountComponentAtNode(rootElement!);
};
};

export const renderNavigationApp = (
{ element }: AppMountParameters,
services: Services,
props: WorkspaceNavigationAppProps
) => {
ReactDOM.render(
<OpenSearchDashboardsContextProvider services={services}>
<WorkspaceNavigationApp {...props} />
</OpenSearchDashboardsContextProvider>,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ export const WorkspaceInitial = () => {
const isDarkTheme = uiSettings.get('theme:darkMode');
const backGroundUrl = isDarkTheme ? BackgroundDarkSVG : BackgroundLightSVG;

const noAdminToolTip = i18n.translate('workspace.initial.card.createWorkspace.toolTip', {
defaultMessage:
'Contact your administrator to create a workspace or to be added to an existing one.',
});
const noAdminToolTip = (
<>
{i18n.translate('workspace.initial.card.createWorkspace.toolTip', {
defaultMessage:
'Contact your administrator to create a workspace or to be added to an existing one.',
})}
</>
);
yubonluo marked this conversation as resolved.
Show resolved Hide resolved

const createButton = (
<EuiSmallButton
Expand All @@ -69,7 +73,7 @@ export const WorkspaceInitial = () => {
defaultMessage: 'Create a workspace',
})}
description={
<EuiToolTip content={isDashboardAdmin || noAdminToolTip}>
<EuiToolTip content={isDashboardAdmin ? <></> : noAdminToolTip}>
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
<>
{i18n.translate('workspace.initial.card.createWorkspace.description', {
defaultMessage: 'Organize projects by use case in a collaborative workspace.',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { BehaviorSubject } from 'rxjs';
import { useObservable } from 'react-use';
import { useEffect } from 'react';
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public';
import { WorkspaceUseCase } from '../types';
import { getFirstUseCaseOfFeatureConfigs, getUseCaseUrl } from '../utils';
import { ApplicationStart, HttpSetup, WorkspaceObject } from '../../../../core/public';

export interface WorkspaceNavigationAppProps {
registeredUseCases$: BehaviorSubject<WorkspaceUseCase[]>;
}

const navigateToWorkspaceOverview = (
availableUseCases: WorkspaceUseCase[],
workspace: WorkspaceObject,
application: ApplicationStart,
http: HttpSetup
) => {
const currentUseCase = availableUseCases.find(
(useCase) => useCase.id === getFirstUseCaseOfFeatureConfigs(workspace?.features ?? [])
);
const useCaseUrl = getUseCaseUrl(currentUseCase, workspace, application, http);
application.navigateToUrl(useCaseUrl);
};

export const WorkspaceNavigationApp = (props: WorkspaceNavigationAppProps) => {
const {
services: { workspaces, application, http, uiSettings },
} = useOpenSearchDashboards();
const availableUseCases = useObservable(props.registeredUseCases$, []);

useEffect(() => {
if (!workspaces || !application || !http || !uiSettings) {
return;
}

const workspaceList = workspaces.workspaceList$.getValue();

// If user only has one workspace, go to overview page of that workspace
if (workspaceList.length === 1) {
const firstWorkspace = workspaceList[0];
navigateToWorkspaceOverview(availableUseCases, firstWorkspace, application, http);
} else {
// Temporarily use defaultWorkspace as a placeholder
const defaultWorkspaceId = uiSettings.get('defaultWorkspace', null);
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) {
navigateToWorkspaceOverview(availableUseCases, defaultWorkspace, application, http);
} else {
application.navigateToApp('home');
}
}
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
}, [workspaces, application, http, availableUseCases, uiSettings]);

return null;
};
19 changes: 16 additions & 3 deletions src/plugins/workspace/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('Workspace plugin', () => {
savedObjectsManagement: savedObjectManagementSetupMock,
management: managementPluginMock.createSetupContract(),
});
expect(setupMock.application.register).toBeCalledTimes(5);
expect(setupMock.application.register).toBeCalledTimes(6);
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(savedObjectManagementSetupMock.columns.register).toBeCalledTimes(1);
});
Expand All @@ -53,7 +53,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(6);
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(workspaceClientMock.enterWorkspace).toBeCalledTimes(0);
});
Expand Down Expand Up @@ -90,7 +90,7 @@ describe('Workspace plugin', () => {
await workspacePlugin.setup(setupMock, {
management: managementPluginMock.createSetupContract(),
});
expect(setupMock.application.register).toBeCalledTimes(5);
expect(setupMock.application.register).toBeCalledTimes(6);
expect(WorkspaceClientMock).toBeCalledTimes(1);
expect(workspaceClientMock.enterWorkspace).toBeCalledWith('workspaceId');
expect(setupMock.getStartServices).toBeCalledTimes(2);
Expand Down Expand Up @@ -215,6 +215,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
13 changes: 13 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 Down Expand Up @@ -374,6 +375,18 @@
workspaceAvailability: WorkspaceAvailability.outsideWorkspace,
});

// 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: '',
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
const { renderNavigationApp } = await import('./application');
yubonluo marked this conversation as resolved.
Show resolved Hide resolved
return mountWorkspaceApp(params, renderNavigationApp);

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

View check run for this annotation

Codecov / codecov/patch

src/plugins/workspace/public/plugin.ts#L384-L385

Added lines #L384 - L385 were not covered by tests
},
workspaceAvailability: WorkspaceAvailability.outsideWorkspace,
});

// workspace list
core.application.register({
id: WORKSPACE_LIST_APP_ID,
Expand Down
8 changes: 5 additions & 3 deletions src/plugins/workspace/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe('Workspace server plugin', () => {
const workspacePlugin = new WorkspacePlugin(initializerContextConfigMock);
const response = httpServerMock.createResponseFactory();

it('with / request path', async () => {
it('with / request path and no workspaces', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/',
});
Expand All @@ -216,7 +216,7 @@ describe('Workspace server plugin', () => {
expect(toolKitMock.next).toBeCalledTimes(1);
});

it('with more than one workspace', async () => {
it('with / request path and workspaces', async () => {
const request = httpServerMock.createOpenSearchDashboardsRequest({
path: '/',
});
Expand All @@ -229,7 +229,9 @@ describe('Workspace server plugin', () => {
const toolKitMock = httpServerMock.createToolkit();

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

Expand Down
11 changes: 8 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 @@ -115,12 +116,16 @@ export class WorkspacePlugin implements Plugin<WorkspacePluginSetup, WorkspacePl
if (path === '/') {
const workspaceListResponse = await this.client?.list(
{ request, logger: this.logger },
{ page: 1, perPage: 1 }
{ page: 1, perPage: 2 }
);
const basePath = core.http.basePath.serverBasePath;

if (workspaceListResponse?.success && workspaceListResponse.result.total > 0) {
return toolkit.next();
return response.redirected({
headers: { location: `${basePath}/app/${WORKSPACE_NAVIGATION_APP_ID}` },
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