Skip to content

Commit

Permalink
Add workspace create page
Browse files Browse the repository at this point in the history
Signed-off-by: Lin Wang <wonglam@amazon.com>
  • Loading branch information
wanglam committed Mar 6, 2024
1 parent 370dbc6 commit baa72cf
Show file tree
Hide file tree
Showing 18 changed files with 1,905 additions and 4 deletions.
10 changes: 10 additions & 0 deletions src/core/public/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@ export interface App<HistoryLocationState = unknown> {
* ```
*/
exactRoute?: boolean;

/**
* The dependencies of one application, required feature will be automatic select and can't
* be unselect in the workspace configuration.
*/
dependencies?: {
[key: string]: {
type: 'required' | 'optional';
};
};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,4 @@ export { __osdBootstrap__ } from './osd_bootstrap';

export { WorkspacesStart, WorkspacesSetup, WorkspacesService } from './workspace';

export { WORKSPACE_TYPE } from '../utils';
export { WORKSPACE_TYPE, MANAGEMENT_WORKSPACE_ID } from '../utils';
6 changes: 6 additions & 0 deletions src/plugins/workspace/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
* 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];
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 =
Expand All @@ -16,3 +20,5 @@ export enum WorkspacePermissionMode {
LibraryRead = 'library_read',
LibraryWrite = 'library_write',
}
export const WORKSPACE_OP_TYPE_CREATE = 'create';
export const WORKSPACE_OP_TYPE_UPDATE = 'update';
17 changes: 17 additions & 0 deletions src/plugins/workspace/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,25 @@ import ReactDOM from 'react-dom';
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 { Services } from './types';

export const renderCreatorApp = (
{ element, history, appBasePath }: AppMountParameters,
services: Services
) => {
ReactDOM.render(
<OpenSearchDashboardsContextProvider services={services}>
<WorkspaceCreatorApp />
</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
53 changes: 53 additions & 0 deletions src/plugins/workspace/public/components/utils/feature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
isFeatureDependBySelectedFeatures,
getFinalFeatureIdsByDependency,
generateFeatureDependencyMap,
} from './feature';

describe('feature utils', () => {
describe('isFeatureDependBySelectedFeatures', () => {
it('should return true', () => {
expect(isFeatureDependBySelectedFeatures('a', ['b'], { b: ['a'] })).toBe(true);
expect(isFeatureDependBySelectedFeatures('a', ['b'], { b: ['a', 'c'] })).toBe(true);
});
it('should return false', () => {
expect(isFeatureDependBySelectedFeatures('a', ['b'], { b: ['c'] })).toBe(false);
expect(isFeatureDependBySelectedFeatures('a', ['b'], {})).toBe(false);
});
});

describe('getFinalFeatureIdsByDependency', () => {
it('should return consistent feature ids', () => {
expect(getFinalFeatureIdsByDependency(['a'], { a: ['b'] }, ['c', 'd'])).toStrictEqual([
'c',
'd',
'a',
'b',
]);
expect(getFinalFeatureIdsByDependency(['a'], { a: ['b', 'e'] }, ['c', 'd'])).toStrictEqual([
'c',
'd',
'a',
'b',
'e',
]);
});
});

it('should generate consistent features dependency map', () => {
expect(
generateFeatureDependencyMap([
{ id: 'a', dependencies: { b: { type: 'required' }, c: { type: 'optional' } } },
{ id: 'b', dependencies: { c: { type: 'required' } } },
])
).toEqual({
a: ['b'],
b: ['c'],
});
});
});
60 changes: 60 additions & 0 deletions src/plugins/workspace/public/components/utils/feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { App } from '../../../../../core/public';

export const isFeatureDependBySelectedFeatures = (
featureId: string,
selectedFeatureIds: string[],
featureDependencies: { [key: string]: string[] }
) =>
selectedFeatureIds.some((selectedFeatureId) =>
(featureDependencies[selectedFeatureId] || []).some((dependencies) =>
dependencies.includes(featureId)
)
);

/**
*
* Generate new feature id list based the old feature id list
* and feature dependencies map. The feature dependency map may
* has duplicate ids with old feature id list. Use set here to
* get the unique feature ids.
*
* @param featureIds a feature id list need to add based old feature id list
* @param featureDependencies a feature dependencies map to get depended feature ids
* @param oldFeatureIds a feature id list that represent current feature id selection states
*/
export const getFinalFeatureIdsByDependency = (
featureIds: string[],
featureDependencies: { [key: string]: string[] },
oldFeatureIds: string[] = []
) =>
Array.from(
new Set([
...oldFeatureIds,
...featureIds.reduce(
(pValue, featureId) => [...pValue, ...(featureDependencies[featureId] || [])],
featureIds
),
])
);

export const generateFeatureDependencyMap = (
allFeatures: Array<Pick<App, 'id' | 'dependencies'>>
) =>
allFeatures.reduce<{ [key: string]: string[] }>(
(pValue, { id, dependencies }) =>
dependencies
? {
...pValue,
[id]: [
...(pValue[id] || []),
...Object.keys(dependencies).filter((key) => dependencies[key].type === 'required'),
],
}
: pValue,
{}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { WorkspaceCreator } from './workspace_creator';
export { WorkspacePermissionSetting } from './workspace_permission_setting_panel';
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
EuiBottomBar,
EuiButton,
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { i18n } from '@osd/i18n';
import React, { useState } from 'react';
import { ApplicationStart } from 'opensearch-dashboards/public';
import { WORKSPACE_OP_TYPE_CREATE, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants';
import { WorkspaceCancelModal } from './workspace_cancel_modal';

interface WorkspaceBottomBarProps {
formId: string;
opType?: string;
numberOfErrors: number;
application: ApplicationStart;
numberOfUnSavedChanges: number;
}

export const WorkspaceBottomBar = ({
formId,
opType,
numberOfErrors,
numberOfUnSavedChanges,
application,
}: WorkspaceBottomBarProps) => {
const [isCancelModalVisible, setIsCancelModalVisible] = useState(false);
const closeCancelModal = () => setIsCancelModalVisible(false);
const showCancelModal = () => setIsCancelModalVisible(true);

return (
<div>
<EuiSpacer size="xl" />
<EuiSpacer size="xl" />
<EuiBottomBar>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s">
{opType === WORKSPACE_OP_TYPE_UPDATE ? (
<EuiText textAlign="left">
{i18n.translate('workspace.form.bottomBar.unsavedChanges', {
defaultMessage: `${numberOfUnSavedChanges} Unsaved change(s)`,
})}
</EuiText>
) : (
<EuiText textAlign="left">
{i18n.translate('workspace.form.bottomBar.errors', {
defaultMessage: `${numberOfErrors} Error(s)`,
})}
</EuiText>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="m">
<EuiButtonEmpty
color="ghost"
onClick={showCancelModal}
data-test-subj="workspaceForm-bottomBar-cancelButton"
>
{i18n.translate('workspace.form.bottomBar.cancel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
<EuiSpacer />
{opType === WORKSPACE_OP_TYPE_CREATE && (
<EuiButton
fill
type="submit"
color="primary"
form={formId}
data-test-subj="workspaceForm-bottomBar-createButton"
>
{i18n.translate('workspace.form.bottomBar.createWorkspace', {
defaultMessage: 'Create workspace',
})}
</EuiButton>
)}
{opType === WORKSPACE_OP_TYPE_UPDATE && (
<EuiButton form={formId} type="submit" fill color="primary">
{i18n.translate('workspace.form.bottomBar.saveChanges', {
defaultMessage: 'Save changes',
})}
</EuiButton>
)}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiBottomBar>
<WorkspaceCancelModal
application={application}
visible={isCancelModalVisible}
closeCancelModal={closeCancelModal}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { i18n } from '@osd/i18n';
import { EuiConfirmModal } from '@elastic/eui';
import { ApplicationStart } from 'opensearch-dashboards/public';
import { WORKSPACE_LIST_APP_ID } from '../../../common/constants';

interface WorkspaceCancelModalProps {
visible: boolean;
application: ApplicationStart;
closeCancelModal: () => void;
}

export const WorkspaceCancelModal = ({
application,
visible,
closeCancelModal,
}: WorkspaceCancelModalProps) => {
if (!visible) {
return null;
}

return (
<EuiConfirmModal
data-test-subj="workspaceForm-cancelModal"
title={i18n.translate('workspace.form.cancelModal.title', {
defaultMessage: 'Discard changes?',
})}
onCancel={closeCancelModal}
onConfirm={() => application?.navigateToApp(WORKSPACE_LIST_APP_ID)}
cancelButtonText={i18n.translate('workspace.form.cancelButtonText.', {
defaultMessage: 'Continue editing',
})}
confirmButtonText={i18n.translate('workspace.form.confirmButtonText.', {
defaultMessage: 'Discard changes',
})}
buttonColor="danger"
defaultFocusedButton="confirm"
>
{i18n.translate('workspace.form.cancelModal.body', {
defaultMessage: 'This will discard all changes. Are you sure?',
})}
</EuiConfirmModal>
);
};
Loading

0 comments on commit baa72cf

Please sign in to comment.