Skip to content

Commit

Permalink
[Workspace] Add update workspace page (#6270) (#6550)
Browse files Browse the repository at this point in the history
* Add update workspace page

Signed-off-by: gaobinlong <[email protected]>

* Modify change log

Signed-off-by: gaobinlong <[email protected]>

* Increase test coverage

Signed-off-by: gaobinlong <[email protected]>

* Add a new test case

Signed-off-by: gaobinlong <[email protected]>

* Optimize some code

Signed-off-by: gaobinlong <[email protected]>

---------

Signed-off-by: gaobinlong <[email protected]>
(cherry picked from commit 05a29a8)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 536d48a commit 907593e
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 31 deletions.
14 changes: 14 additions & 0 deletions src/plugins/workspace/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AppMountParameters, ScopedHistory } from '../../../core/public';
import { OpenSearchDashboardsContextProvider } from '../../opensearch_dashboards_react/public';
import { WorkspaceFatalError } from './components/workspace_fatal_error';
import { WorkspaceCreatorApp } from './components/workspace_creator_app';
import { WorkspaceUpdaterApp } from './components/workspace_updater_app';
import { WorkspaceListApp } from './components/workspace_list_app';
import { Services } from './types';

Expand All @@ -25,6 +26,19 @@ export const renderCreatorApp = ({ element }: AppMountParameters, services: Serv
};
};

export const renderUpdaterApp = ({ element }: AppMountParameters, services: Services) => {
ReactDOM.render(
<OpenSearchDashboardsContextProvider services={services}>
<WorkspaceUpdaterApp />
</OpenSearchDashboardsContextProvider>,
element
);

return () => {
ReactDOM.unmountComponentAtNode(element);
};
};

