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

[Backport 2.x] [Workspace]Add right sidebar to workspace create form #7931

Merged
merged 1 commit into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/7750.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- [Workspace]Add right sidebar to workspace create form ([#7750](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7750))

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export const RIGHT_SIDEBAR_SCROLL_KEY = 'data-right-sidebar-scroll';

export enum RightSidebarScrollField {
Name = 'name',
Description = 'description',
Color = 'color',
UseCase = 'useCase',
DataSource = 'dataSource',
Member = 'member',
}

export const generateRightSidebarScrollProps = (key: RightSidebarScrollField) => {
return { [RIGHT_SIDEBAR_SCROLL_KEY]: key };
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('WorkspaceCreateActionPanel', () => {
formId={formId}
formData={{ name: longName, description: formData.description }}
application={mockApplication}
isSubmitting={false}
/>
);
const createButton = screen.getByText('Create workspace');
Expand All @@ -40,6 +41,7 @@ describe('WorkspaceCreateActionPanel', () => {
formId={formId}
formData={{ name: formData.name, description: longDescription }}
application={mockApplication}
isSubmitting={false}
/>
);
const createButton = screen.getByText('Create workspace');
Expand All @@ -52,9 +54,23 @@ describe('WorkspaceCreateActionPanel', () => {
formId={formId}
formData={formData}
application={mockApplication}
isSubmitting={false}
/>
);
const createButton = screen.getByText('Create workspace');
expect(createButton.closest('button')).not.toBeDisabled();
});

it('should disable the "Create Workspace" and "Cancel" button when submitting', () => {
render(
<WorkspaceCreateActionPanel
formId={formId}
formData={{ name: 'test' }}
application={mockApplication}
isSubmitting
/>
);
expect(screen.getByText('Create workspace').closest('button')).toBeDisabled();
expect(screen.getByText('Cancel').closest('button')).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { EuiSmallButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiSmallButton, EuiFlexGroup, EuiFlexItem, EuiSmallButtonEmpty } from '@elastic/eui';
import { i18n } from '@osd/i18n';
import React, { useState, useCallback } from 'react';
import type { ApplicationStart } from 'opensearch-dashboards/public';
import type { WorkspaceFormData } from './types';
import { WorkspaceCancelModal } from './workspace_cancel_modal';
import { WorkspaceFormDataState, WorkspaceCancelModal } from '../workspace_form';
import {
MAX_WORKSPACE_DESCRIPTION_LENGTH,
MAX_WORKSPACE_NAME_LENGTH,
} from '../../../common/constants';

interface WorkspaceCreateActionPanelProps {
formId: string;
formData: Partial<Pick<WorkspaceFormData, 'name' | 'description'>>;
formData: Pick<WorkspaceFormDataState, 'name' | 'description'>;
application: ApplicationStart;
isSubmitting: boolean;
}

export const WorkspaceCreateActionPanel = ({
formId,
formData,
application,
isSubmitting,
}: WorkspaceCreateActionPanelProps) => {
const [isCancelModalVisible, setIsCancelModalVisible] = useState(false);
const closeCancelModal = useCallback(() => setIsCancelModalVisible(false), []);
Expand All @@ -34,26 +35,28 @@ export const WorkspaceCreateActionPanel = ({

return (
<>
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiSmallButton
<EuiSmallButtonEmpty
data-test-subj="workspaceForm-bottomBar-cancelButton"
onClick={showCancelModal}
disabled={isSubmitting}
>
{i18n.translate('workspace.form.bottomBar.cancel', {
{i18n.translate('workspace.form.right.sidebar.buttons.cancelText', {
defaultMessage: 'Cancel',
})}
</EuiSmallButton>
</EuiSmallButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiSmallButton
fill
type="submit"
form={formId}
data-test-subj="workspaceForm-bottomBar-createButton"
disabled={createButtonDisabled}
fill
disabled={createButtonDisabled || isSubmitting}
isLoading={isSubmitting}
>
{i18n.translate('workspace.form.bottomBar.createWorkspace', {
{i18n.translate('workspace.form.right.sidebar.buttons.createWorkspaceText', {
defaultMessage: 'Create workspace',
})}
</EuiSmallButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,4 +344,29 @@ describe('WorkspaceCreator', () => {
});
expect(notificationToastsAddDanger).not.toHaveBeenCalled();
});

it('should not create workspace API when submitting', async () => {
workspaceClientCreate.mockImplementationOnce(
() =>
new Promise((resolve) => {
setTimeout(resolve, 100);
})
);
const { getByTestId } = render(<WorkspaceCreator />);
// Ensure workspace create form rendered
await waitFor(() => {
expect(getByTestId('workspaceForm-bottomBar-createButton')).toBeInTheDocument();
});
fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton'));
expect(workspaceClientCreate).toHaveBeenCalledTimes(1);

// Since create button was been disabled, fire form submit event by form directly
fireEvent.submit(getByTestId('workspaceCreatorForm'));
expect(workspaceClientCreate).toHaveBeenCalledTimes(1);

await waitFor(() => {
fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton'));
expect(workspaceClientCreate).toHaveBeenCalledTimes(2);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, euiPaletteColorBlind } from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { BehaviorSubject } from 'rxjs';

import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { WorkspaceForm, WorkspaceFormSubmitData, WorkspaceOperationType } from '../workspace_form';
import { WorkspaceFormSubmitData, WorkspaceOperationType } from '../workspace_form';
import { WORKSPACE_DETAIL_APP_ID } from '../../../common/constants';
import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
import { WorkspaceClient } from '../../workspace_client';
import { convertPermissionSettingsToPermissions } from '../workspace_form';
import { DataSource } from '../../../common/types';
import { DataSourceManagementPluginSetup } from '../../../../../plugins/data_source_management/public';
import { WorkspaceUseCase } from '../../types';
import { WorkspaceFormData } from '../workspace_form/types';
import { getUseCaseFeatureConfig } from '../../utils';
import { useFormAvailableUseCases } from '../workspace_form/use_form_available_use_cases';
import { NavigationPublicPluginStart } from '../../../../../plugins/navigation/public';
import { WorkspaceCreatorForm } from './workspace_creator_form';

export interface WorkspaceCreatorProps {
registeredUseCases$: BehaviorSubject<WorkspaceUseCase[]>;
Expand All @@ -43,6 +43,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => {
dataSourceManagement?: DataSourceManagementPluginSetup;
navigationUI: NavigationPublicPluginStart['ui'];
}>();
const [isFormSubmitting, setIsFormSubmitting] = useState(false);

const isPermissionEnabled = application?.capabilities.workspaces.permissionEnabled;
const { isOnlyAllowEssential, availableUseCases } = useFormAvailableUseCases({
Expand All @@ -52,7 +53,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => {
});

const defaultSelectedUseCase = availableUseCases?.[0];
const defaultWorkspaceFormValues: Partial<WorkspaceFormData> = {
const defaultWorkspaceFormValues: Partial<WorkspaceFormSubmitData> = {
color: euiPaletteColorBlind()[0],
...(defaultSelectedUseCase
? {
Expand All @@ -65,6 +66,10 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => {
const handleWorkspaceFormSubmit = useCallback(
async (data: WorkspaceFormSubmitData) => {
let result;
if (isFormSubmitting) {
return;
}
setIsFormSubmitting(true);
try {
const { permissionSettings, selectedDataSources, ...attributes } = data;
const selectedDataSourceIds = (selectedDataSources ?? []).map((ds: DataSource) => {
Expand Down Expand Up @@ -105,9 +110,11 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => {
text: error instanceof Error ? error.message : JSON.stringify(error),
});
return;
} finally {
setIsFormSubmitting(false);
}
},
[notifications?.toasts, http, application, workspaceClient]
[notifications?.toasts, http, application, workspaceClient, isFormSubmitting]
);

const isFormReadyToRender =
Expand Down Expand Up @@ -137,7 +144,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => {
hasShadow={false}
>
{isFormReadyToRender && (
<WorkspaceForm
<WorkspaceCreatorForm
application={application}
savedObjects={savedObjects}
onSubmit={handleWorkspaceFormSubmit}
Expand All @@ -146,6 +153,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => {
dataSourceManagement={dataSourceManagement}
availableUseCases={availableUseCases}
defaultValues={defaultWorkspaceFormValues}
isSubmitting={isFormSubmitting}
/>
)}
</EuiPageContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
$workspaceCreateRightSideBarTopOffset: 116px;
$workspaceCreateRightSideBarBottomOffset: 100px;

.workspaceCreateRightSidebar {
position: sticky;
top: $workspaceCreateRightSideBarTopOffset;
max-height: calc(100vh - $workspaceCreateRightSideBarTopOffset - $workspaceCreateRightSideBarBottomOffset);
overflow: hidden;
display: flex;
flex-direction: column;
width: 280px;

@include ouiBreakpoint("xs","s") {
position: static;
width: 100%;
}
}

.workspaceCreateRightSideBarContentWrapper {
overflow-y: scroll;

@include ouiBreakpoint("xs","s") {
overflow: visible;
}
}

.workspaceCreateRightSideBarActionsWrapper {
padding: $ouiSizeM;
border-radius: $ouiSizeM;
background: $ouiColorEmptyShade;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { fireEvent, render } from '@testing-library/react';
import { coreMock } from '../../../../../core/public/mocks';
import { DataSourceManagementPluginSetup } from '../../../../../plugins/data_source_management/public';
import { createMockedRegisteredUseCases } from '../../mocks';
import { WorkspaceOperationType } from './constants';
import { WorkspaceForm } from './workspace_form';
import { WorkspaceOperationType } from '../workspace_form';
import { WorkspaceCreatorForm } from './workspace_creator_form';

const mockCoreStart = coreMock.createStart();

Expand Down Expand Up @@ -37,7 +37,8 @@ const setup = (
};

return render(
<WorkspaceForm
<WorkspaceCreatorForm
isSubmitting={false}
application={application}
savedObjects={savedObjects}
operationType={WorkspaceOperationType.Create}
Expand All @@ -53,13 +54,13 @@ describe('WorkspaceForm', () => {
it('should enable data source panel for dashboard admin and when data source is enabled', () => {
const { getByText } = setup(true, mockDataSourceManagementSetup);

expect(getByText('Associate data source')).toBeInTheDocument();
expect(getByText('Associate data sources')).toBeInTheDocument();
});

it('should not display data source panel for non dashboard admin', () => {
const { queryByText } = setup(false, mockDataSourceManagementSetup);

expect(queryByText('Associate data source')).not.toBeInTheDocument();
expect(queryByText('Associate data sources')).not.toBeInTheDocument();
});

it('should not display data source panel when data source is disabled', () => {
Expand Down
Loading
Loading