-
Notifications
You must be signed in to change notification settings - Fork 936
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]Add create workspace page #6179
Changes from 3 commits
89bdd43
ef47225
16faa06
e561f87
a5c3743
e58cea3
f8dd0b8
7441b2c
fae3a34
e5f53f0
41e7201
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,13 @@ | |
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export const WORKSPACE_CREATE_APP_ID = 'workspace_create'; | ||
export const WORKSPACE_LIST_APP_ID = 'workspace_list'; | ||
export const WORKSPACE_UPDATE_APP_ID = 'workspace_update'; | ||
export const WORKSPACE_OVERVIEW_APP_ID = 'workspace_overview'; | ||
// These features will be checked and disabled in checkbox on default. | ||
export const DEFAULT_CHECKED_FEATURES_IDS = [WORKSPACE_UPDATE_APP_ID, WORKSPACE_OVERVIEW_APP_ID]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DEFAULT_CHECKED_FEATURES_IDS, I'm assuming that you want to mention here by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can rename to it. |
||
export const WORKSPACE_FATAL_ERROR_APP_ID = 'workspace_fatal_error'; | ||
export const WORKSPACE_SAVED_OBJECTS_CLIENT_WRAPPER_ID = 'workspace'; | ||
export const WORKSPACE_CONFLICT_CONTROL_SAVED_OBJECTS_CLIENT_WRAPPER_ID = | ||
'workspace_conflict_control'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,5 +7,5 @@ | |
"savedObjects" | ||
], | ||
"optionalPlugins": [], | ||
"requiredBundles": [] | ||
"requiredBundles": ["opensearchDashboardsReact"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import React from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { AppMountParameters } from '../../../core/public'; | ||
import { OpenSearchDashboardsContextProvider } from '../../opensearch_dashboards_react/public'; | ||
import { WorkspaceCreatorApp } from './components/workspace_creator_app'; | ||
import { Services } from './types'; | ||
|
||
export const renderCreatorApp = ({ element }: AppMountParameters, services: Services) => { | ||
ReactDOM.render( | ||
<OpenSearchDashboardsContextProvider services={services}> | ||
<WorkspaceCreatorApp /> | ||
</OpenSearchDashboardsContextProvider>, | ||
element | ||
); | ||
|
||
return () => { | ||
ReactDOM.unmountComponentAtNode(element); | ||
}; | ||
}; |
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'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
/* | ||
* 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, | ||
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 () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is testId There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. The test-id should be |
||
); | ||
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', | ||
}) | ||
); | ||
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']), | ||
}) | ||
); | ||
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(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* 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 handleWorkspaceFormSubmit = useCallback( | ||
async (data: WorkspaceFormSubmitData) => { | ||
let result; | ||
try { | ||
result = await workspaceClient.create(data); | ||
} catch (error) { | ||
notifications?.toasts.addDanger({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add tests for cover error handling, such as server errors or unexpected responses here. thanks |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
**/ | ||
style={{ width: '100%', maxWidth: 1000 }} | ||
> | ||
{application && ( | ||
<WorkspaceForm | ||
application={application} | ||
onSubmit={handleWorkspaceFormSubmit} | ||
operationType={WorkspaceOperationType.Create} | ||
/> | ||
)} | ||
</EuiPageContent> | ||
</EuiPageBody> | ||
</EuiPage> | ||
); | ||
}; |
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> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* | ||
* Copyright OpenSearch Contributors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export enum WorkspaceOperationType { | ||
Create = 'create', | ||
Update = 'update', | ||
} | ||
|
||
export enum WorkspaceFormTabs { | ||
NotSelected, | ||
FeatureVisibility, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give more detail about comments here? I'm a little bit lost here based on the comment here. thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The user needs to select which features will be enabled when creating or updating workspace. The enabled features will be list on the left navigation. The workspace overview and workspace update should be selected by default which means every workspace should have these two pages.