export const renderFatalErrorApp = (params: AppMountParameters, services: Services) => {
const { element } = params;
const history = params.history as ScopedHistory<{ error?: string }>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,29 @@ export const WorkspaceCreator = () => {
let result;
try {
result = await workspaceClient.create(data);
if (result?.success) {
notifications?.toasts.addSuccess({
title: i18n.translate('workspace.create.success', {
defaultMessage: 'Create workspace successfully',
}),
});
if (application && http) {
const newWorkspaceId = result.result.id;
// Redirect page after one second, leave one second time to show create successful toast.
window.setTimeout(() => {
window.location.href = formatUrlWithWorkspaceId(
application.getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, {
absolute: true,
}),
newWorkspaceId,
http.basePath
);
}, 1000);
}
return;
} else {
throw new Error(result?.error ? result?.error : 'create workspace failed');
}
} catch (error) {
notifications?.toasts.addDanger({
title: i18n.translate('workspace.create.failed', {
Expand All @@ -31,33 +54,6 @@ export const WorkspaceCreator = () => {
});
return;
}
if (result?.success) {
notifications?.toasts.addSuccess({
title: i18n.translate('workspace.create.success', {
defaultMessage: 'Create workspace successfully',
}),
});
if (application && http) {
const newWorkspaceId = result.result.id;
// Redirect page after one second, leave one second time to show create successful toast.
window.setTimeout(() => {
window.location.href = formatUrlWithWorkspaceId(
application.getUrlForApp(WORKSPACE_OVERVIEW_APP_ID, {
absolute: true,
}),
newWorkspaceId,
http.basePath
);
}, 1000);
}
return;
}
notifications?.toasts.addDanger({
title: i18n.translate('workspace.create.failed', {
defaultMessage: 'Failed to create workspace',
}),
text: result?.error,
});
},
[notifications?.toasts, http, application, workspaceClient]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,13 @@ export const WorkspaceBottomBar = ({
</EuiButton>
)}
{operationType === WorkspaceOperationType.Update && (
<EuiButton form={formId} type="submit" fill color="primary">
<EuiButton
form={formId}
type="submit"
fill
color="primary"
data-test-subj="workspaceForm-bottomBar-updateButton"
>
{i18n.translate('workspace.form.bottomBar.saveChanges', {
defaultMessage: 'Save changes',
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { WorkspaceUpdater } from './workspace_updater';
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { PublicAppInfo, WorkspaceObject } from 'opensearch-dashboards/public';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { BehaviorSubject } from 'rxjs';
import { WorkspaceUpdater as WorkspaceCreatorComponent } from './workspace_updater';
import { coreMock, workspacesServiceMock } from '../../../../../core/public/mocks';
import { createOpenSearchDashboardsReactContext } from '../../../../opensearch_dashboards_react/public';

const workspaceClientUpdate = jest.fn().mockReturnValue({ result: true, success: true });

const navigateToApp = jest.fn();
const notificationToastsAddSuccess = jest.fn();
const notificationToastsAddDanger = jest.fn();
const PublicAPPInfoMap = new Map([
['app1', { id: 'app1', title: 'app1' }],
['app2', { id: 'app2', title: 'app2', category: { id: 'category1', label: 'category1' } }],
['app3', { id: 'app3', category: { id: 'category1', label: 'category1' } }],
['app4', { id: 'app4', category: { id: 'category2', label: 'category2' } }],
['app5', { id: 'app5', category: { id: 'category2', label: 'category2' } }],
]);
const createWorkspacesSetupContractMockWithValue = () => {
const currentWorkspaceId$ = new BehaviorSubject<string>('abljlsds');
const currentWorkspace: WorkspaceObject = {
id: 'abljlsds',
name: 'test1',
description: 'test1',
features: [],
color: '',
icon: '',
reserved: false,
};
const workspaceList$ = new BehaviorSubject<WorkspaceObject[]>([currentWorkspace]);
const currentWorkspace$ = new BehaviorSubject<WorkspaceObject | null>(currentWorkspace);
const initialized$ = new BehaviorSubject<boolean>(false);
return {
currentWorkspaceId$,
workspaceList$,
currentWorkspace$,
initialized$,
};
};

const mockCoreStart = coreMock.createStart();

const WorkspaceUpdater = (props: any) => {
const workspacesService = props.workspacesService || createWorkspacesSetupContractMockWithValue();
const { Provider } = createOpenSearchDashboardsReactContext({
...mockCoreStart,
...{
application: {
...mockCoreStart.application,
capabilities: {
...mockCoreStart.application.capabilities,
},
navigateToApp,
getUrlForApp: jest.fn(() => '/app/workspace_overview'),
applications$: new BehaviorSubject<Map<string, PublicAppInfo>>(PublicAPPInfoMap as any),
},
workspaces: workspacesService,
notifications: {
...mockCoreStart.notifications,
toasts: {
...mockCoreStart.notifications.toasts,
addDanger: notificationToastsAddDanger,
addSuccess: notificationToastsAddSuccess,
},
},
workspaceClient: {
...mockCoreStart.workspaces,
update: workspaceClientUpdate,
},
},
});

return (
<Provider>
<WorkspaceCreatorComponent {...props} />
</Provider>
);
};

function clearMockedFunctions() {
workspaceClientUpdate.mockClear();
notificationToastsAddDanger.mockClear();
notificationToastsAddSuccess.mockClear();
}

describe('WorkspaceUpdater', () => {
beforeEach(() => clearMockedFunctions());
const { location } = window;
const setHrefSpy = jest.fn((href) => href);

beforeAll(() => {
if (window.location) {
// @ts-ignore
delete window.location;
}
window.location = {} as Location;
Object.defineProperty(window.location, 'href', {
get: () => 'http://localhost/',
set: setHrefSpy,
});
});

afterAll(() => {
window.location = location;
});

it('cannot render when the name of the current workspace is empty', async () => {
const mockedWorkspacesService = workspacesServiceMock.createSetupContract();
const { container } = render(<WorkspaceUpdater workspacesService={mockedWorkspacesService} />);
expect(container).toMatchInlineSnapshot(`<div />`);
});

it('cannot update workspace with invalid name', async () => {
const { getByTestId } = render(<WorkspaceUpdater />);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: '~' },
});
expect(workspaceClientUpdate).not.toHaveBeenCalled();
});

it('cannot update workspace with invalid description', async () => {
const { getByTestId } = render(<WorkspaceUpdater />);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});
const descriptionInput = getByTestId('workspaceForm-workspaceDetails-descriptionInputText');
fireEvent.input(descriptionInput, {
target: { value: '~' },
});
expect(workspaceClientUpdate).not.toHaveBeenCalled();
});

it('cancel update workspace', async () => {
const { findByText, getByTestId } = render(<WorkspaceUpdater />);
fireEvent.click(getByTestId('workspaceForm-bottomBar-cancelButton'));
await findByText('Discard changes?');
fireEvent.click(getByTestId('confirmModalConfirmButton'));
expect(navigateToApp).toHaveBeenCalled();
});

it('update workspace successfully', async () => {
const { getByTestId } = render(<WorkspaceUpdater />);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});

const descriptionInput = getByTestId('workspaceForm-workspaceDetails-descriptionInputText');
fireEvent.input(descriptionInput, {
target: { value: 'test workspace description' },
});

const colorSelector = getByTestId(
'euiColorPickerAnchor workspaceForm-workspaceDetails-colorPicker'
);
fireEvent.input(colorSelector, {
target: { value: '#000000' },
});

fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-app1'));
fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-category1'));

fireEvent.click(getByTestId('workspaceForm-bottomBar-updateButton'));
expect(workspaceClientUpdate).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
name: 'test workspace name',
color: '#000000',
description: 'test workspace description',
features: expect.arrayContaining(['app1', 'app2', 'app3']),
})
);
await waitFor(() => {
expect(notificationToastsAddSuccess).toHaveBeenCalled();
});
expect(notificationToastsAddDanger).not.toHaveBeenCalled();
await waitFor(() => {
expect(setHrefSpy).toHaveBeenCalledWith(expect.stringMatching(/workspace_overview$/));
});
});

