Skip to content

Commit d983d14

Browse files
wanglamruanyl
authored andcommitted
Add color, icon and defaultVISTheme for workspace (opensearch-project#36)
* feat: add color, icon and defaultVISTheme field for workspace saved object Signed-off-by: Lin Wang <wonglam@amazon.com> * add new fields to workspace form Signed-off-by: Lin Wang <wonglam@amazon.com> * feat: remove feature or group name hack Signed-off-by: Lin Wang <wonglam@amazon.com> --------- Signed-off-by: Lin Wang <wonglam@amazon.com>
1 parent 274c96d commit d983d14

File tree

5 files changed

+171
-77
lines changed

5 files changed

+171
-77
lines changed

src/core/server/workspaces/routes/index.ts

+11-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ import { IWorkspaceDBImpl } from '../types';
99

1010
const WORKSPACES_API_BASE_URL = '/api/workspaces';
1111

12+
const workspaceAttributesSchema = schema.object({
13+
description: schema.maybe(schema.string()),
14+
name: schema.string(),
15+
features: schema.maybe(schema.arrayOf(schema.string())),
16+
color: schema.maybe(schema.string()),
17+
icon: schema.maybe(schema.string()),
18+
defaultVISTheme: schema.maybe(schema.string()),
19+
});
20+
1221
export function registerRoutes({
1322
client,
1423
logger,
@@ -72,11 +81,7 @@ export function registerRoutes({
7281
path: '',
7382
validate: {
7483
body: schema.object({
75-
attributes: schema.object({
76-
description: schema.maybe(schema.string()),
77-
name: schema.string(),
78-
features: schema.maybe(schema.arrayOf(schema.string())),
79-
}),
84+
attributes: workspaceAttributesSchema,
8085
}),
8186
},
8287
},
@@ -102,11 +107,7 @@ export function registerRoutes({
102107
id: schema.string(),
103108
}),
104109
body: schema.object({
105-
attributes: schema.object({
106-
description: schema.maybe(schema.string()),
107-
name: schema.string(),
108-
features: schema.maybe(schema.arrayOf(schema.string())),
109-
}),
110+
attributes: workspaceAttributesSchema,
110111
}),
111112
},
112113
},

src/core/server/workspaces/saved_objects/workspace.ts

+9
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ export const workspace: SavedObjectsType = {
4141
features: {
4242
type: 'text',
4343
},
44+
color: {
45+
type: 'text',
46+
},
47+
icon: {
48+
type: 'text',
49+
},
50+
defaultVISTheme: {
51+
type: 'text',
52+
},
4453
},
4554
},
4655
};

src/core/server/workspaces/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ export interface WorkspaceAttribute {
1515
name: string;
1616
description?: string;
1717
features?: string[];
18+
color?: string;
19+
icon?: string;
20+
defaultVISTheme?: string;
1821
}
1922

