Skip to content

Commit

Permalink
allow user to turn on/off workspace from advance settings (opensearch…
Browse files Browse the repository at this point in the history
…-project#46)

return 404 if accessing a workspace path when workspace is disabled

---------

Signed-off-by: Yulong Ruan <ruanyl@amazon.com>
  • Loading branch information
ruanyl authored and SuZhou-Joe committed Jul 19, 2023
1 parent 0103ac6 commit 757982d
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 49 deletions.
4 changes: 2 additions & 2 deletions src/core/public/chrome/chrome_service.tsx
Expand Up @@ -182,7 +182,7 @@ export class ChromeService {
});

const getWorkspaceUrl = (id: string) => {
return workspaces?.formatUrlWithWorkspaceId(
return workspaces.formatUrlWithWorkspaceId(
application.getUrlForApp(WORKSPACE_APP_ID, {
path: '/',
absolute: true,
Expand All @@ -194,7 +194,7 @@ export class ChromeService {
const exitWorkspace = async () => {
let result;
try {
result = await workspaces?.client.exitWorkspace();
result = await workspaces.client.exitWorkspace();
} catch (error) {
notifications?.toasts.addDanger({
title: i18n.translate('workspace.exit.failed', {
Expand Down
2 changes: 0 additions & 2 deletions src/core/public/core_app/core_app.ts
Expand Up @@ -42,14 +42,12 @@ import type { IUiSettingsClient } from '../ui_settings';
import type { InjectedMetadataSetup } from '../injected_metadata';
import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors';
import { renderApp as renderStatusApp } from './status';
import { WorkspacesSetup } from '../workspace';

interface SetupDeps {
application: InternalApplicationSetup;
http: HttpSetup;
injectedMetadata: InjectedMetadataSetup;
notifications: NotificationsSetup;
workspaces: WorkspacesSetup;
}

interface StartDeps {
Expand Down
6 changes: 3 additions & 3 deletions src/core/public/core_system.ts
Expand Up @@ -163,14 +163,14 @@ export class CoreSystem {
const http = this.http.setup({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
const notifications = this.notifications.setup({ uiSettings });
const workspaces = await this.workspaces.setup({ http });
const workspaces = await this.workspaces.setup({ http, uiSettings });

const pluginDependencies = this.plugins.getOpaqueIds();
const context = this.context.setup({
pluginDependencies: new Map([...pluginDependencies]),
});
const application = this.application.setup({ context, http });
this.coreApp.setup({ application, http, injectedMetadata, notifications, workspaces });
this.coreApp.setup({ application, http, injectedMetadata, notifications });

const core: InternalCoreSetup = {
application,
Expand Down Expand Up @@ -204,7 +204,6 @@ export class CoreSystem {
const uiSettings = await this.uiSettings.start();
const docLinks = this.docLinks.start({ injectedMetadata });
const http = await this.http.start();
const workspaces = await this.workspaces.start();
const savedObjects = await this.savedObjects.start({ http });
const i18n = await this.i18n.start();
const fatalErrors = await this.fatalErrors.start();
Expand All @@ -226,6 +225,7 @@ export class CoreSystem {
targetDomElement: notificationsTargetDomElement,
});
const application = await this.application.start({ http, overlays });
const workspaces = await this.workspaces.start();
const chrome = await this.chrome.start({
application,
docLinks,
Expand Down
5 changes: 5 additions & 0 deletions src/core/public/http/types.ts
Expand Up @@ -97,6 +97,11 @@ export interface IBasePath {
*/
get: () => string;

/**
* Gets the `basePath
*/
getBasePath: () => string;

/**
* Prepends `path` with the basePath + workspace.
*/
Expand Down
9 changes: 5 additions & 4 deletions src/core/public/ui_settings/ui_settings_service.mock.ts
Expand Up @@ -33,7 +33,7 @@ import type { PublicMethodsOf } from '@osd/utility-types';
import { UiSettingsService } from './';
import { IUiSettingsClient } from './types';

const createSetupContractMock = () => {
const createUiSettingsClientMock = () => {
const setupContract: jest.Mocked<IUiSettingsClient> = {
getAll: jest.fn(),
get: jest.fn(),
Expand Down Expand Up @@ -66,12 +66,13 @@ const createMock = () => {
stop: jest.fn(),
};

mocked.setup.mockReturnValue(createSetupContractMock());
mocked.setup.mockReturnValue(createUiSettingsClientMock());
mocked.start.mockReturnValue(createUiSettingsClientMock());
return mocked;
};

export const uiSettingsServiceMock = {
create: createMock,
createSetupContract: createSetupContractMock,
createStartContract: createSetupContractMock,
createSetupContract: createUiSettingsClientMock,
createStartContract: createUiSettingsClientMock,
};
8 changes: 5 additions & 3 deletions src/core/public/workspace/workspaces_client.ts
Expand Up @@ -70,10 +70,12 @@ export class WorkspacesClient {
}
}
);
}

/**
* Initialize workspace list
*/
/**
* Initialize workspace list
*/
init() {
this.updateWorkspaceListAndNotify();
}

Expand Down
9 changes: 8 additions & 1 deletion src/core/public/workspace/workspaces_service.ts
Expand Up @@ -6,6 +6,7 @@ import { CoreService } from 'src/core/types';
import { WorkspacesClient, WorkspacesClientContract } from './workspaces_client';
import type { WorkspaceAttribute } from '../../server/types';
import { HttpSetup } from '../http';
import { IUiSettingsClient } from '../ui_settings';

/**
* @public
Expand All @@ -26,8 +27,14 @@ export class WorkspacesService implements CoreService<WorkspacesSetup, Workspace
private setFormatUrlWithWorkspaceId(formatFn: WorkspacesStart['formatUrlWithWorkspaceId']) {
this.formatUrlWithWorkspaceId = formatFn;
}
public async setup({ http }: { http: HttpSetup }) {
public async setup({ http, uiSettings }: { http: HttpSetup; uiSettings: IUiSettingsClient }) {
this.client = new WorkspacesClient(http);

// If workspace was disabled while opening a workspace url, navigate to basePath
if (uiSettings.get('workspace:enabled') === true) {
this.client.init();
}

return {
client: this.client,
formatUrlWithWorkspaceId: (url: string, id: string) => this.formatUrlWithWorkspaceId(url, id),
Expand Down
1 change: 1 addition & 0 deletions src/core/server/server.ts
Expand Up @@ -263,6 +263,7 @@ export class Server {
});
await this.workspaces.start({
savedObjects: savedObjectsStart,
uiSettings: uiSettingsStart,
});

this.coreStart = {
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/ui_settings/settings/index.test.ts
Expand Up @@ -36,6 +36,7 @@ import { getNotificationsSettings } from './notifications';
import { getThemeSettings } from './theme';
import { getCoreSettings } from './index';
import { getStateSettings } from './state';
import { getWorkspaceSettings } from './workspace';

describe('getCoreSettings', () => {
it('should not have setting overlaps', () => {
Expand All @@ -48,6 +49,7 @@ describe('getCoreSettings', () => {
getNotificationsSettings(),
getThemeSettings(),
getStateSettings(),
getWorkspaceSettings(),
].reduce((sum, settings) => sum + Object.keys(settings).length, 0);

expect(coreSettingsLength).toBe(summedLength);
Expand Down
2 changes: 2 additions & 0 deletions src/core/server/ui_settings/settings/index.ts
Expand Up @@ -36,6 +36,7 @@ import { getNavigationSettings } from './navigation';
import { getNotificationsSettings } from './notifications';
import { getThemeSettings } from './theme';
import { getStateSettings } from './state';
import { getWorkspaceSettings } from './workspace';

export const getCoreSettings = (): Record<string, UiSettingsParams> => {
return {
Expand All @@ -46,5 +47,6 @@ export const getCoreSettings = (): Record<string, UiSettingsParams> => {
...getNotificationsSettings(),
...getThemeSettings(),
...getStateSettings(),
...getWorkspaceSettings(),
};
};
25 changes: 25 additions & 0 deletions src/core/server/ui_settings/settings/workspace.ts
@@ -0,0 +1,25 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema } from '@osd/config-schema';
import { i18n } from '@osd/i18n';
import { UiSettingsParams } from '../../../types';

export const getWorkspaceSettings = (): Record<string, UiSettingsParams> => {
return {
'workspace:enabled': {
name: i18n.translate('core.ui_settings.params.workspace.enableWorkspaceTitle', {
defaultMessage: 'Enable Workspace',
}),
value: false,
requiresPageReload: true,
description: i18n.translate('core.ui_settings.params.workspace.enableWorkspaceTitle', {
defaultMessage: 'Enable or disable OpenSearch Dashboards Workspace',
}),
category: ['workspace'],
schema: schema.boolean(),
},
};
};
29 changes: 23 additions & 6 deletions src/core/server/workspaces/workspaces_service.ts
Expand Up @@ -15,6 +15,7 @@ import {
import { IWorkspaceDBImpl } from './types';
import { WorkspacesClientWithSavedObject } from './workspaces_client';
import { WorkspacePermissionControl } from './workspace_permission_control';
import { UiSettingsServiceStart } from '../ui_settings/types';

export interface WorkspacesServiceSetup {
client: IWorkspaceDBImpl;
Expand All @@ -37,30 +38,45 @@ export type InternalWorkspacesServiceStart = WorkspacesServiceStart;
/** @internal */
export interface WorkspacesStartDeps {
savedObjects: InternalSavedObjectsServiceStart;
uiSettings: UiSettingsServiceStart;
}

export class WorkspacesService
implements CoreService<WorkspacesServiceSetup, WorkspacesServiceStart> {
private logger: Logger;
private client?: IWorkspaceDBImpl;
private permissionControl?: WorkspacePermissionControl;

private startDeps?: WorkspacesStartDeps;
constructor(coreContext: CoreContext) {
this.logger = coreContext.logger.get('workspaces-service');
}

private proxyWorkspaceTrafficToRealHandler(setupDeps: WorkspacesSetupDeps) {
/**
* Proxy all {basePath}/w/{workspaceId}{osdPath*} paths to
* {basePath}{osdPath*}
* {basePath}{osdPath*} when workspace is enabled
*
* Return HTTP 404 if accessing {basePath}/w/{workspaceId} when workspace is disabled
*/
setupDeps.http.registerOnPreRouting((request, response, toolkit) => {
setupDeps.http.registerOnPreRouting(async (request, response, toolkit) => {
const regexp = /\/w\/([^\/]*)/;
const matchedResult = request.url.pathname.match(regexp);

if (matchedResult) {
const requestUrl = new URL(request.url.toString());
requestUrl.pathname = requestUrl.pathname.replace(regexp, '');
return toolkit.rewriteUrl(requestUrl.toString());
if (this.startDeps) {
const savedObjectsClient = this.startDeps.savedObjects.getScopedClient(request);
const uiSettingsClient = this.startDeps.uiSettings.asScopedToClient(savedObjectsClient);
const workspacesEnabled = await uiSettingsClient.get<boolean>('workspace:enabled');

if (workspacesEnabled) {
const requestUrl = new URL(request.url.toString());
requestUrl.pathname = requestUrl.pathname.replace(regexp, '');
return toolkit.rewriteUrl(requestUrl.toString());
} else {
// If workspace was disable, return HTTP 404
return response.notFound();
}
}
}
return toolkit.next();
});
Expand Down Expand Up @@ -90,6 +106,7 @@ export class WorkspacesService
}

public async start(deps: WorkspacesStartDeps): Promise<InternalWorkspacesServiceStart> {
this.startDeps = deps;
this.logger.debug('Starting SavedObjects service');

return {
Expand Down
Expand Up @@ -7,14 +7,15 @@ import React, { useState, useCallback, useMemo, useEffect } from 'react';

import { EuiButton, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import useObservable from 'react-use/lib/useObservable';
import { CoreStart, WorkspaceAttribute } from '../../../../../core/public';
import { ApplicationStart, WorkspaceAttribute, WorkspacesStart } from '../../../../../core/public';
import { WORKSPACE_APP_ID, PATHS } from '../../../common/constants';
import { switchWorkspace } from '../../components/utils/workspace';

type WorkspaceOption = EuiComboBoxOptionOption<WorkspaceAttribute>;

interface WorkspaceDropdownListProps {
coreStart: CoreStart;
workspaces: WorkspacesStart;
application: ApplicationStart;
}

function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption {
Expand All @@ -27,10 +28,8 @@ export function getErrorMessage(err: any) {
}

export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) {
const { coreStart } = props;

const workspaceList = useObservable(coreStart.workspaces.client.workspaceList$, []);
const currentWorkspace = useObservable(coreStart.workspaces.client.currentWorkspace$, null);
const workspaceList = useObservable(props.workspaces.client.workspaceList$, []);
const currentWorkspace = useObservable(props.workspaces.client.currentWorkspace$, null);

const [loading, setLoading] = useState(false);
const [workspaceOptions, setWorkspaceOptions] = useState([] as WorkspaceOption[]);
Expand Down Expand Up @@ -58,14 +57,14 @@ export function WorkspaceDropdownList(props: WorkspaceDropdownListProps) {
/** switch the workspace */
setLoading(true);
const id = workspaceOption[0].key!;
switchWorkspace(coreStart, id);
switchWorkspace({ workspaces: props.workspaces, application: props.application }, id);
setLoading(false);
},
[coreStart]
[props.application, props.workspaces]
);

const onCreateWorkspaceClick = () => {
coreStart.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create });
props.application.navigateToApp(WORKSPACE_APP_ID, { path: PATHS.create });
};

useEffect(() => {
Expand Down
19 changes: 15 additions & 4 deletions src/plugins/workspace/public/mount.tsx
Expand Up @@ -5,14 +5,25 @@

import React from 'react';
import ReactDOM from 'react-dom';
import { CoreStart } from '../../../core/public';
import { ApplicationStart, ChromeStart, WorkspacesStart } from '../../../core/public';
import { WorkspaceDropdownList } from './containers/workspace_dropdown_list';

export const mountDropdownList = (core: CoreStart) => {
core.chrome.navControls.registerLeft({
export const mountDropdownList = ({
application,
workspaces,
chrome,
}: {
application: ApplicationStart;
workspaces: WorkspacesStart;
chrome: ChromeStart;
}) => {
chrome.navControls.registerLeft({
order: 0,
mount: (element) => {
ReactDOM.render(<WorkspaceDropdownList coreStart={core} />, element);
ReactDOM.render(
<WorkspaceDropdownList workspaces={workspaces} application={application} />,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
Expand Down

0 comments on commit 757982d

Please sign in to comment.