forked from opensearch-project/OpenSearch-Dashboards
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add workspace create page (opensearch-project#284)
* Add workspace create page Signed-off-by: Lin Wang <[email protected]> * Address PR comments Signed-off-by: Lin Wang <[email protected]> * Add more comments Signed-off-by: Lin Wang <[email protected]> * Add example for dependencies field in App Signed-off-by: Lin Wang <[email protected]> * Separate workspace feature selector Signed-off-by: Lin Wang <[email protected]> * Correct example for dependencies Signed-off-by: Lin Wang <[email protected]> * Remove unclear icon and defaultVISTheme input Signed-off-by: Lin Wang <[email protected]> * Remove unclear dependencies feature Signed-off-by: Lin Wang <[email protected]> * Remove states and fix onChange fired after mount Signed-off-by: Lin Wang <[email protected]> --------- Signed-off-by: Lin Wang <[email protected]>
- Loading branch information
1 parent
93a6585
commit 529fc01
Showing
21 changed files
with
1,836 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
src/plugins/workspace/public/components/workspace_creator/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export { WorkspaceCreator } from './workspace_creator'; |
226 changes: 226 additions & 0 deletions
226
src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React from 'react'; | ||
import { PublicAppInfo } from 'opensearch-dashboards/public'; | ||
import { fireEvent, render, waitFor } from '@testing-library/react'; | ||
import { BehaviorSubject } from 'rxjs'; | ||
import { WorkspaceCreator as WorkspaceCreatorComponent } from './workspace_creator'; | ||
import { coreMock } from '../../../../../core/public/mocks'; | ||
import { createOpenSearchDashboardsReactContext } from '../../../../opensearch_dashboards_react/public'; | ||
|
||
const workspaceClientCreate = jest | ||
.fn() | ||
.mockReturnValue({ result: { id: 'successResult' }, 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 mockCoreStart = coreMock.createStart(); | ||
|
||
const WorkspaceCreator = (props: any) => { | ||
const { Provider } = createOpenSearchDashboardsReactContext({ | ||
...mockCoreStart, | ||
...{ | ||
application: { | ||
...mockCoreStart.application, | ||
capabilities: { | ||
...mockCoreStart.application.capabilities, | ||
workspaces: { | ||
permissionEnabled: true, | ||
}, | ||
}, | ||
navigateToApp, | ||
getUrlForApp: jest.fn(), | ||
applications$: new BehaviorSubject<Map<string, PublicAppInfo>>(PublicAPPInfoMap as any), | ||
}, | ||
notifications: { | ||
...mockCoreStart.notifications, | ||
toasts: { | ||
...mockCoreStart.notifications.toasts, | ||
addDanger: notificationToastsAddDanger, | ||
addSuccess: notificationToastsAddSuccess, | ||
}, | ||
}, | ||
workspaceClient: { | ||
...mockCoreStart.workspaces, | ||
create: workspaceClientCreate, | ||
}, | ||
}, | ||
}); | ||
|
||
return ( | ||
<Provider> | ||
<WorkspaceCreatorComponent {...props} /> | ||
</Provider> | ||
); | ||
}; | ||
|
||
function clearMockedFunctions() { | ||
workspaceClientCreate.mockClear(); | ||
notificationToastsAddDanger.mockClear(); | ||
notificationToastsAddSuccess.mockClear(); | ||
} | ||
|
||
describe('WorkspaceCreator', () => { | ||
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 create workspace when name empty', async () => { | ||
const { getByTestId } = render(<WorkspaceCreator />); | ||
fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); | ||
expect(workspaceClientCreate).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('cannot create workspace with invalid name', async () => { | ||
const { getByTestId } = render(<WorkspaceCreator />); | ||
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); | ||
fireEvent.input(nameInput, { | ||
target: { value: '~' }, | ||
}); | ||
expect(workspaceClientCreate).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('cannot create workspace with invalid description', async () => { | ||
const { getByTestId } = render(<WorkspaceCreator />); | ||
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(workspaceClientCreate).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('cancel create workspace', async () => { | ||
const { findByText, getByTestId } = render(<WorkspaceCreator />); | ||
fireEvent.click(getByTestId('workspaceForm-bottomBar-cancelButton')); | ||
await findByText('Discard changes?'); | ||
fireEvent.click(getByTestId('confirmModalConfirmButton')); | ||
expect(navigateToApp).toHaveBeenCalled(); | ||
}); | ||
|
||
it('create workspace with detailed information', async () => { | ||
const { getByTestId } = render(<WorkspaceCreator />); | ||
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-bottomBar-createButton')); | ||
expect(workspaceClientCreate).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
name: 'test workspace name', | ||
color: '#000000', | ||
description: 'test workspace description', | ||
}), | ||
expect.any(Array) | ||
); | ||
await waitFor(() => { | ||
expect(notificationToastsAddSuccess).toHaveBeenCalled(); | ||
}); | ||
expect(notificationToastsAddDanger).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('create workspace with customized features', async () => { | ||
const { getByTestId } = render(<WorkspaceCreator />); | ||
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); | ||
fireEvent.input(nameInput, { | ||
target: { value: 'test workspace name' }, | ||
}); | ||
fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-app1')); | ||
fireEvent.click(getByTestId('workspaceForm-workspaceFeatureVisibility-category1')); | ||
fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); | ||
expect(workspaceClientCreate).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
name: 'test workspace name', | ||
features: expect.arrayContaining(['app1', 'app2', 'app3']), | ||
}), | ||
expect.any(Array) | ||
); | ||
await waitFor(() => { | ||
expect(notificationToastsAddSuccess).toHaveBeenCalled(); | ||
}); | ||
expect(notificationToastsAddDanger).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('create workspace with customized permissions', async () => { | ||
const { getByTestId, getByText } = render(<WorkspaceCreator />); | ||
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); | ||
fireEvent.input(nameInput, { | ||
target: { value: 'test workspace name' }, | ||
}); | ||
fireEvent.click(getByText('Users & Permissions')); | ||
fireEvent.click(getByTestId('workspaceForm-permissionSettingPanel-user-addNew')); | ||
const userIdInput = getByTestId('workspaceForm-permissionSettingPanel-0-userId'); | ||
fireEvent.click(userIdInput); | ||
fireEvent.input(getByTestId('comboBoxSearchInput'), { | ||
target: { value: 'test user id' }, | ||
}); | ||
fireEvent.blur(getByTestId('comboBoxSearchInput')); | ||
fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); | ||
expect(workspaceClientCreate).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
name: 'test workspace name', | ||
}), | ||
expect.arrayContaining([expect.objectContaining({ type: 'user', userId: 'test user id' })]) | ||
); | ||
await waitFor(() => { | ||
expect(notificationToastsAddSuccess).toHaveBeenCalled(); | ||
}); | ||
expect(notificationToastsAddDanger).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should show danger toasts after create workspace failed', async () => { | ||
workspaceClientCreate.mockReturnValue({ result: { id: 'failResult' }, success: false }); | ||
const { getByTestId } = render(<WorkspaceCreator />); | ||
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); | ||
fireEvent.input(nameInput, { | ||
target: { value: 'test workspace name' }, | ||
}); | ||
fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); | ||
expect(workspaceClientCreate).toHaveBeenCalled(); | ||
await waitFor(() => { | ||
expect(notificationToastsAddDanger).toHaveBeenCalled(); | ||
}); | ||
expect(notificationToastsAddSuccess).not.toHaveBeenCalled(); | ||
}); | ||
}); |
97 changes: 97 additions & 0 deletions
97
src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React, { useCallback } from 'react'; | ||
import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent, EuiSpacer } from '@elastic/eui'; | ||
import { i18n } from '@osd/i18n'; | ||
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; | ||
import { WorkspaceForm, WorkspaceFormSubmitData, WorkspaceOperationType } from '../workspace_form'; | ||
import { WORKSPACE_OVERVIEW_APP_ID } from '../../../common/constants'; | ||
import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils'; | ||
import { WorkspaceClient } from '../../workspace_client'; | ||
|
||
export const WorkspaceCreator = () => { | ||
const { | ||
services: { application, notifications, http, workspaceClient }, | ||
} = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>(); | ||
|
||
const isPermissionEnabled = application?.capabilities.workspaces.permissionEnabled; | ||
const handleWorkspaceFormSubmit = useCallback( | ||
async (data: WorkspaceFormSubmitData) => { | ||
let result; | ||
try { | ||
const { permissions, ...attributes } = data; | ||
result = await workspaceClient.create(attributes, permissions); | ||
} catch (error) { | ||
notifications?.toasts.addDanger({ | ||
title: i18n.translate('workspace.create.failed', { | ||
defaultMessage: 'Failed to create workspace', | ||
}), | ||
text: error instanceof Error ? error.message : JSON.stringify(error), | ||
}); | ||
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] | ||
); | ||
|
||
return ( | ||
<EuiPage paddingSize="none"> | ||
<EuiPageBody> | ||
<EuiPageHeader restrictWidth pageTitle="Create Workspace" /> | ||
<EuiSpacer /> | ||
<EuiPageContent | ||
verticalPosition="center" | ||
horizontalPosition="center" | ||
paddingSize="none" | ||
color="subdued" | ||
hasShadow={false} | ||
/** | ||
* Since above EuiPageHeader has a maxWidth: 1000 style, | ||
* add maxWidth: 100 below to align with the above page header | ||
**/ | ||
style={{ width: '100%', maxWidth: 1000 }} | ||
> | ||
{application && ( | ||
<WorkspaceForm | ||
application={application} | ||
onSubmit={handleWorkspaceFormSubmit} | ||
operationType={WorkspaceOperationType.Create} | ||
permissionEnabled={isPermissionEnabled} | ||
permissionLastAdminItemDeletable | ||
/> | ||
)} | ||
</EuiPageContent> | ||
</EuiPageBody> | ||
</EuiPage> | ||
); | ||
}; |
35 changes: 35 additions & 0 deletions
35
src/plugins/workspace/public/components/workspace_creator_app.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React, { useEffect } from 'react'; | ||
import { I18nProvider } from '@osd/i18n/react'; | ||
import { i18n } from '@osd/i18n'; | ||
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; | ||
import { WorkspaceCreator } from './workspace_creator'; | ||
|
||
export const WorkspaceCreatorApp = () => { | ||
const { | ||
services: { chrome }, | ||
} = useOpenSearchDashboards(); | ||
|
||
/** | ||
* set breadcrumbs to chrome | ||
*/ | ||
useEffect(() => { | ||
chrome?.setBreadcrumbs([ | ||
{ | ||
text: i18n.translate('workspace.workspaceCreateTitle', { | ||
defaultMessage: 'Create workspace', | ||
}), | ||
}, | ||
]); | ||
}, [chrome]); | ||
|
||
return ( | ||
<I18nProvider> | ||
<WorkspaceCreator /> | ||
</I18nProvider> | ||
); | ||
}; |
Oops, something went wrong.