2023
export interface WorkspaceFindOptions {

src/plugins/workspace/public/components/workspace_creator/workspace_form.tsx

+112-67
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,23 @@ import {
2020
EuiFlexGrid,
2121
EuiFlexGroup,
2222
EuiImage,
23-
EuiAccordion,
2423
EuiCheckbox,
2524
EuiCheckboxGroup,
2625
EuiCheckableCardProps,
2726
EuiCheckboxGroupProps,
2827
EuiCheckboxProps,
2928
EuiFieldTextProps,
29+
EuiColorPicker,
30+
EuiColorPickerProps,
31+
EuiComboBox,
32+
EuiComboBoxProps,
3033
} from '@elastic/eui';
3134

3235
import { WorkspaceTemplate } from '../../../../../core/types';
3336
import { AppNavLinkStatus, ApplicationStart } from '../../../../../core/public';
3437
import { useApplications, useWorkspaceTemplate } from '../../hooks';
3538
import { WORKSPACE_OP_TYPE_CREATE, WORKSPACE_OP_TYPE_UPDATE } from '../../../common/constants';
39+
import { WorkspaceIconSelector } from './workspace_icon_selector';
3640

3741
interface WorkspaceFeature {
3842
id: string;
@@ -49,6 +53,9 @@ export interface WorkspaceFormData {
4953
name: string;
5054
description?: string;
5155
features: string[];
56+
color?: string;
57+
icon?: string;
58+
defaultVISTheme?: string;
5259
}
5360

5461
type WorkspaceFormErrors = { [key in keyof WorkspaceFormData]?: string };
@@ -59,6 +66,8 @@ const isWorkspaceFeatureGroup = (
5966

6067
const workspaceHtmlIdGenerator = htmlIdGenerator();
6168

69+
const defaultVISThemeOptions = [{ label: 'Categorical', value: 'categorical' }];
70+
6271
interface WorkspaceFormProps {
6372
application: ApplicationStart;
6473
onSubmit?: (formData: WorkspaceFormData) => void;
@@ -76,6 +85,10 @@ export const WorkspaceForm = ({
7685

7786
const [name, setName] = useState(defaultValues?.name);
7887
const [description, setDescription] = useState(defaultValues?.description);
88+
const [color, setColor] = useState(defaultValues?.color);
89+
const [icon, setIcon] = useState(defaultValues?.icon);
90+
const [defaultVISTheme, setDefaultVISTheme] = useState(defaultValues?.defaultVISTheme);
91+
7992
const [selectedTemplateId, setSelectedTemplateId] = useState<string>();
8093
const [selectedFeatureIds, setSelectedFeatureIds] = useState(defaultValues?.features || []);
8194
const selectedTemplate = workspaceTemplates.find(
@@ -87,6 +100,9 @@ export const WorkspaceForm = ({
87100
name,
88101
description,
89102
features: selectedFeatureIds,
103+
color,
104+
icon,
105+
defaultVISTheme,
90106
});
91107
const getFormDataRef = useRef(getFormData);
92108
getFormDataRef.current = getFormData;
@@ -120,6 +136,11 @@ export const WorkspaceForm = ({
120136
}, []);
121137
}, [applications]);
122138

139+
const selectedDefaultVISThemeOptions = useMemo(
140+
() => defaultVISThemeOptions.filter((item) => item.value === defaultVISTheme),
141+
[defaultVISTheme]
142+
);
143+
123144
if (!formIdRef.current) {
124145
formIdRef.current = workspaceHtmlIdGenerator();
125146
}
@@ -198,6 +219,20 @@ export const WorkspaceForm = ({
198219
setDescription(e.target.value);
199220
}, []);
200221

222+
const handleColorChange = useCallback<Required<EuiColorPickerProps>['onChange']>((text) => {
223+
setColor(text);
224+
}, []);
225+
226+
const handleIconChange = useCallback((newIcon: string) => {
227+
setIcon(newIcon);
228+
}, []);
229+
230+
const handleDefaultVISThemeInputChange = useCallback<
231+
Required<EuiComboBoxProps<string>>['onChange']
232+
>((options) => {
233+
setDefaultVISTheme(options[0]?.value);
234+
}, []);
235+
201236
return (
202237
<EuiForm id={formIdRef.current} onSubmit={handleFormSubmit} component="form">
203238
<EuiPanel>
@@ -217,6 +252,25 @@ export const WorkspaceForm = ({
217252
>
218253
<EuiFieldText value={description} onChange={handleDescriptionInputChange} />
219254
</EuiFormRow>
255+
<EuiFormRow label="Color" isInvalid={!!formErrors.color} error={formErrors.color}>
256+
<EuiColorPicker color={color} onChange={handleColorChange} />
257+
</EuiFormRow>
258+
<EuiFormRow label="Icon" isInvalid={!!formErrors.icon} error={formErrors.icon}>
259+
<WorkspaceIconSelector value={icon} onChange={handleIconChange} color={color} />
260+
</EuiFormRow>
261+
<EuiFormRow
262+
label="Default VIS Theme"
263+
isInvalid={!!formErrors.defaultVISTheme}
264+
error={formErrors.defaultVISTheme}
265+
>
266+
<EuiComboBox
267+
options={defaultVISThemeOptions}
268+
singleSelection
269+
onChange={handleDefaultVISThemeInputChange}
270+
selectedOptions={selectedDefaultVISThemeOptions}
271+
isClearable={false}
272+
/>
273+
</EuiFormRow>
220274
</EuiPanel>
221275
<EuiSpacer />
222276
<EuiPanel>
@@ -267,74 +321,65 @@ export const WorkspaceForm = ({
267321
<EuiSpacer />
268322
</>
269323
)}
270-
<EuiAccordion
271-
id={workspaceHtmlIdGenerator()}
272-
buttonContent={
273-
<>
274-
<EuiTitle size="xs">
275-
<h3>Advanced Options</h3>
276-
</EuiTitle>
277-
</>
278-
}
279-
>
280-
<EuiFlexGrid style={{ paddingLeft: 20, paddingTop: 20 }} columns={2}>
281-
{featureOrGroups.map((featureOrGroup) => {
282-
const features = isWorkspaceFeatureGroup(featureOrGroup)
324+
</EuiPanel>
325+
<EuiSpacer />
326+
<EuiPanel>
327+
<EuiTitle size="s">
328+
<h2>Workspace features</h2>
329+
</EuiTitle>
330+
<EuiFlexGrid style={{ paddingLeft: 20, paddingTop: 20 }} columns={2}>
331+
{featureOrGroups.map((featureOrGroup) => {
332+
const features = isWorkspaceFeatureGroup(featureOrGroup) ? featureOrGroup.features : [];
333+
const selectedIds = selectedFeatureIds.filter((id) =>
334+
(isWorkspaceFeatureGroup(featureOrGroup)
283335
? featureOrGroup.features
284-
: [];
285-
const selectedIds = selectedFeatureIds.filter((id) =>
286-
(isWorkspaceFeatureGroup(featureOrGroup)
287-
? featureOrGroup.features
288-
: [featureOrGroup]
289-
).find((item) => item.id === id)
290-
);
291-
return (
292-
<EuiFlexItem key={featureOrGroup.name}>
293-
<EuiCheckbox
294-
id={
295-
isWorkspaceFeatureGroup(featureOrGroup)
296-
? featureOrGroup.name
297-
: featureOrGroup.id
298-
}
299-
onChange={
300-
isWorkspaceFeatureGroup(featureOrGroup)
301-
? handleFeatureGroupChange
302-
: handleFeatureCheckboxChange
303-
}
304-
label={`${
305-
featureOrGroup.name === 'OpenSearch Plugins'
306-
? 'OpenSearch Features'
307-
: featureOrGroup.name
308-
}${features.length > 0 ? `(${selectedIds.length}/${features.length})` : ''}`}
309-
checked={selectedIds.length > 0}
310-
indeterminate={
311-
isWorkspaceFeatureGroup(featureOrGroup) &&
312-
selectedIds.length > 0 &&
313-
selectedIds.length < features.length
314-
}
336+
: [featureOrGroup]
337+
).find((item) => item.id === id)
338+
);
339+
return (
340+
<EuiFlexItem key={featureOrGroup.name}>
341+
<EuiCheckbox
342+
id={
343+
isWorkspaceFeatureGroup(featureOrGroup)
344+
? featureOrGroup.name
345+
: featureOrGroup.id
346+
}
347+
onChange={
348+
isWorkspaceFeatureGroup(featureOrGroup)
349+
? handleFeatureGroupChange
350+
: handleFeatureCheckboxChange
351+
}
352+
label={`${featureOrGroup.name}${
353+
features.length > 0 ? `(${selectedIds.length}/${features.length})` : ''
354+
}`}
355+
checked={selectedIds.length > 0}
356+
indeterminate={
357+
isWorkspaceFeatureGroup(featureOrGroup) &&
358+
selectedIds.length > 0 &&
359+
selectedIds.length < features.length
360+
}
361+
/>
362+
{isWorkspaceFeatureGroup(featureOrGroup) && (
363+
<EuiCheckboxGroup
364+
options={featureOrGroup.features.map((item) => ({
365+
id: item.id,
366+
label: item.name,
367+
}))}
368+
idToSelectedMap={selectedIds.reduce(
369+
(previousValue, currentValue) => ({
370+
...previousValue,
371+
[currentValue]: true,
372+
}),
373+
{}
374+
)}
375+
onChange={handleFeatureChange}
376+
style={{ marginLeft: 40 }}
315377
/>
316-
{isWorkspaceFeatureGroup(featureOrGroup) && (
317-
<EuiCheckboxGroup
318-
options={featureOrGroup.features.map((item) => ({
319-
id: item.id,
320-
label: item.name,
321-
}))}
322-
idToSelectedMap={selectedIds.reduce(
323-
(previousValue, currentValue) => ({
324-
...previousValue,
325-
[currentValue]: true,
326-
}),
327-
{}
328-
)}
329-
onChange={handleFeatureChange}
330-
style={{ marginLeft: 40 }}
331-
/>
332-
)}
333-
</EuiFlexItem>
334-
);
335-
})}
336-
</EuiFlexGrid>
337-
</EuiAccordion>
378+
)}
379+
</EuiFlexItem>
380+
);
381+
})}
382+
</EuiFlexGrid>
338383
</EuiPanel>
339384
<EuiSpacer />
340385
<EuiText textAlign="right">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
8+
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
9+
10+
const icons = ['glasses', 'search', 'bell'];
11+
12+
export const WorkspaceIconSelector = ({
13+
color,
14+
value,
15+
onChange,
16+
}: {
17+
color?: string;
18+
value?: string;
19+
onChange: (value: string) => void;
20+
}) => {
21+
return (
22+
<EuiFlexGroup>
23+
{icons.map((item) => (
24+
<EuiFlexItem
25+
key={item}
26+
onClick={() => {
27+
onChange(item);
28+
}}
29+
grow={false}
30+
>
31+
<EuiIcon size="l" type={item} color={value === item ? color : undefined} />
32+
</EuiFlexItem>
33+
))}
34+
</EuiFlexGroup>
35+
);
36+
};

0 commit comments

Comments
 (0)