diff --git a/src/plugins/workspace/public/components/workspace_creator/index.tsx b/src/plugins/workspace/public/components/workspace_creator/index.tsx index 11a3e0feaf92..c8cdbfab65be 100644 --- a/src/plugins/workspace/public/components/workspace_creator/index.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/index.tsx @@ -3,8 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; - -export const WorkspaceCreator = () => { - return
TODO
; -}; +export { WorkspaceCreator } from './workspace_creator'; diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx new file mode 100644 index 000000000000..e7006464ba7d --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { EuiPage, EuiPageBody, EuiPageHeader, EuiPageContent } from '@elastic/eui'; + +import { useOpenSearchDashboards } from '../../../../../plugins/opensearch_dashboards_react/public'; + +import { WorkspaceForm } from './workspace_form'; + +export const WorkspaceCreator = () => { + const { + services: { application }, + } = useOpenSearchDashboards(); + + const handleWorkspaceFormSubmit = useCallback(() => {}, []); + + return ( + + + + + {application && ( + + )} + + + + ); +}; diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx new file mode 100644 index 000000000000..8cb3a1e3c39d --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx @@ -0,0 +1,338 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useState, FormEventHandler, useRef, useMemo } from 'react'; +import { groupBy } from 'lodash'; +import { + EuiPanel, + EuiSpacer, + EuiTitle, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiText, + EuiButton, + EuiFlexItem, + EuiCheckableCard, + htmlIdGenerator, + EuiFlexGrid, + EuiFlexGroup, + EuiImage, + EuiAccordion, + EuiCheckbox, + EuiCheckboxGroup, + EuiCheckableCardProps, + EuiCheckboxGroupProps, + EuiCheckboxProps, + EuiFieldTextProps, +} from '@elastic/eui'; + +import { WorkspaceTemplate } from '../../../../../core/types'; +import { AppNavLinkStatus, ApplicationStart } from '../../../../../core/public'; +import { useApplications, useWorkspaceTemplate } from '../../hooks'; + +interface WorkspaceFeature { + id: string; + name: string; + templates: WorkspaceTemplate[]; +} + +interface WorkspaceFeatureGroup { + name: string; + features: WorkspaceFeature[]; +} + +interface WorkspaceFormData { + name: string; + description?: string; + features: string[]; +} + +type WorkspaceFormErrors = { [key in keyof WorkspaceFormData]?: string }; + +const isWorkspaceFeatureGroup = ( + featureOrGroup: WorkspaceFeature | WorkspaceFeatureGroup +): featureOrGroup is WorkspaceFeatureGroup => 'features' in featureOrGroup; + +const workspaceHtmlIdGenerator = htmlIdGenerator(); + +interface WorkspaceFormProps { + application: ApplicationStart; + onSubmit?: (formData: WorkspaceFormData) => void; + defaultValues?: WorkspaceFormData; +} +export const WorkspaceForm = ({ application, onSubmit, defaultValues }: WorkspaceFormProps) => { + const { workspaceTemplates, templateFeatureMap } = useWorkspaceTemplate(application); + const applications = useApplications(application); + + const [name, setName] = useState(defaultValues?.name); + const [description, setDescription] = useState(defaultValues?.description); + const [selectedTemplateId, setSelectedTemplateId] = useState(); + const [selectedFeatureIds, setSelectedFeatureIds] = useState(defaultValues?.features || []); + const selectedTemplate = workspaceTemplates.find( + (template) => template.id === selectedTemplateId + ); + const [formErrors, setFormErrors] = useState({}); + const formIdRef = useRef(); + const getFormData = () => ({ + name, + description, + features: selectedFeatureIds, + }); + const getFormDataRef = useRef(getFormData); + getFormDataRef.current = getFormData; + + const featureOrGroups = useMemo(() => { + const category2Applications = groupBy(applications, 'category.label'); + return Object.keys(category2Applications).reduce< + Array + >((previousValue, currentKey) => { + const apps = category2Applications[currentKey]; + const features = apps + .filter( + ({ navLinkStatus, chromeless }) => + navLinkStatus !== AppNavLinkStatus.hidden && !chromeless + ) + .map(({ id, title, workspaceTemplate }) => ({ + id, + name: title, + templates: workspaceTemplate || [], + })); + if (features.length === 1 || currentKey === 'undefined') { + return [...previousValue, ...features]; + } + return [ + ...previousValue, + { + name: apps[0].category?.label || '', + features, + }, + ]; + }, []); + }, [applications]); + + if (!formIdRef.current) { + formIdRef.current = workspaceHtmlIdGenerator(); + } + + const handleTemplateCardChange = useCallback( + (e) => { + const templateId = e.target.value; + setSelectedTemplateId(templateId); + setSelectedFeatureIds( + featureOrGroups.reduce( + (previousData, currentData) => [ + ...previousData, + ...(isWorkspaceFeatureGroup(currentData) ? currentData.features : [currentData]) + .filter(({ templates }) => !!templates.find((template) => template.id === templateId)) + .map((feature) => feature.id), + ], + [] + ) + ); + }, + [featureOrGroups] + ); + + const handleFeatureChange = useCallback((featureId) => { + setSelectedFeatureIds((previousData) => + previousData.includes(featureId) + ? previousData.filter((id) => id !== featureId) + : [...previousData, featureId] + ); + }, []); + + const handleFeatureCheckboxChange = useCallback( + (e) => { + handleFeatureChange(e.target.id); + }, + [handleFeatureChange] + ); + + const handleFeatureGroupChange = useCallback( + (e) => { + for (const featureOrGroup of featureOrGroups) { + if (isWorkspaceFeatureGroup(featureOrGroup) && featureOrGroup.name === e.target.id) { + const groupFeatureIds = featureOrGroup.features.map((feature) => feature.id); + setSelectedFeatureIds((previousData) => { + const notExistsIds = groupFeatureIds.filter((id) => !previousData.includes(id)); + if (notExistsIds.length > 0) { + return [...previousData, ...notExistsIds]; + } + return previousData.filter((id) => !groupFeatureIds.includes(id)); + }); + } + } + }, + [featureOrGroups] + ); + + const handleFormSubmit = useCallback( + (e) => { + e.preventDefault(); + const formData = getFormDataRef.current(); + if (!formData.name) { + setFormErrors({ name: "Name can't be empty." }); + return; + } + setFormErrors({}); + onSubmit?.({ ...formData, name: formData.name }); + }, + [onSubmit] + ); + + const handleNameInputChange = useCallback['onChange']>((e) => { + setName(e.target.value); + }, []); + + const handleDescriptionInputChange = useCallback['onChange']>((e) => { + setDescription(e.target.value); + }, []); + + return ( + + + +

Workspace details

+
+ + + + + + Description - optional + + } + > + + +
+ + + +

Workspace Template

+
+ + + {workspaceTemplates.map((template) => ( + + + + ))} + + + {selectedTemplate && ( + <> + +

Features

+
+ + + {selectedTemplate.coverImage && ( + + + + )} + + {selectedTemplate.description} + +

Key Features:

+
+ + + {templateFeatureMap.get(selectedTemplate.id)?.map((feature) => ( + • {feature.title} + ))} + +
+
+ + + )} + + +

Advanced Options

+
+ + } + > + + {featureOrGroups.map((featureOrGroup) => { + const features = isWorkspaceFeatureGroup(featureOrGroup) + ? featureOrGroup.features + : []; + const selectedIds = selectedFeatureIds.filter((id) => + (isWorkspaceFeatureGroup(featureOrGroup) + ? featureOrGroup.features + : [featureOrGroup] + ).find((item) => item.id === id) + ); + return ( + + 0 ? `(${selectedIds.length}/${features.length})` : '' + }`} + checked={selectedIds.length > 0} + indeterminate={ + isWorkspaceFeatureGroup(featureOrGroup) && + selectedIds.length > 0 && + selectedIds.length < features.length + } + /> + {isWorkspaceFeatureGroup(featureOrGroup) && ( + ({ + id: item.id, + label: item.name, + }))} + idToSelectedMap={selectedIds.reduce( + (previousValue, currentValue) => ({ + ...previousValue, + [currentValue]: true, + }), + {} + )} + onChange={handleFeatureChange} + style={{ marginLeft: 40 }} + /> + )} + + ); + })} + +
+
+ + + + Create workspace + + +
+ ); +}; diff --git a/src/plugins/workspace/public/hooks.ts b/src/plugins/workspace/public/hooks.ts index 71019c5948a2..636a00742146 100644 --- a/src/plugins/workspace/public/hooks.ts +++ b/src/plugins/workspace/public/hooks.ts @@ -8,6 +8,17 @@ import { useObservable } from 'react-use'; import { useMemo } from 'react'; import { WorkspaceTemplate } from '../../../core/types'; +export function useApplications(application: ApplicationStart) { + const applications = useObservable(application.applications$); + return useMemo(() => { + const apps: PublicAppInfo[] = []; + applications?.forEach((app) => { + apps.push(app); + }); + return apps; + }, [applications]); +} + export function useWorkspaceTemplate(application: ApplicationStart) { const applications = useObservable(application.applications$);