it('should show danger toasts after update workspace failed', async () => {
workspaceClientUpdate.mockReturnValue({ result: false, success: false });
const { getByTestId } = render(<WorkspaceUpdater />);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});
fireEvent.click(getByTestId('workspaceForm-bottomBar-updateButton'));
expect(workspaceClientUpdate).toHaveBeenCalled();
await waitFor(() => {
expect(notificationToastsAddDanger).toHaveBeenCalled();
});
expect(notificationToastsAddSuccess).not.toHaveBeenCalled();
});

it('should show danger toasts after update workspace threw error', async () => {
workspaceClientUpdate.mockImplementation(() => {
throw new Error('update workspace failed');
});
const { getByTestId } = render(<WorkspaceUpdater />);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});
fireEvent.click(getByTestId('workspaceForm-bottomBar-updateButton'));
expect(workspaceClientUpdate).toHaveBeenCalled();
await waitFor(() => {
expect(notificationToastsAddDanger).toHaveBeenCalled();
});
expect(notificationToastsAddSuccess).not.toHaveBeenCalled();
});

it('should show danger toasts when currentWorkspace is missing after click update button', async () => {
const mockedWorkspacesService = workspacesServiceMock.createSetupContract();
const { getByTestId } = render(<WorkspaceUpdater workspaceService={mockedWorkspacesService} />);

const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});
fireEvent.click(getByTestId('workspaceForm-bottomBar-updateButton'));
mockedWorkspacesService.currentWorkspace$ = new BehaviorSubject<WorkspaceObject | null>(null);
expect(workspaceClientUpdate).toHaveBeenCalled();
await waitFor(() => {
expect(notificationToastsAddDanger).toHaveBeenCalled();
});
expect(notificationToastsAddSuccess).not.toHaveBeenCalled();
});
});
Loading

0 comments on commit 907593e

Please sign in to comment.