From f953874f99be0f7256e2e8e82a854b516c37280c Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 29 Mar 2022 14:08:54 -0700 Subject: [PATCH 01/47] [Chore] Moves Drag and Drop to new branch (#1400) * Initial Drag and Drop plugin code (#946) * Initial Drag and Drop plugin code Signed-off-by: Ashwin Pc * Adds state management to Drag and Drop Signed-off-by: Ashwin Pc * Moves Drag and Drop to create visualization menu Signed-off-by: Ashwin Pc * Field Search in Data panel (#995) Add ability to search on index fields Signed-off-by: Abbas Hussain * Adds initial type service (#1260) Signed-off-by: Ashwin Pc * chore: updates viz modal snapshot Signed-off-by: Ashwin Pc * fix(License): Fixes license headers Signed-off-by: Ashwin Pc Co-authored-by: Abbas Hussain --- package.json | 1 + .../__snapshots__/new_vis_modal.test.tsx.snap | 216 ++++++++++-------- .../wizard/type_selection/type_selection.tsx | 2 +- src/plugins/wizard/.i18nrc.json | 7 + src/plugins/wizard/README.md | 11 + src/plugins/wizard/common/index.ts | 9 + .../common/wizard_saved_object_attributes.ts | 14 ++ src/plugins/wizard/opensearch_dashboards.json | 17 ++ .../wizard/public/application/_variables.scss | 3 + .../wizard/public/application/app.scss | 13 ++ src/plugins/wizard/public/application/app.tsx | 29 +++ .../public/application/components/_util.scss | 8 + .../components/data_tab/config_panel.scss | 9 + .../components/data_tab/config_panel.tsx | 31 +++ .../components/data_tab/config_section.scss | 23 ++ .../components/data_tab/config_section.tsx | 83 +++++++ .../components/data_tab/field_search.tsx | 47 ++++ .../components/data_tab/field_selector.scss | 10 + .../components/data_tab/field_selector.tsx | 118 ++++++++++ .../data_tab/field_selector_field.scss | 12 + .../data_tab/field_selector_field.tsx | 83 +++++++ .../components/data_tab/index.scss | 7 + .../application/components/data_tab/index.tsx | 19 ++ .../application/components/side_nav.scss | 18 ++ .../application/components/side_nav.tsx | 72 ++++++ .../application/components/style_tab.tsx | 10 + .../application/components/top_nav.scss | 4 + .../public/application/components/top_nav.tsx | 40 ++++ .../application/components/workspace.scss | 12 + .../application/components/workspace.tsx | 113 +++++++++ .../wizard/public/application/index.tsx | 35 +++ .../application/utils/async_search/index.ts | 48 ++++ .../utils/drag_drop/drag_drop_context.tsx | 113 +++++++++ .../application/utils/drag_drop/index.ts | 6 + .../application/utils/get_top_nav_config.tsx | 127 ++++++++++ .../utils/state_management/config_slice.ts | 62 +++++ .../state_management/datasource_slice.ts | 63 +++++ .../utils/state_management/hooks.ts | 11 + .../utils/state_management/index.ts | 7 + .../utils/state_management/preload.ts | 22 ++ .../utils/state_management/store.ts | 34 +++ .../state_management/visualization_slice.ts | 39 ++++ src/plugins/wizard/public/index.ts | 14 ++ src/plugins/wizard/public/plugin.ts | 98 ++++++++ .../public/services/type_service/index.ts | 6 + .../services/type_service/type_service.ts | 86 +++++++ .../type_service/visualization_type.ts | 48 ++++ src/plugins/wizard/public/types.ts | 35 +++ .../public/visualizations/bar_chart/index.ts | 16 ++ .../wizard/public/visualizations/index.ts | 16 ++ .../public/visualizations/pie_chart/index.ts | 15 ++ src/plugins/wizard/server/index.ts | 16 ++ src/plugins/wizard/server/plugin.ts | 44 ++++ src/plugins/wizard/server/routes/index.ts | 22 ++ .../wizard/server/saved_objects/index.ts | 6 + .../wizard/server/saved_objects/wizard_app.ts | 36 +++ src/plugins/wizard/server/types.ts | 9 + yarn.lock | 17 ++ 58 files changed, 1995 insertions(+), 97 deletions(-) create mode 100644 src/plugins/wizard/.i18nrc.json create mode 100755 src/plugins/wizard/README.md create mode 100644 src/plugins/wizard/common/index.ts create mode 100644 src/plugins/wizard/common/wizard_saved_object_attributes.ts create mode 100644 src/plugins/wizard/opensearch_dashboards.json create mode 100644 src/plugins/wizard/public/application/_variables.scss create mode 100644 src/plugins/wizard/public/application/app.scss create mode 100644 src/plugins/wizard/public/application/app.tsx create mode 100644 src/plugins/wizard/public/application/components/_util.scss create mode 100644 src/plugins/wizard/public/application/components/data_tab/config_panel.scss create mode 100644 src/plugins/wizard/public/application/components/data_tab/config_panel.tsx create mode 100644 src/plugins/wizard/public/application/components/data_tab/config_section.scss create mode 100644 src/plugins/wizard/public/application/components/data_tab/config_section.tsx create mode 100644 src/plugins/wizard/public/application/components/data_tab/field_search.tsx create mode 100644 src/plugins/wizard/public/application/components/data_tab/field_selector.scss create mode 100644 src/plugins/wizard/public/application/components/data_tab/field_selector.tsx create mode 100644 src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss create mode 100644 src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx create mode 100644 src/plugins/wizard/public/application/components/data_tab/index.scss create mode 100644 src/plugins/wizard/public/application/components/data_tab/index.tsx create mode 100644 src/plugins/wizard/public/application/components/side_nav.scss create mode 100644 src/plugins/wizard/public/application/components/side_nav.tsx create mode 100644 src/plugins/wizard/public/application/components/style_tab.tsx create mode 100644 src/plugins/wizard/public/application/components/top_nav.scss create mode 100644 src/plugins/wizard/public/application/components/top_nav.tsx create mode 100644 src/plugins/wizard/public/application/components/workspace.scss create mode 100644 src/plugins/wizard/public/application/components/workspace.tsx create mode 100644 src/plugins/wizard/public/application/index.tsx create mode 100644 src/plugins/wizard/public/application/utils/async_search/index.ts create mode 100644 src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx create mode 100644 src/plugins/wizard/public/application/utils/drag_drop/index.ts create mode 100644 src/plugins/wizard/public/application/utils/get_top_nav_config.tsx create mode 100644 src/plugins/wizard/public/application/utils/state_management/config_slice.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/hooks.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/index.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/preload.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/store.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts create mode 100644 src/plugins/wizard/public/index.ts create mode 100644 src/plugins/wizard/public/plugin.ts create mode 100644 src/plugins/wizard/public/services/type_service/index.ts create mode 100644 src/plugins/wizard/public/services/type_service/type_service.ts create mode 100644 src/plugins/wizard/public/services/type_service/visualization_type.ts create mode 100644 src/plugins/wizard/public/types.ts create mode 100644 src/plugins/wizard/public/visualizations/bar_chart/index.ts create mode 100644 src/plugins/wizard/public/visualizations/index.ts create mode 100644 src/plugins/wizard/public/visualizations/pie_chart/index.ts create mode 100644 src/plugins/wizard/server/index.ts create mode 100644 src/plugins/wizard/server/plugin.ts create mode 100644 src/plugins/wizard/server/routes/index.ts create mode 100644 src/plugins/wizard/server/saved_objects/index.ts create mode 100644 src/plugins/wizard/server/saved_objects/wizard_app.ts create mode 100644 src/plugins/wizard/server/types.ts diff --git a/package.json b/package.json index b18c6dc863a5..c83730e6a3a9 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "@osd/std": "1.0.0", "@osd/ui-framework": "1.0.0", "@osd/ui-shared-deps": "1.0.0", + "@reduxjs/toolkit": "^1.6.2", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 0c72df9a5fe5..2685e7cc8d21 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -264,10 +264,9 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
-

-

-

-

- - + + + +

- - + + + +

- - + + + +

- - + + + +

-

-

-

-

- - + + + +

- - + + + +

- - + + + +

- - + + + +

); diff --git a/src/plugins/wizard/.i18nrc.json b/src/plugins/wizard/.i18nrc.json new file mode 100644 index 000000000000..2b511494a460 --- /dev/null +++ b/src/plugins/wizard/.i18nrc.json @@ -0,0 +1,7 @@ +{ + "prefix": "wizard", + "paths": { + "wizard": "." + }, + "translations": ["translations/ja-JP.json"] +} diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md new file mode 100755 index 000000000000..bcb362b374cb --- /dev/null +++ b/src/plugins/wizard/README.md @@ -0,0 +1,11 @@ +# wizard + +A OpenSearch Dashboards plugin + +--- + +## Development + +See the [OpenSearch Dashboards contributing +guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/CONTRIBUTING.md) for instructions +setting up your development environment. diff --git a/src/plugins/wizard/common/index.ts b/src/plugins/wizard/common/index.ts new file mode 100644 index 000000000000..4b3522fec709 --- /dev/null +++ b/src/plugins/wizard/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const PLUGIN_ID = 'wizard'; +export const PLUGIN_NAME = 'Wizard'; + +export { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from './wizard_saved_object_attributes'; diff --git a/src/plugins/wizard/common/wizard_saved_object_attributes.ts b/src/plugins/wizard/common/wizard_saved_object_attributes.ts new file mode 100644 index 000000000000..ff6c12417d24 --- /dev/null +++ b/src/plugins/wizard/common/wizard_saved_object_attributes.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from 'opensearch-dashboards/public'; + +export const WIZARD_SAVED_OBJECT = 'wizard'; + +export interface WizardSavedObjectAttributes extends SavedObjectAttributes { + title: string; + description?: string; + state: string; +} diff --git a/src/plugins/wizard/opensearch_dashboards.json b/src/plugins/wizard/opensearch_dashboards.json new file mode 100644 index 000000000000..8dd00aae3890 --- /dev/null +++ b/src/plugins/wizard/opensearch_dashboards.json @@ -0,0 +1,17 @@ +{ + "id": "wizard", + "version": "1.0.0", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": [ + "navigation", + "data", + "opensearchDashboardsReact", + "savedObjects", + "embeddable", + "dashboard", + "visualizations" + ], + "optionalPlugins": [] +} diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss new file mode 100644 index 000000000000..c1b3646e8e49 --- /dev/null +++ b/src/plugins/wizard/public/application/_variables.scss @@ -0,0 +1,3 @@ +@import '@elastic/eui/src/global_styling/variables/header'; + +$osdHeaderOffset: $euiHeaderHeightCompensation * 2; \ No newline at end of file diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss new file mode 100644 index 000000000000..2e1e93f44312 --- /dev/null +++ b/src/plugins/wizard/public/application/app.scss @@ -0,0 +1,13 @@ +@import "variables"; + +.wizLayout { + padding: 0; + display: grid; + grid-template-rows: min-content 1fr; + grid-template-columns: 420px 1fr; + grid-template-areas: + "topNav topNav" + "sideNav workspace" + ; + height: calc(100vh - #{$osdHeaderOffset}); // TODO: update 190px to correct offset variable +} diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx new file mode 100644 index 000000000000..7d578ee77cda --- /dev/null +++ b/src/plugins/wizard/public/application/app.tsx @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { I18nProvider } from '@osd/i18n/react'; +import { EuiPage } from '@elastic/eui'; +import { SideNav } from './components/side_nav'; +import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; +import { Workspace } from './components/workspace'; + +import './app.scss'; +import { TopNav } from './components/top_nav'; + +export const WizardApp = () => { + // Render the application DOM. + return ( + + + + + + + + + + ); +}; diff --git a/src/plugins/wizard/public/application/components/_util.scss b/src/plugins/wizard/public/application/components/_util.scss new file mode 100644 index 000000000000..9a444c1fe091 --- /dev/null +++ b/src/plugins/wizard/public/application/components/_util.scss @@ -0,0 +1,8 @@ +@mixin scrollNavParent ($template-row: none) { + display: grid; + min-height: 0; + + @if $template-row != 'none' { + grid-template-rows: $template-row; + } +} \ No newline at end of file diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.scss b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss new file mode 100644 index 000000000000..7477dcfca813 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss @@ -0,0 +1,9 @@ +.wizConfigPanel { + background: #f0f1f3; + border-left: $euiBorderThin; + padding: $euiSizeS; +} + +.wizConfigPanel__title { + margin-left: $euiSizeS; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx new file mode 100644 index 000000000000..ec910b7352de --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiForm, EuiTitle } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { ConfigSection } from './config_section'; + +import './config_panel.scss'; +import { useTypedSelector } from '../../utils/state_management'; + +export function ConfigPanel() { + const { configSections } = useTypedSelector((state) => state.config); + + return ( + + +

+ {i18n.translate('wizard.nav.dataTab.configPanel.title', { + defaultMessage: 'Configuration', + })} +

+ + {Object.entries(configSections).map(([sectionId, sectionProps], index) => ( + + ))} + + ); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/config_section.scss b/src/plugins/wizard/public/application/components/data_tab/config_section.scss new file mode 100644 index 000000000000..79d0d3a913fd --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/config_section.scss @@ -0,0 +1,23 @@ +.wizConfigSection { + margin-top: $euiSize; + border-bottom: $euiBorderThin; + padding-bottom: $euiSize; + + &:last-child { + border-bottom: none; + } + + & .euiFormRow__labelWrapper { + margin-left: $euiSizeS; + } +} + +.wizConfigSection__dropTarget { + @include euiSlightShadow; + background: $euiColorEmptyShade; + border: $euiBorderThin; + box-shadow: 0px 2px 2px rgba(152, 162, 179, 0.15); + border-radius: $euiBorderRadius; + padding: $euiSizeS $euiSizeM; + color: $euiColorDarkShade; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/config_section.tsx b/src/plugins/wizard/public/application/components/data_tab/config_section.tsx new file mode 100644 index 000000000000..64f74824d71a --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/config_section.tsx @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButtonIcon, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React, { useCallback } from 'react'; +import { IndexPatternField } from 'src/plugins/data/common'; +import { useDrop } from '../../utils/drag_drop'; +import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; +import { + addConfigSectionField, + removeConfigSectionField, +} from '../../utils/state_management/config_slice'; + +import './config_section.scss'; + +interface ConfigSectionProps { + id: string; + title: string; +} + +export const ConfigSection = ({ title, id }: ConfigSectionProps) => { + const dispatch = useTypedDispatch(); + const { fields } = useTypedSelector((state) => state.config.configSections[id]); + + const dropHandler = useCallback( + (field: IndexPatternField) => { + dispatch( + addConfigSectionField({ + sectionId: id, + field, + }) + ); + }, + [dispatch, id] + ); + const [dropProps, { isValidDropTarget, dragData }] = useDrop('dataPlane', dropHandler); + + const dropTargetString = dragData + ? dragData.type + : i18n.translate('wizard.nav.dataTab.configPanel.dropTarget.placeholder', { + defaultMessage: 'Click or drop to add', + }); + + return ( +
+ +

{title}

+
+ {fields.length ? ( + fields.map((field, index) => ( + + + {field.displayName} + + + dispatch( + removeConfigSectionField({ + sectionId: id, + field, + }) + ) + } + /> + + )) + ) : ( +
+ {dropTargetString} +
+ )} +
+ ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/field_search.tsx b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx new file mode 100644 index 000000000000..2db8404c93c6 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { setSearchField } from '../../utils/state_management/datasource_slice'; +import { useTypedDispatch } from '../../utils/state_management'; + +export interface Props { + /** + * the input value of the user + */ + value?: string; +} + +/** + * Component is Wizard's side bar to search of available fields + * Additionally there's a button displayed that allows the user to show/hide more filter fields + */ +export function FieldSearch({ value }: Props) { + const searchPlaceholder = i18n.translate('wizard.fieldChooser.searchPlaceHolder', { + defaultMessage: 'Search field names', + }); + + const dispatch = useTypedDispatch(); + + return ( + + + + dispatch(setSearchField(event.currentTarget.value))} + placeholder={searchPlaceholder} + value={value} + /> + + + + ); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss new file mode 100644 index 000000000000..c05f75457b0b --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss @@ -0,0 +1,10 @@ +@import "../util"; + +.wizFieldSelector { + @include scrollNavParent(auto 1fr); + padding: $euiSizeS; +} + +.wizFieldSelector__fieldGroups { + overflow-y: auto; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx new file mode 100644 index 000000000000..1464f31aabd9 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useState, useEffect } from 'react'; +import { EuiFlexItem, EuiAccordion, EuiSpacer, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; +import { FieldSearch } from './field_search'; + +import { + IndexPatternField, + OPENSEARCH_FIELD_TYPES, + OSD_FIELD_TYPES, +} from '../../../../../data/public'; +import { FieldSelectorField } from './field_selector_field'; + +import './field_selector.scss'; +import { useTypedSelector } from '../../utils/state_management'; + +interface IFieldCategories { + categorical: IndexPatternField[]; + numerical: IndexPatternField[]; + meta: IndexPatternField[]; +} + +const META_FIELDS: string[] = [ + OPENSEARCH_FIELD_TYPES._ID, + OPENSEARCH_FIELD_TYPES._INDEX, + OPENSEARCH_FIELD_TYPES._SOURCE, + OPENSEARCH_FIELD_TYPES._TYPE, +]; + +export const FieldSelector = () => { + const indexFields = useTypedSelector((state) => state.dataSource.visualizableFields); + const [filteredFields, setFilteredFields] = useState(indexFields); + const fieldSearchValue = useTypedSelector((state) => state.dataSource.searchField); + + useEffect(() => { + const filteredSubset = indexFields.filter((field) => + field.displayName.includes(fieldSearchValue) + ); + + setFilteredFields(filteredSubset); + return; + }, [indexFields, fieldSearchValue]); + + const fields = filteredFields?.reduce( + (fieldGroups, currentField) => { + const category = getFieldCategory(currentField); + fieldGroups[category].push(currentField); + + return fieldGroups; + }, + { + categorical: [], + numerical: [], + meta: [], + } + ); + + return ( +
+
+
+ + +
+
+ + + +
+
+ ); +}; + +interface FieldGroupProps { + fields?: IndexPatternField[]; + header: string; + id: string; +} + +const FieldGroup = ({ fields, header, id }: FieldGroupProps) => ( + <> + + {header} + + } + extraAction={ + + {fields?.length || 0} + + } + initialIsOpen + > + {fields?.map((field, i) => ( + + + + ))} + + + +); + +function getFieldCategory(field: IndexPatternField): keyof IFieldCategories { + if (META_FIELDS.includes(field.name)) return 'meta'; + if (field.type === OSD_FIELD_TYPES.NUMBER) return 'numerical'; + + return 'categorical'; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss new file mode 100644 index 000000000000..0ace9a914b37 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss @@ -0,0 +1,12 @@ +.wizFieldSelectorField { + @include euiBottomShadowSmall; + padding: $euiSizeXS; + background-color: $euiColorEmptyShade; + border: $euiBorderThin; + margin-top: $euiSizeS; + + & > button { + align-items: center; + gap: 4px; + } +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx new file mode 100644 index 000000000000..e545a6b33a63 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { IndexPatternField } from 'src/plugins/data/public'; +import { FieldButton, FieldIcon } from '../../../../../opensearch_dashboards_react/public'; +import { useDrag } from '../../utils/drag_drop/drag_drop_context'; + +import './field_selector_field.scss'; + +export interface FieldSelectorFieldProps { + field: IndexPatternField; +} + +// TODO: +// 1. Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx) +// 2. Add popover for fields stats from discover as well +export const FieldSelectorField = ({ field }: FieldSelectorFieldProps) => { + const [infoIsOpen, setOpen] = useState(false); + const [dragProps] = useDrag(field, `dataPlane`); + + function togglePopover() { + setOpen(!infoIsOpen); + } + + function wrapOnDot(str?: string) { + // u200B is a non-width white-space character, which allows + // the browser to efficiently word-wrap right after the dot + // without us having to draw a lot of extra DOM elements, etc + return str ? str.replace(/\./g, '.\u200B') : ''; + } + + const fieldName = ( + + {wrapOnDot(field.displayName)} + + ); + + return ( + } + // fieldAction={actionButton} + fieldName={fieldName} + {...dragProps} + /> + ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/index.scss b/src/plugins/wizard/public/application/components/data_tab/index.scss new file mode 100644 index 000000000000..1ba02bcc9879 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/index.scss @@ -0,0 +1,7 @@ +@import "../util"; + +.wizDataTab { + @include scrollNavParent; + display: grid; + grid-template-columns: 50% 50%; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/index.tsx b/src/plugins/wizard/public/application/components/data_tab/index.tsx new file mode 100644 index 000000000000..dd062f3a787d --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { FieldSelector } from './field_selector'; +import { ConfigPanel } from './config_panel'; + +import './index.scss'; + +export const DataTab = () => { + return ( +
+ + +
+ ); +}; diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss new file mode 100644 index 000000000000..88ff7ffb0e47 --- /dev/null +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -0,0 +1,18 @@ +@import "util"; + +.wizSidenav { + @include scrollNavParent(auto 1fr); + grid-area: sideNav; + border-right: $euiBorderThin; +} + +.wizDatasourceSelector { + padding: $euiSize $euiSize 0 $euiSize; +} + +.wizSidenavTabs { + @include scrollNavParent(min-content 1fr); + &>[role="tabpanel"] { + @include scrollNavParent; + } +} diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx new file mode 100644 index 000000000000..2f9eab83fad3 --- /dev/null +++ b/src/plugins/wizard/public/application/components/side_nav.tsx @@ -0,0 +1,72 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; + +import { EuiFormLabel, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; + +import { DataTab } from './data_tab'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../types'; +import { StyleTab } from './style_tab'; + +import './side_nav.scss'; +import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; +import { setIndexPattern } from '../utils/state_management/datasource_slice'; + +export const SideNav = () => { + const { + services: { + data, + savedObjects: { client: savedObjectsClient }, + }, + } = useOpenSearchDashboards(); + const { IndexPatternSelect } = data.ui; + const { indexPattern } = useTypedSelector((state) => state.dataSource); + const dispatch = useTypedDispatch(); + + const tabs: EuiTabbedContentTab[] = [ + { + id: 'data-tab', + name: i18n.translate('wizard.nav.dataTab.title', { + defaultMessage: 'Data', + }), + content: , + }, + { + id: 'style-tab', + name: i18n.translate('wizard.nav.styleTab.title', { + defaultMessage: 'Style', + }), + content: , + }, + ]; + + return ( +
+
+ + {i18n.translate('wizard.nav.dataSource.selector.title', { + defaultMessage: 'Index Pattern', + })} + + { + const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); + dispatch(setIndexPattern(newIndexPattern)); + }} + isClearable={false} + /> +
+ +
+ ); +}; diff --git a/src/plugins/wizard/public/application/components/style_tab.tsx b/src/plugins/wizard/public/application/components/style_tab.tsx new file mode 100644 index 000000000000..3d1eb0d98b35 --- /dev/null +++ b/src/plugins/wizard/public/application/components/style_tab.tsx @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +export const StyleTab = () => { + return
TODO: Layout styles come here.
; +}; diff --git a/src/plugins/wizard/public/application/components/top_nav.scss b/src/plugins/wizard/public/application/components/top_nav.scss new file mode 100644 index 000000000000..f8e1d1d6cfa4 --- /dev/null +++ b/src/plugins/wizard/public/application/components/top_nav.scss @@ -0,0 +1,4 @@ +.wizTopNav { + grid-area: topNav; + border-bottom: $euiBorderThin; +} \ No newline at end of file diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx new file mode 100644 index 000000000000..5afa39f7bafd --- /dev/null +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import { PLUGIN_ID } from '../../../common'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { getTopNavconfig } from '../utils/get_top_nav_config'; +import { WizardServices } from '../../types'; + +import './top_nav.scss'; +import { useTypedSelector } from '../utils/state_management'; + +export const TopNav = () => { + const { services } = useOpenSearchDashboards(); + const { + setHeaderActionMenu, + navigation: { + ui: { TopNavMenu }, + }, + } = services; + + const config = useMemo(() => getTopNavconfig(services), [services]); + const { indexPattern } = useTypedSelector((state) => state.dataSource); + + return ( +
+ +
+ ); +}; diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss new file mode 100644 index 000000000000..94a97e881bf6 --- /dev/null +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -0,0 +1,12 @@ +.wizWorkspace { + display: grid; + grid-template-rows: auto 1fr; + grid-area: workspace; + grid-gap: $euiSizeM; + padding: $euiSizeM; + background-color: $euiColorEmptyShade; +} + +.wizWorkspace__empty { + height: 100%; +} diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx new file mode 100644 index 000000000000..a6550a58fb80 --- /dev/null +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -0,0 +1,113 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiPopover, +} from '@elastic/eui'; +import React, { FC, useState, useMemo } from 'react'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../types'; +import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; +import { setActiveVisualization } from '../utils/state_management/visualization_slice'; + +import './workspace.scss'; + +export const Workspace: FC = ({ children }) => { + return ( +
+ + + + + + + {children ? ( + children + ) : ( + + Welcome to the wizard!} + body={

Drag some fields onto the panel to visualize some data.

} + /> +
+ )} +
+
+ ); +}; + +const TypeSelectorPopover = () => { + const [isPopoverOpen, setPopover] = useState(false); + const { activeVisualization: activeVisualizationId } = useTypedSelector( + (state) => state.visualization + ); + const { + services: { types }, + } = useOpenSearchDashboards(); + const dispatch = useTypedDispatch(); + + // TODO: Error if no active visualization + const activeVisualization = types.get(activeVisualizationId || ''); + const visualizationTypes = types.all(); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const panels = useMemo( + () => [ + { + id: 0, + title: 'Chart types', + items: visualizationTypes.map( + ({ name, title, icon, description }): EuiContextMenuPanelItemDescriptor => ({ + name: title, + icon: , + onClick: () => { + closePopover(); + dispatch(setActiveVisualization(name)); + }, + toolTipContent: description, + toolTipPosition: 'right', + }) + ), + }, + ], + [dispatch, visualizationTypes] + ); + + const button = ( + + {activeVisualization?.title} + + ); + + return ( + + + + ); +}; diff --git a/src/plugins/wizard/public/application/index.tsx b/src/plugins/wizard/public/application/index.tsx new file mode 100644 index 000000000000..c451d082b153 --- /dev/null +++ b/src/plugins/wizard/public/application/index.tsx @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { Provider as ReduxProvider } from 'react-redux'; +import { Store } from 'redux'; +import { AppMountParameters } from '../../../../core/public'; +import { WizardServices } from '../types'; +import { WizardApp } from './app'; +import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; + +export const renderApp = ( + { appBasePath, element }: AppMountParameters, + services: WizardServices, + store: Store +) => { + ReactDOM.render( + + + + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/plugins/wizard/public/application/utils/async_search/index.ts b/src/plugins/wizard/public/application/utils/async_search/index.ts new file mode 100644 index 000000000000..9746cde24e4c --- /dev/null +++ b/src/plugins/wizard/public/application/utils/async_search/index.ts @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CreateAggConfigParams } from 'src/plugins/data/common'; +import { DataPublicPluginStart, IndexPattern } from 'src/plugins/data/public'; + +interface IDoAsyncSearch { + data: DataPublicPluginStart; + indexPattern: IndexPattern | null; + aggs?: CreateAggConfigParams[]; +} + +export const doAsyncSearch = async ({ data, indexPattern, aggs }: IDoAsyncSearch) => { + if (!indexPattern || !aggs || !aggs.length) return; + + // Constuct the query portion of the search request + const query = data.query.getOpenSearchQuery(indexPattern); + + // Constuct the aggregations portion of the search request by using the `data.search.aggs` service. + // const aggs = [{ type: 'avg', params: { field: field.name } }]; + // const aggs = [ + // { type: 'terms', params: { field: 'day_of_week' } }, + // { type: 'avg', params: { field: field.name } }, + // { type: 'terms', params: { field: 'customer_gender' } }, + // ]; + const aggConfigs = data.search.aggs.createAggConfigs(indexPattern, aggs); + const aggsDsl = aggConfigs.toDsl(); + + const request = { + params: { + index: indexPattern.title, + body: { + aggs: aggsDsl, + query, + }, + }, + }; + + // Submit the search request using the `data.search` service. + const { rawResponse } = await data.search.search(request).toPromise(); + + return { + rawResponse, + aggConfigs, + }; +}; diff --git a/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx new file mode 100644 index 000000000000..a89226885d5d --- /dev/null +++ b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx @@ -0,0 +1,113 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { createContext, DragEvent, FC, ReactNode, useContext, useState } from 'react'; + +interface DrapDataType { + namespace: string; + value: any; +} + +// TODO: Replace any with corret type +// TODO: Split into separate files +interface IDragDropContext { + data?: DrapDataType; + setData?: any; + isDragging: boolean; + setIsDragging?: any; +} + +const defaultContextProps = { + isDragging: false, +}; + +const DragDropContext = createContext(defaultContextProps); + +const DragDropProvider: FC = ({ children }) => { + const [isDragging, setIsDragging] = useState(false); + const [data, setData] = useState(); + return ( + + {children} + + ); +}; + +const useDragDropContext = () => useContext(DragDropContext); + +const useDrag = (dragData: any, namespace: string) => { + const { setData, setIsDragging } = useDragDropContext(); + const dragElementProps = { + draggable: true, + onDragStart: (event: DragEvent) => { + setIsDragging(true); + setData({ + namespace, + value: dragData, + }); + }, + onDragEnd: (event: DragEvent) => { + setIsDragging(false); + setData(null); + }, + }; + return [dragElementProps]; +}; + +interface IDropAttributes { + onDragOver: (event: DragEvent) => void; + onDrop: (event: DragEvent) => void; + onDragEnter: (event: DragEvent) => void; + onDragLeave: (event: DragEvent) => void; +} + +interface IDropState { + isDragging: boolean; + canDrop: boolean; + isValidDropTarget: boolean; + dragData: any; +} +const useDrop = (namespace: string, onDropCallback: Function): [IDropAttributes, IDropState] => { + const { data, isDragging, setIsDragging, setData } = useDragDropContext(); + const [canDrop, setCanDrop] = useState(false); + + const dropAttributes: IDropAttributes = { + onDragOver: (event) => { + event.preventDefault(); + }, + onDrop: (event) => { + setIsDragging(false); + onDropCallback(data?.value); + setData(null); + }, + onDragEnter: (event) => { + if (data?.namespace === namespace) { + setCanDrop(true); + } + }, + onDragLeave: (event) => { + setCanDrop(false); + }, + }; + return [ + dropAttributes, + { + isDragging, + canDrop, + isValidDropTarget: isDragging && data?.namespace === namespace, + dragData: data?.value, + }, + ]; +}; + +export { DragDropContext, DragDropProvider, useDragDropContext, useDrag, useDrop }; diff --git a/src/plugins/wizard/public/application/utils/drag_drop/index.ts b/src/plugins/wizard/public/application/utils/drag_drop/index.ts new file mode 100644 index 000000000000..3799a2eb6052 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/drag_drop/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './drag_drop_context'; diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx new file mode 100644 index 000000000000..725f7f2baa92 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { TopNavMenuData } from '../../../../navigation/public'; +import { + OnSaveProps, + SavedObjectSaveModalOrigin, + showSaveModal, +} from '../../../../saved_objects/public'; +import { WizardServices } from '../..'; + +export const getTopNavconfig = ({ + savedObjects: { client: savedObjectsClient }, + toastNotifications, + i18n: { Context: I18nContext }, +}: WizardServices) => { + const topNavConfig: TopNavMenuData[] = [ + { + id: 'save', + iconType: 'save', + emphasize: true, + label: 'Save', + testId: 'wizardSaveButton', + run: (anchorElement) => { + const onSave = async ({ + // TODO: Figure out what the other props here do + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + newDescription, + returnToOrigin, + }: OnSaveProps & { returnToOrigin: boolean }) => { + // TODO: Save the actual state of the wizard + const wizardSavedObject = await savedObjectsClient.create('wizard', { + title: newTitle, + description: newDescription, + state: JSON.stringify({}), + }); + + try { + const id = await wizardSavedObject.save(); + + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate( + 'wizard.topNavMenu.saveVisualization.successNotificationText', + { + defaultMessage: `Saved '{visTitle}'`, + values: { + visTitle: newTitle, + }, + } + ), + 'data-test-subj': 'saveVisualizationSuccess', + }); + + return { id }; + } + + throw new Error('Saved but no id returned'); + } catch (error: any) { + // eslint-disable-next-line no-console + console.error(error); + + toastNotifications.addDanger({ + title: i18n.translate( + 'visualize.topNavMenu.saveVisualization.failureNotificationText', + { + defaultMessage: `Error on saving '{visTitle}'`, + values: { + visTitle: newTitle, + }, + } + ), + text: error.message, + 'data-test-subj': 'saveVisualizationError', + }); + return { error }; + } + }; + + const saveModal = ( + {}} + /> + ); + + showSaveModal(saveModal, I18nContext); + }, + }, + ]; + + return topNavConfig; +}; diff --git a/src/plugins/wizard/public/application/utils/state_management/config_slice.ts b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts new file mode 100644 index 000000000000..5d8908596104 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IndexPatternField } from '../../../../../data/public'; + +interface ConfigSections { + [id: string]: { + title: string; + fields: IndexPatternField[]; + }; +} +interface ConfigState { + configSections: ConfigSections; +} + +// TODO: Temp. Remove once visualizations can be refgistered and editor configs can be passed along +// TODO: this is a placeholder while the config section is iorned out +const initialState: ConfigState = { + configSections: { + x: { + title: 'X Axis', + fields: [], + }, + y: { + title: 'Y Axis', + fields: [], + }, + }, +}; + +interface SectionField { + sectionId: string; + field: IndexPatternField; +} + +export const slice = createSlice({ + name: 'configuration', + initialState, + reducers: { + addConfigSectionField: (state, action: PayloadAction) => { + const { field, sectionId } = action.payload; + if (state.configSections[sectionId]) { + state.configSections[sectionId].fields.push(field); + } + }, + removeConfigSectionField: (state, action: PayloadAction) => { + const { field, sectionId } = action.payload; + if (state.configSections[sectionId]) { + const fieldIndex = state.configSections[sectionId].fields.findIndex( + (configField) => configField === field + ); + if (fieldIndex !== -1) state.configSections[sectionId].fields.splice(fieldIndex, 1); + } + }, + }, +}); + +export const { reducer } = slice; +export const { addConfigSectionField, removeConfigSectionField } = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts new file mode 100644 index 000000000000..d51d463d68ee --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IndexPattern } from 'src/plugins/data/common'; +import { WizardServices } from '../../../types'; + +import { IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public'; + +const ALLOWED_FIELDS: string[] = [OSD_FIELD_TYPES.STRING, OSD_FIELD_TYPES.NUMBER]; + +interface DataSourceState { + indexPattern: IndexPattern | null; + visualizableFields: IndexPatternField[]; + searchField: string; +} + +const initialState: DataSourceState = { + indexPattern: null, + visualizableFields: [], + searchField: '', +}; + +export const getPreloadedState = async ({ data }: WizardServices): Promise => { + const preloadedState = { ...initialState }; + + const defaultIndexPattern = await data.indexPatterns.getDefault(); + if (defaultIndexPattern) { + preloadedState.indexPattern = defaultIndexPattern; + preloadedState.visualizableFields = defaultIndexPattern.fields.filter(isVisualizable); + } + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'dataSource', + initialState, + reducers: { + setIndexPattern: (state, action: PayloadAction) => { + state.indexPattern = action.payload; + state.visualizableFields = action.payload.fields.filter(isVisualizable); + }, + setSearchField: (state, action: PayloadAction) => { + state.searchField = action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setIndexPattern, setSearchField } = slice.actions; + +// TODO: Temporary validate function +// Need to identify how to get fieldCounts to use the standard filter and group functions +function isVisualizable(field: IndexPatternField): boolean { + const isAggregatable = field.aggregatable === true; + const isNotScripted = !field.scripted; + const isAllowed = ALLOWED_FIELDS.includes(field.type); + + return isAggregatable && isNotScripted && isAllowed; +} diff --git a/src/plugins/wizard/public/application/utils/state_management/hooks.ts b/src/plugins/wizard/public/application/utils/state_management/hooks.ts new file mode 100644 index 000000000000..823c34528c90 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/hooks.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useTypedDispatch = () => useDispatch(); +export const useTypedSelector: TypedUseSelectorHook = useSelector; diff --git a/src/plugins/wizard/public/application/utils/state_management/index.ts b/src/plugins/wizard/public/application/utils/state_management/index.ts new file mode 100644 index 000000000000..edb5c2a17184 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './store'; +export * from './hooks'; diff --git a/src/plugins/wizard/public/application/utils/state_management/preload.ts b/src/plugins/wizard/public/application/utils/state_management/preload.ts new file mode 100644 index 000000000000..ad78b642c23e --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/preload.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PreloadedState } from '@reduxjs/toolkit'; +import { WizardServices } from '../../..'; +import { getPreloadedState as getPreloadedDatasourceState } from './datasource_slice'; +import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; +import { RootState } from './store'; + +export const getPreloadedState = async ( + services: WizardServices +): Promise> => { + const dataSourceState = await getPreloadedDatasourceState(services); + const visualizationState = await getPreloadedVisualizationState(services); + + return { + dataSource: dataSourceState, + visualization: visualizationState, + }; +}; diff --git a/src/plugins/wizard/public/application/utils/state_management/store.ts b/src/plugins/wizard/public/application/utils/state_management/store.ts new file mode 100644 index 000000000000..4fa56c1a7c97 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; +import { reducer as dataSourceReducer } from './datasource_slice'; +import { reducer as configReducer } from './config_slice'; +import { reducer as visualizationReducer } from './visualization_slice'; +import { WizardServices } from '../../..'; +import { getPreloadedState } from './preload'; + +const rootReducer = combineReducers({ + dataSource: dataSourceReducer, + config: configReducer, + visualization: visualizationReducer, +}); + +export const configurePreloadedStore = (preloadedState: PreloadedState) => { + return configureStore({ + reducer: rootReducer, + preloadedState, + }); +}; + +export const getPreloadedStore = async (services: WizardServices) => { + const preloadedState = await getPreloadedState(services); + return configurePreloadedStore(preloadedState); +}; + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +type Store = ReturnType; +export type AppDispatch = Store['dispatch']; diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts new file mode 100644 index 000000000000..692f9434c8de --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { WizardServices } from '../../../types'; + +interface VisualizationState { + activeVisualization: string | null; +} + +const initialState: VisualizationState = { + activeVisualization: null, +}; + +export const getPreloadedState = async ({ types }: WizardServices): Promise => { + const preloadedState = { ...initialState }; + + const defaultVisualization = types.all()[0]; + if (defaultVisualization) { + preloadedState.activeVisualization = defaultVisualization.name; + } + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'visualization', + initialState, + reducers: { + setActiveVisualization: (state, action: PayloadAction) => { + state.activeVisualization = action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setActiveVisualization } = slice.actions; diff --git a/src/plugins/wizard/public/index.ts b/src/plugins/wizard/public/index.ts new file mode 100644 index 000000000000..97f9007549a0 --- /dev/null +++ b/src/plugins/wizard/public/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginInitializerContext } from '../../../core/public'; +import { WizardPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. +export function plugin(initializerContext: PluginInitializerContext) { + return new WizardPlugin(initializerContext); +} +export { WizardServices, WizardPluginStartDependencies } from './types'; diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts new file mode 100644 index 000000000000..5b309080a872 --- /dev/null +++ b/src/plugins/wizard/public/plugin.ts @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { + AppMountParameters, + AppNavLinkStatus, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '../../../core/public'; +import { + WizardPluginSetupDependencies, + WizardPluginStartDependencies, + WizardServices, + WizardSetup, +} from './types'; +import { PLUGIN_NAME } from '../common'; +import { TypeService } from './services/type_service'; +import { getPreloadedStore } from './application/utils/state_management'; + +export class WizardPlugin + implements + Plugin { + private typeService = new TypeService(); + + constructor(public initializerContext: PluginInitializerContext) {} + + public setup( + core: CoreSetup, + { visualizations }: WizardPluginSetupDependencies + ) { + const typeService = this.typeService; + // Register the plugin to core + core.application.register({ + id: 'wizard', + title: PLUGIN_NAME, + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in opensearch_dashboards.json + const [coreStart, pluginsStart] = await core.getStartServices(); + const { data, savedObjects, navigation } = pluginsStart; + + const { registerDefaultTypes } = await import('./visualizations'); + registerDefaultTypes(typeService.setup()); + + const services: WizardServices = { + ...coreStart, + toastNotifications: coreStart.notifications.toasts, + data, + savedObjectsPublic: savedObjects, + navigation, + setHeaderActionMenu: params.setHeaderActionMenu, + types: typeService.start(), + }; + + // make sure the index pattern list is up to date + data.indexPatterns.clearCache(); + // make sure a default index pattern exists + // if not, the page will be redirected to management and visualize won't be rendered + // TODO: Add the redirect + await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); + + const store = await getPreloadedStore(services); + + // Render the application + return renderApp(params, services, store); + }, + }); + + // Register the plugin as an alias to create visualization + visualizations.registerAlias({ + name: 'wizard', + title: 'Wizard', + description: i18n.translate('wizard.vizPicker.description', { + defaultMessage: 'TODO...', + }), + // TODO: Replace with actual icon once available + icon: 'vector', + stage: 'beta', + aliasApp: 'wizard', + aliasPath: '#/', + }); + + return { + ...typeService.setup(), + }; + } + + public start(core: CoreStart) {} + + public stop() {} +} diff --git a/src/plugins/wizard/public/services/type_service/index.ts b/src/plugins/wizard/public/services/type_service/index.ts new file mode 100644 index 000000000000..1fae953fb9b8 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './type_service'; diff --git a/src/plugins/wizard/public/services/type_service/type_service.ts b/src/plugins/wizard/public/services/type_service/type_service.ts new file mode 100644 index 000000000000..d43d779f75ea --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/type_service.ts @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VisualizationType, VisualizationTypeOptions } from './visualization_type'; + +/** + * Vis Types Service + * + * @internal + */ +export class TypeService { + private types: Record = {}; + + private registerVisualizationType(visDefinition: VisualizationType) { + if (this.types[visDefinition.name]) { + throw new Error('type already exists!'); + } + this.types[visDefinition.name] = visDefinition; + } + + public setup() { + return { + /** + * registers a visualization type + * @param config - visualization type definition + */ + createVisualizationType: (config: VisualizationTypeOptions): void => { + const vis = new VisualizationType(config); + this.registerVisualizationType(vis); + }, + }; + } + + public start() { + return { + /** + * returns specific visualization or undefined if not found + * @param {string} visualization - id of visualization to return + */ + get: (visualization: string): VisualizationType | undefined => { + return this.types[visualization]; + }, + /** + * returns all registered visualization types + */ + all: (): VisualizationType[] => { + return [...Object.values(this.types)]; + }, + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @internal */ +export type TypeServiceSetup = ReturnType; +export type TypeServiceStart = ReturnType; diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.ts b/src/plugins/wizard/public/services/type_service/visualization_type.ts new file mode 100644 index 000000000000..cddb000f41db --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/visualization_type.ts @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IconType } from '@elastic/eui'; + +export interface VisualizationTypeOptions { + readonly name: string; + readonly title: string; + readonly description?: string; + readonly icon: IconType; + readonly stage?: 'beta' | 'production'; + readonly contributions: { + containers?: { + // Define new or override existing view containers + name: string; + title: string; + location: 'panel' | 'toolbar'; + // render: (schemas: ContainerSchema[]) => {}; // recieves an array of items to render within the container + }; + items?: { + 'container-name': any[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency + // 'container-name': ContainerSchema[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency + }; + }; + // pipeline: Expression; +} + +export type IVisualizationType = Required; + +export class VisualizationType implements IVisualizationType { + public readonly name; + public readonly title; + public readonly description; + public readonly icon; + public readonly stage; + public readonly contributions; + + constructor(options: VisualizationTypeOptions) { + this.name = options.name; + this.title = options.title; + this.description = options.description ?? ''; + this.icon = options.icon; + this.stage = options.stage ?? 'production'; + this.contributions = options.contributions; + } +} diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts new file mode 100644 index 000000000000..07b1e5141c61 --- /dev/null +++ b/src/plugins/wizard/public/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsStart } from 'src/plugins/saved_objects/public'; +import { AppMountParameters, CoreStart, ToastsStart } from 'opensearch-dashboards/public'; +import { EmbeddableSetup } from 'src/plugins/embeddable/public'; +import { DashboardStart } from 'src/plugins/dashboard/public'; +import { VisualizationsSetup } from 'src/plugins/visualizations/public'; +import { NavigationPublicPluginStart } from '../../navigation/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; + +export type WizardSetup = TypeServiceSetup; + +export interface WizardPluginSetupDependencies { + embeddable: EmbeddableSetup; + visualizations: VisualizationsSetup; +} +export interface WizardPluginStartDependencies { + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; + savedObjects: SavedObjectsStart; + dashboard: DashboardStart; +} + +export interface WizardServices extends CoreStart { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + toastNotifications: ToastsStart; + savedObjectsPublic: SavedObjectsStart; + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; + types: TypeServiceStart; +} diff --git a/src/plugins/wizard/public/visualizations/bar_chart/index.ts b/src/plugins/wizard/public/visualizations/bar_chart/index.ts new file mode 100644 index 000000000000..cc05f790993f --- /dev/null +++ b/src/plugins/wizard/public/visualizations/bar_chart/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisualizationTypeOptions } from '../../services/type_service/visualization_type'; + +export const createBarChartConfig = (): VisualizationTypeOptions => { + return { + name: 'bar_chart', + title: 'Bar Chart', + icon: 'visBarVertical', + description: 'This is a bar chart', + contributions: {}, + }; +}; diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts new file mode 100644 index 000000000000..604de170c8ab --- /dev/null +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { TypeServiceSetup } from '../services/type_service'; +import { createBarChartConfig } from './bar_chart'; +import { createPieChartConfig } from './pie_chart'; + +export function registerDefaultTypes(typeServieSetup: TypeServiceSetup) { + const visualizationTypes = [createBarChartConfig, createPieChartConfig]; + + visualizationTypes.forEach((createTypeConfig) => { + typeServieSetup.createVisualizationType(createTypeConfig()); + }); +} diff --git a/src/plugins/wizard/public/visualizations/pie_chart/index.ts b/src/plugins/wizard/public/visualizations/pie_chart/index.ts new file mode 100644 index 000000000000..b47965bc1905 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/pie_chart/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisualizationTypeOptions } from '../../services/type_service/visualization_type'; + +export const createPieChartConfig = (): VisualizationTypeOptions => { + return { + name: 'pie_chart', + title: 'Pie Chart', + icon: 'visPie', + contributions: {}, + }; +}; diff --git a/src/plugins/wizard/server/index.ts b/src/plugins/wizard/server/index.ts new file mode 100644 index 000000000000..e995ea17b4a7 --- /dev/null +++ b/src/plugins/wizard/server/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginInitializerContext } from '../../../core/server'; +import { WizardPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new WizardPlugin(initializerContext); +} + +export { WizardPluginSetup, WizardPluginStart } from './types'; diff --git a/src/plugins/wizard/server/plugin.ts b/src/plugins/wizard/server/plugin.ts new file mode 100644 index 000000000000..d45e4081cce9 --- /dev/null +++ b/src/plugins/wizard/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { WizardPluginSetup, WizardPluginStart } from './types'; +import { defineRoutes } from './routes'; +import { wizardApp } from './saved_objects'; + +export class WizardPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup({ http, savedObjects }: CoreSetup) { + this.logger.debug('wizard: Setup'); + const router = http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + // Register saved object types + savedObjects.registerType(wizardApp); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('wizard: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/wizard/server/routes/index.ts b/src/plugins/wizard/server/routes/index.ts new file mode 100644 index 000000000000..f6268695e838 --- /dev/null +++ b/src/plugins/wizard/server/routes/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IRouter } from '../../../../core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/wizard/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/src/plugins/wizard/server/saved_objects/index.ts b/src/plugins/wizard/server/saved_objects/index.ts new file mode 100644 index 000000000000..aa90fcea911b --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { wizardApp } from './wizard_app'; diff --git a/src/plugins/wizard/server/saved_objects/wizard_app.ts b/src/plugins/wizard/server/saved_objects/wizard_app.ts new file mode 100644 index 000000000000..138bea03b22a --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType } from 'src/core/server'; +import { WIZARD_SAVED_OBJECT } from '../../common'; + +export const wizardApp: SavedObjectsType = { + name: WIZARD_SAVED_OBJECT, + hidden: false, + namespaceType: 'single', + management: { + icon: 'visVisualBuilder', // TODO: Need a custom icon here + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: (obj: { attributes: { title: string } }) => obj.attributes.title, + // getInAppUrl: TODO: Enable once editing is supported + }, + migrations: {}, + mappings: { + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + // TODO: Determine what needs to be pulled out of state and added directly into the mapping + state: { + type: 'text', + index: false, + }, + }, + }, +}; diff --git a/src/plugins/wizard/server/types.ts b/src/plugins/wizard/server/types.ts new file mode 100644 index 000000000000..5d26185a0374 --- /dev/null +++ b/src/plugins/wizard/server/types.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WizardPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface WizardPluginStart {} diff --git a/yarn.lock b/yarn.lock index 5804cdfa640c..ef19a747860b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2544,6 +2544,16 @@ colors "~1.2.1" string-argv "~0.3.1" +"@reduxjs/toolkit@^1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37" + integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA== + dependencies: + immer "^9.0.6" + redux "^4.1.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -14983,6 +14993,13 @@ redux@^4.0.0, redux@^4.0.4, redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" +redux@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" + integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" From 50685e1509f659babf2db936780f6965b54bd07e Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Mon, 2 May 2022 18:48:11 -0700 Subject: [PATCH 02/47] [D&D] Type Service Contributions (#1402) * feat(Contributions): Simple working contributions Signed-off-by: Ashwin Pc * feat(Contributions): Add new field from dropbox Signed-off-by: Ashwin Pc * feat(Contributions): Working edit mode Signed-off-by: Ashwin Pc * fix(Contributions): Fixes switching dropbox fields Signed-off-by: Ashwin Pc * chore: Updates license headers Signed-off-by: Ashwin Pc * feat(Contributions): Adds initial drag & drop validation Signed-off-by: Ashwin Pc * feat(Dropbox): Allows multiple instances of the same field Signed-off-by: Ashwin Pc * feat(Dropbox): Working reorder fields Signed-off-by: Ashwin Pc * feat(Contributions): Working container contributions Signed-off-by: Ashwin Pc * chore: clean up exports Signed-off-by: Ashwin Pc * test(Contribution): Adds unit test `mergeArrays` Signed-off-by: Ashwin Pc * test(TypeService): Adds plugin unit tests Signed-off-by: Ashwin Pc * chore: remove redundant code Signed-off-by: Ashwin Pc * test(Functional): Updates Chart types Signed-off-by: Ashwin Pc --- src/plugins/wizard/opensearch_dashboards.json | 3 +- .../application/{components => }/_util.scss | 0 .../wizard/public/application/app.scss | 4 +- .../components/data_tab/config_panel.scss | 9 - .../components/data_tab/config_panel.tsx | 31 --- .../components/data_tab/config_section.scss | 23 --- .../components/data_tab/config_section.tsx | 83 -------- .../components/data_tab/field_selector.scss | 10 - .../application/components/side_nav.scss | 2 +- .../application/components/side_nav.tsx | 30 +-- .../application/components/workspace.scss | 6 +- .../application/components/workspace.tsx | 10 +- .../application/contributions/constants.ts | 12 ++ .../containers/common/items/index.tsx | 9 + .../containers/common/items/select.tsx | 47 +++++ .../containers/common/items/text_input.tsx | 37 ++++ .../containers/common/items/types.ts | 43 ++++ .../containers/data_tab/config_panel.scss | 34 ++++ .../containers/data_tab/config_panel.tsx | 91 +++++++++ .../containers}/data_tab/field_search.tsx | 4 +- .../containers/data_tab/field_selector.scss | 22 +++ .../containers}/data_tab/field_selector.tsx | 48 +++-- .../data_tab/field_selector_field.scss | 0 .../data_tab/field_selector_field.tsx | 16 +- .../containers}/data_tab/index.scss | 2 +- .../containers}/data_tab/index.tsx | 6 +- .../containers/data_tab/items/dropbox.scss | 55 ++++++ .../containers/data_tab/items/dropbox.tsx | 113 +++++++++++ .../containers/data_tab/items/form_field.tsx | 20 ++ .../containers/data_tab/items/index.tsx | 9 + .../containers/data_tab/items/title.tsx | 56 ++++++ .../containers/data_tab/items/types.ts | 67 +++++++ .../containers/data_tab/items/use/index.ts | 7 + .../data_tab/items/use/use_dropbox.tsx | 182 +++++++++++++++++ .../data_tab/items/use/use_form_field.tsx | 77 ++++++++ .../data_tab/utils/item_to_panel.tsx | 60 ++++++ .../contributions/containers/index.ts | 7 + .../containers/style_tab/index.tsx} | 2 + .../public/application/contributions/index.ts | 6 + .../utils/drag_drop/drag_drop_context.tsx | 78 +++++--- .../application/utils/drag_drop/types.ts | 17 ++ .../utils/state_management/config_slice.ts | 184 ++++++++++++++---- .../utils/state_management/preload.ts | 3 + .../public/application/utils/use/index.ts | 6 + .../utils/use/use_visualization_type.ts | 24 +++ src/plugins/wizard/public/plugin.test.ts | 44 +++++ .../public/services/type_service/index.ts | 1 + .../type_service/type_service.test.ts | 67 +++++++ .../services/type_service/type_service.ts | 10 +- .../public/services/type_service/types.ts | 41 ++++ .../services/type_service/utils/index.ts | 6 + .../type_service/utils/merge_array.test.ts | 24 +++ .../type_service/utils/merge_arrays.ts | 39 ++++ .../type_service/visualization_type.test.tsx | 95 +++++++++ .../type_service/visualization_type.ts | 48 ----- .../type_service/visualization_type.tsx | 77 ++++++++ .../public/visualizations/bar_chart/index.ts | 80 +++++++- .../wizard/public/visualizations/index.ts | 4 +- .../public/visualizations/pie_chart/index.ts | 2 +- .../functional/apps/visualize/_chart_types.ts | 1 + 60 files changed, 1751 insertions(+), 343 deletions(-) rename src/plugins/wizard/public/application/{components => }/_util.scss (100%) delete mode 100644 src/plugins/wizard/public/application/components/data_tab/config_panel.scss delete mode 100644 src/plugins/wizard/public/application/components/data_tab/config_panel.tsx delete mode 100644 src/plugins/wizard/public/application/components/data_tab/config_section.scss delete mode 100644 src/plugins/wizard/public/application/components/data_tab/config_section.tsx delete mode 100644 src/plugins/wizard/public/application/components/data_tab/field_selector.scss create mode 100644 src/plugins/wizard/public/application/contributions/constants.ts create mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/types.ts create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx rename src/plugins/wizard/public/application/{components => contributions/containers}/data_tab/field_search.tsx (89%) create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.scss rename src/plugins/wizard/public/application/{components => contributions/containers}/data_tab/field_selector.tsx (80%) rename src/plugins/wizard/public/application/{components => contributions/containers}/data_tab/field_selector_field.scss (100%) rename src/plugins/wizard/public/application/{components => contributions/containers}/data_tab/field_selector_field.tsx (85%) rename src/plugins/wizard/public/application/{components => contributions/containers}/data_tab/index.scss (79%) rename src/plugins/wizard/public/application/{components => contributions/containers}/data_tab/index.tsx (84%) create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/index.ts create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/index.ts rename src/plugins/wizard/public/application/{components/style_tab.tsx => contributions/containers/style_tab/index.tsx} (82%) create mode 100644 src/plugins/wizard/public/application/contributions/index.ts create mode 100644 src/plugins/wizard/public/application/utils/drag_drop/types.ts create mode 100644 src/plugins/wizard/public/application/utils/use/index.ts create mode 100644 src/plugins/wizard/public/application/utils/use/use_visualization_type.ts create mode 100644 src/plugins/wizard/public/plugin.test.ts create mode 100644 src/plugins/wizard/public/services/type_service/type_service.test.ts create mode 100644 src/plugins/wizard/public/services/type_service/types.ts create mode 100644 src/plugins/wizard/public/services/type_service/utils/index.ts create mode 100644 src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts create mode 100644 src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts create mode 100644 src/plugins/wizard/public/services/type_service/visualization_type.test.tsx delete mode 100644 src/plugins/wizard/public/services/type_service/visualization_type.ts create mode 100644 src/plugins/wizard/public/services/type_service/visualization_type.tsx diff --git a/src/plugins/wizard/opensearch_dashboards.json b/src/plugins/wizard/opensearch_dashboards.json index 8dd00aae3890..5d4402106e0c 100644 --- a/src/plugins/wizard/opensearch_dashboards.json +++ b/src/plugins/wizard/opensearch_dashboards.json @@ -11,7 +11,8 @@ "savedObjects", "embeddable", "dashboard", - "visualizations" + "visualizations", + "opensearchUiShared" ], "optionalPlugins": [] } diff --git a/src/plugins/wizard/public/application/components/_util.scss b/src/plugins/wizard/public/application/_util.scss similarity index 100% rename from src/plugins/wizard/public/application/components/_util.scss rename to src/plugins/wizard/public/application/_util.scss diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss index 2e1e93f44312..4f05c6ecd7a2 100644 --- a/src/plugins/wizard/public/application/app.scss +++ b/src/plugins/wizard/public/application/app.scss @@ -4,10 +4,10 @@ padding: 0; display: grid; grid-template-rows: min-content 1fr; - grid-template-columns: 420px 1fr; + grid-template-columns: 470px 1fr; grid-template-areas: "topNav topNav" "sideNav workspace" ; - height: calc(100vh - #{$osdHeaderOffset}); // TODO: update 190px to correct offset variable + height: calc(100vh - #{$osdHeaderOffset}); } diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.scss b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss deleted file mode 100644 index 7477dcfca813..000000000000 --- a/src/plugins/wizard/public/application/components/data_tab/config_panel.scss +++ /dev/null @@ -1,9 +0,0 @@ -.wizConfigPanel { - background: #f0f1f3; - border-left: $euiBorderThin; - padding: $euiSizeS; -} - -.wizConfigPanel__title { - margin-left: $euiSizeS; -} diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx deleted file mode 100644 index ec910b7352de..000000000000 --- a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { EuiForm, EuiTitle } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@osd/i18n'; -import { ConfigSection } from './config_section'; - -import './config_panel.scss'; -import { useTypedSelector } from '../../utils/state_management'; - -export function ConfigPanel() { - const { configSections } = useTypedSelector((state) => state.config); - - return ( - - -

- {i18n.translate('wizard.nav.dataTab.configPanel.title', { - defaultMessage: 'Configuration', - })} -

-
- {Object.entries(configSections).map(([sectionId, sectionProps], index) => ( - - ))} -
- ); -} diff --git a/src/plugins/wizard/public/application/components/data_tab/config_section.scss b/src/plugins/wizard/public/application/components/data_tab/config_section.scss deleted file mode 100644 index 79d0d3a913fd..000000000000 --- a/src/plugins/wizard/public/application/components/data_tab/config_section.scss +++ /dev/null @@ -1,23 +0,0 @@ -.wizConfigSection { - margin-top: $euiSize; - border-bottom: $euiBorderThin; - padding-bottom: $euiSize; - - &:last-child { - border-bottom: none; - } - - & .euiFormRow__labelWrapper { - margin-left: $euiSizeS; - } -} - -.wizConfigSection__dropTarget { - @include euiSlightShadow; - background: $euiColorEmptyShade; - border: $euiBorderThin; - box-shadow: 0px 2px 2px rgba(152, 162, 179, 0.15); - border-radius: $euiBorderRadius; - padding: $euiSizeS $euiSizeM; - color: $euiColorDarkShade; -} diff --git a/src/plugins/wizard/public/application/components/data_tab/config_section.tsx b/src/plugins/wizard/public/application/components/data_tab/config_section.tsx deleted file mode 100644 index 64f74824d71a..000000000000 --- a/src/plugins/wizard/public/application/components/data_tab/config_section.tsx +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { EuiButtonIcon, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; -import { i18n } from '@osd/i18n'; -import React, { useCallback } from 'react'; -import { IndexPatternField } from 'src/plugins/data/common'; -import { useDrop } from '../../utils/drag_drop'; -import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; -import { - addConfigSectionField, - removeConfigSectionField, -} from '../../utils/state_management/config_slice'; - -import './config_section.scss'; - -interface ConfigSectionProps { - id: string; - title: string; -} - -export const ConfigSection = ({ title, id }: ConfigSectionProps) => { - const dispatch = useTypedDispatch(); - const { fields } = useTypedSelector((state) => state.config.configSections[id]); - - const dropHandler = useCallback( - (field: IndexPatternField) => { - dispatch( - addConfigSectionField({ - sectionId: id, - field, - }) - ); - }, - [dispatch, id] - ); - const [dropProps, { isValidDropTarget, dragData }] = useDrop('dataPlane', dropHandler); - - const dropTargetString = dragData - ? dragData.type - : i18n.translate('wizard.nav.dataTab.configPanel.dropTarget.placeholder', { - defaultMessage: 'Click or drop to add', - }); - - return ( -
- -

{title}

-
- {fields.length ? ( - fields.map((field, index) => ( - - - {field.displayName} - - - dispatch( - removeConfigSectionField({ - sectionId: id, - field, - }) - ) - } - /> - - )) - ) : ( -
- {dropTargetString} -
- )} -
- ); -}; diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss deleted file mode 100644 index c05f75457b0b..000000000000 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss +++ /dev/null @@ -1,10 +0,0 @@ -@import "../util"; - -.wizFieldSelector { - @include scrollNavParent(auto 1fr); - padding: $euiSizeS; -} - -.wizFieldSelector__fieldGroups { - overflow-y: auto; -} diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss index 88ff7ffb0e47..8da4b26d20e6 100644 --- a/src/plugins/wizard/public/application/components/side_nav.scss +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -1,4 +1,4 @@ -@import "util"; +@import "../util"; .wizSidenav { @include scrollNavParent(auto 1fr); diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx index 2f9eab83fad3..3bb1f1d76618 100644 --- a/src/plugins/wizard/public/application/components/side_nav.tsx +++ b/src/plugins/wizard/public/application/components/side_nav.tsx @@ -5,17 +5,13 @@ import React from 'react'; import { i18n } from '@osd/i18n'; - import { EuiFormLabel, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; - -import { DataTab } from './data_tab'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../types'; -import { StyleTab } from './style_tab'; - import './side_nav.scss'; import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; import { setIndexPattern } from '../utils/state_management/datasource_slice'; +import { useVisualizationType } from '../utils/use'; export const SideNav = () => { const { @@ -27,23 +23,15 @@ export const SideNav = () => { const { IndexPatternSelect } = data.ui; const { indexPattern } = useTypedSelector((state) => state.dataSource); const dispatch = useTypedDispatch(); + const { + contributions: { containers }, + } = useVisualizationType(); - const tabs: EuiTabbedContentTab[] = [ - { - id: 'data-tab', - name: i18n.translate('wizard.nav.dataTab.title', { - defaultMessage: 'Data', - }), - content: , - }, - { - id: 'style-tab', - name: i18n.translate('wizard.nav.styleTab.title', { - defaultMessage: 'Style', - }), - content: , - }, - ]; + const tabs: EuiTabbedContentTab[] = containers.sidePanel.map(({ id, name, Component }) => ({ + id, + name, + content: Component, + })); return (
diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss index 94a97e881bf6..58bcd6f1e7cc 100644 --- a/src/plugins/wizard/public/application/components/workspace.scss +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -5,8 +5,8 @@ grid-gap: $euiSizeM; padding: $euiSizeM; background-color: $euiColorEmptyShade; -} -.wizWorkspace__empty { - height: 100%; + &__empty { + height: 100%; + } } diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index a6550a58fb80..6715c7fdf7c5 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -17,8 +17,9 @@ import { import React, { FC, useState, useMemo } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../types'; -import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; +import { useTypedDispatch } from '../utils/state_management'; import { setActiveVisualization } from '../utils/state_management/visualization_slice'; +import { useVisualizationType } from '../utils/use'; import './workspace.scss'; @@ -49,17 +50,12 @@ export const Workspace: FC = ({ children }) => { const TypeSelectorPopover = () => { const [isPopoverOpen, setPopover] = useState(false); - const { activeVisualization: activeVisualizationId } = useTypedSelector( - (state) => state.visualization - ); const { services: { types }, } = useOpenSearchDashboards(); const dispatch = useTypedDispatch(); - - // TODO: Error if no active visualization - const activeVisualization = types.get(activeVisualizationId || ''); const visualizationTypes = types.all(); + const activeVisualization = useVisualizationType(); const onButtonClick = () => { setPopover(!isPopoverOpen); diff --git a/src/plugins/wizard/public/application/contributions/constants.ts b/src/plugins/wizard/public/application/contributions/constants.ts new file mode 100644 index 000000000000..0a581cabec81 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/constants.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonItemTypes } from './containers/common/items'; +import { DataTabItemTypes } from './containers/data_tab/items'; + +export const ItemTypes = { + ...CommonItemTypes, + ...DataTabItemTypes, +}; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx b/src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx new file mode 100644 index 000000000000..eb081eb2932a --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; + +export { Select } from './select'; +export { TextInput } from './text_input'; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx b/src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx new file mode 100644 index 000000000000..cc6c0060c5b0 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo } from 'react'; +import { EuiFormRow, EuiSuperSelect } from '@elastic/eui'; +import { WizardServices } from 'src/plugins/wizard/public'; +import { useOpenSearchDashboards } from '../../../../../../../opensearch_dashboards_react/public'; +import { useTypedSelector } from '../../../../utils/state_management'; +import { SelectContribution } from './types'; + +interface SelectProps extends Omit, 'type'> { + value: string; +} + +export const Select = ({ label, options, onChange, value, ...rest }: SelectProps) => { + const rootState = useTypedSelector((state) => state); + const { services } = useOpenSearchDashboards(); + const selectOptions = useMemo( + () => (typeof options === 'function' ? options(rootState, services) : options), + [options, rootState, services] + ); + // const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + { + onChange?.(newValue); + }} + // isInvalid={isInvalid} + valueOfSelected={value || ''} + data-test-subj="select" + options={selectOptions} + /> + + ); +}; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx b/src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx new file mode 100644 index 000000000000..91d34a16f072 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { InputContribution } from './types'; + +interface InputProps extends Omit { + value: string; +} + +export const TextInput = ({ label, onChange, value, ...rest }: InputProps) => { + // const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + return ( + + { + onChange?.(event.target.value); + }} + // isInvalid={isInvalid} + value={value || ''} + data-test-subj="text_input" + /> + + ); +}; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/types.ts b/src/plugins/wizard/public/application/contributions/containers/common/items/types.ts new file mode 100644 index 000000000000..1761a4de2000 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/common/items/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiSuperSelectProps } from '@elastic/eui'; +import { WizardServices } from 'src/plugins/wizard/public'; +import { RootState } from '../../../../utils/state_management'; + +/** + * Types for contributions shared across various panels + */ +export enum ITEM_TYPES { + SELECT = 'select', + INPUT = 'input', +} + +export const CommonItemTypes = { + ...ITEM_TYPES, +}; + +export interface SelectContribution { + type: ITEM_TYPES.SELECT; + id: string; + label: string; + options: + | EuiSuperSelectProps['options'] + | ((state: RootState, services: WizardServices) => EuiSuperSelectProps['options']); + onChange?: (option: string) => void; + 'data-test-subj'?: string; + idAria?: string; +} + +export interface InputContribution { + type: ITEM_TYPES.INPUT; + id: string; + label: string; + onChange?: (value: string) => void; + 'data-test-subj'?: string; + idAria?: string; +} + +export type CommonItemContribution = SelectContribution | InputContribution; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss new file mode 100644 index 000000000000..90dd89f1e3bb --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss @@ -0,0 +1,34 @@ +.wizConfig { + background: $euiColorLightestShade; + border-left: $euiBorderThin; + position: relative; + overflow-x: hidden; + + &__section { + width: 100%; + transition: transform $euiAnimSpeedNormal 0s $euiAnimSlightResistance; + } + + &__title { + padding: $euiSizeS; + padding-bottom: 0; + + &.showDivider { + border-bottom: 1px solid $euiColorLightShade; + } + } + + &__content { + padding: $euiSizeS; + } + + &--secondary { + position: absolute; + top: 0; + left: 100%; + } + + &.showSecondary > .wizConfig__section { + transform: translateX(-100%); + } +} \ No newline at end of file diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx new file mode 100644 index 000000000000..ad1f49a7ff26 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx @@ -0,0 +1,91 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiForm } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useVisualizationType } from '../../../utils/use'; +import { + DropboxContribution, + MainItemContribution, + SecondaryItemContribution, + TitleItemContribution, + ITEM_TYPES, +} from './items'; +import { useTypedSelector } from '../../../utils/state_management'; +import { mapItemToPanelComponents } from './utils/item_to_panel'; +import { ItemTypes } from '../../constants'; +import { SelectContribution } from '../common/items'; +import { INDEX_FIELD_KEY } from './items/use/use_form_field'; +import { DATA_TAB_ID } from '.'; +import './config_panel.scss'; + +const DEFAULT_ITEMS: MainItemContribution[] = [getTitleContribution()]; + +export function ConfigPanel() { + const { + contributions: { items }, + } = useVisualizationType(); + const activeItem = useTypedSelector((state) => state.config.activeItem); + const configItemState = useTypedSelector((state) => state.config.items[activeItem?.id || '']); + + const hydratedItems: MainItemContribution[] = [...(items?.[DATA_TAB_ID] ?? []), ...DEFAULT_ITEMS]; + + const mainPanel = useMemo(() => mapItemToPanelComponents(hydratedItems), [hydratedItems]); + const secondaryPanel = useMemo(() => { + if (!activeItem || !configItemState || typeof configItemState === 'string') return; + + // Generate each secondary panel base on active item type + if (activeItem.type === ITEM_TYPES.DROPBOX) { + const activeDropboxContribution = hydratedItems.find( + (item: MainItemContribution) => + item.type === ITEM_TYPES.DROPBOX && item?.id === activeItem?.id + ) as DropboxContribution | undefined; + + if (!activeDropboxContribution) return null; + + let itemsToRender: SecondaryItemContribution[] = [ + getTitleContribution(activeDropboxContribution.label), + getFieldSelectorContribution(), + ]; + + const dropboxFieldInstance = configItemState.instances.find( + ({ id }) => id === activeItem.instanceId + ); + if (dropboxFieldInstance && dropboxFieldInstance.properties.fieldName) { + itemsToRender = [...itemsToRender, ...activeDropboxContribution.items]; + } + + return mapItemToPanelComponents(itemsToRender, true); + } + }, [activeItem, configItemState, hydratedItems]); + + return ( + +
{mainPanel}
+
{secondaryPanel}
+
+ ); +} + +function getTitleContribution(title?: string): TitleItemContribution { + return { + type: ITEM_TYPES.TITLE, + title: [title, 'Configuration'].join(' '), + }; +} + +function getFieldSelectorContribution(): SelectContribution { + return { + type: ItemTypes.SELECT, + id: INDEX_FIELD_KEY, + label: 'Select a Field', + options: (state) => { + return state.dataSource.visualizableFields.map((field) => ({ + value: field.name, + inputDisplay: field.displayName, + })); + }, + }; +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_search.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx similarity index 89% rename from src/plugins/wizard/public/application/components/data_tab/field_search.tsx rename to src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx index 2db8404c93c6..d086cfc1f362 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_search.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { i18n } from '@osd/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { setSearchField } from '../../utils/state_management/datasource_slice'; -import { useTypedDispatch } from '../../utils/state_management'; +import { setSearchField } from '../../../utils/state_management/datasource_slice'; +import { useTypedDispatch } from '../../../utils/state_management'; export interface Props { /** diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.scss b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.scss new file mode 100644 index 000000000000..e2c44387126a --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.scss @@ -0,0 +1,22 @@ +@import "../../../util"; + +.wizFieldSelector { + @include scrollNavParent(auto 1fr); + padding: $euiSizeS; + + &__fieldGroups { + @include euiYScrollWithShadows; + overflow-y: auto; + margin-right: -$euiSizeS; + padding-right: $euiSizeS; + margin-top: $euiSizeS; + } + + &__fieldGroup { + margin-top: $euiSizeS; + + &:first-child { + margin-top: 0; + } + } +} diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx similarity index 80% rename from src/plugins/wizard/public/application/components/data_tab/field_selector.tsx rename to src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx index 1464f31aabd9..9d1771fde2b5 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx @@ -11,11 +11,11 @@ import { IndexPatternField, OPENSEARCH_FIELD_TYPES, OSD_FIELD_TYPES, -} from '../../../../../data/public'; +} from '../../../../../../data/public'; import { FieldSelectorField } from './field_selector_field'; import './field_selector.scss'; -import { useTypedSelector } from '../../utils/state_management'; +import { useTypedSelector } from '../../../utils/state_management'; interface IFieldCategories { categorical: IndexPatternField[]; @@ -85,29 +85,27 @@ interface FieldGroupProps { } const FieldGroup = ({ fields, header, id }: FieldGroupProps) => ( - <> - - {header} - - } - extraAction={ - - {fields?.length || 0} - - } - initialIsOpen - > - {fields?.map((field, i) => ( - - - - ))} - - - + + {header} + + } + extraAction={ + + {fields?.length || 0} + + } + initialIsOpen + > + {fields?.map((field, i) => ( + + + + ))} + ); function getFieldCategory(field: IndexPatternField): keyof IFieldCategories { diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.scss similarity index 100% rename from src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss rename to src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.scss diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.tsx similarity index 85% rename from src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx rename to src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.tsx index e545a6b33a63..f000f9395280 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.tsx @@ -30,8 +30,8 @@ import React, { useState } from 'react'; import { IndexPatternField } from 'src/plugins/data/public'; -import { FieldButton, FieldIcon } from '../../../../../opensearch_dashboards_react/public'; -import { useDrag } from '../../utils/drag_drop/drag_drop_context'; +import { FieldButton, FieldIcon } from '../../../../../../opensearch_dashboards_react/public'; +import { useDrag } from '../../../utils/drag_drop/drag_drop_context'; import './field_selector_field.scss'; @@ -39,12 +39,22 @@ export interface FieldSelectorFieldProps { field: IndexPatternField; } +export type FieldDragData = Pick; + // TODO: // 1. Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx) // 2. Add popover for fields stats from discover as well export const FieldSelectorField = ({ field }: FieldSelectorFieldProps) => { + const { displayName, type, name } = field; const [infoIsOpen, setOpen] = useState(false); - const [dragProps] = useDrag(field, `dataPlane`); + const [dragProps] = useDrag({ + namespace: 'field-data', + value: { + displayName, + name, + type, + }, + }); function togglePopover() { setOpen(!infoIsOpen); diff --git a/src/plugins/wizard/public/application/components/data_tab/index.scss b/src/plugins/wizard/public/application/contributions/containers/data_tab/index.scss similarity index 79% rename from src/plugins/wizard/public/application/components/data_tab/index.scss rename to src/plugins/wizard/public/application/contributions/containers/data_tab/index.scss index 1ba02bcc9879..773a12538430 100644 --- a/src/plugins/wizard/public/application/components/data_tab/index.scss +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/index.scss @@ -1,4 +1,4 @@ -@import "../util"; +@import "../../../util"; .wizDataTab { @include scrollNavParent; diff --git a/src/plugins/wizard/public/application/components/data_tab/index.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/index.tsx similarity index 84% rename from src/plugins/wizard/public/application/components/data_tab/index.tsx rename to src/plugins/wizard/public/application/contributions/containers/data_tab/index.tsx index dd062f3a787d..9fefef7d8ce9 100644 --- a/src/plugins/wizard/public/application/components/data_tab/index.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/index.tsx @@ -5,9 +5,11 @@ import React from 'react'; import { FieldSelector } from './field_selector'; -import { ConfigPanel } from './config_panel'; import './index.scss'; +import { ConfigPanel } from './config_panel'; + +export const DATA_TAB_ID = 'data_tab'; export const DataTab = () => { return ( @@ -17,3 +19,5 @@ export const DataTab = () => { ); }; + +export * from './items'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss new file mode 100644 index 000000000000..b81ab2a07b33 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss @@ -0,0 +1,55 @@ +.dropBox { + margin-top: $euiSize; + border-bottom: $euiBorderThin; + padding-bottom: $euiSize; + + &:first-child { + margin-top: 0; + } + + &:last-child { + border-bottom: none; + } + + &__container { + display: grid; + grid-gap: $euiSizeXS; + padding: $euiSizeS; + background-color: #E9EDF3; + border-radius: $euiBorderRadius; + } + + &__field { + display: grid; + grid-template-columns: auto 1fr auto; + grid-gap: $euiSizeS; + padding: $euiSizeS $euiSizeM; + align-items: center; + } + + &__draggable { + padding: 2px 0; + } + + &__field_text { + text-overflow: ellipsis; + overflow: hidden; + } + + &__dropTarget { + color: $euiColorDarkShade; + grid-template-columns: 1fr auto; + + &.validField { + background-color: #A8D9E7; + border-color: #A8D9E7; + + &.canDrop { + background-color: rgba(0, 161, 201, 0.3); + border-color: #006BB4; + border-style: dashed; + } + } + + } +} \ No newline at end of file diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx new file mode 100644 index 000000000000..a9acf0c9ad71 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx @@ -0,0 +1,113 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFormRow, + EuiPanel, + EuiText, + euiDragDropReorder, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { FieldIcon } from '../../../../../../../opensearch_dashboards_react/public'; +import { IDropAttributes, IDropState } from '../../../../utils/drag_drop'; +import './dropbox.scss'; +import { DropboxContribution, DropboxDisplay } from './types'; +import { useDropbox } from './use'; + +interface DropboxProps extends IDropState { + id: string; + label: string; + fields: DropboxDisplay[]; + limit?: number; + onAddField: () => void; + onEditField: (id: string) => void; + onDeleteField: (id: string) => void; + onReorderField: (reorderedIds: string[]) => void; + dropProps: IDropAttributes; +} + +const DropboxComponent = ({ + id: dropboxId, + label: boxLabel, + fields, + onAddField, + onDeleteField, + onEditField, + onReorderField, + limit = 1, + isValidDropTarget, + canDrop, + dropProps, +}: DropboxProps) => { + const handleDragEnd = useCallback( + ({ source, destination }) => { + if (!source || !destination) return; + + const instanceIds = fields.map(({ id }) => id); + const reorderedIds = euiDragDropReorder(instanceIds, source.index, destination.index); + + onReorderField(reorderedIds); + }, + [fields, onReorderField] + ); + + return ( + + +
+ + {fields.map(({ id, label, icon }, index) => ( + + + + onEditField(id)}> + + {label} + + + onDeleteField(id)} + /> + + + ))} + + {fields.length < limit && ( + + Click or drop to add + onAddField()} + /> + + )} +
+
+
+ ); +}; + +const Dropbox = React.memo((dropBox: DropboxContribution) => { + const props = useDropbox(dropBox); + + return ; +}); + +export { Dropbox, DropboxComponent, DropboxProps }; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx new file mode 100644 index 000000000000..d068474eb267 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React, { ComponentType } from 'react'; +import { Select, TextInput, ITEM_TYPES } from '../../common/items'; +import { FieldContributions } from './types'; +import { useFormField } from './use'; + +const mapItemToFormFieldComponent: { [key in ITEM_TYPES]: ComponentType } = { + [ITEM_TYPES.SELECT]: Select, + [ITEM_TYPES.INPUT]: TextInput, +}; + +export const FormField = ({ type, id, onChange, ...props }: FieldContributions) => { + const FieldComponent = mapItemToFormFieldComponent[type]; + const hookProps = useFormField(id, onChange); + + return ; +}; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx new file mode 100644 index 000000000000..72a3ad62df58 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './types'; +export * from './dropbox'; +export { Title } from './title'; +export { FormField } from './form_field'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx new file mode 100644 index 000000000000..84720ae6ff1c --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx @@ -0,0 +1,56 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { TitleItemContribution } from './types'; +import { useTypedDispatch } from '../../../../utils/state_management'; +import { setActiveItem } from '../../../../utils/state_management/config_slice'; + +export interface TitleProps { + title: string; + icon?: React.ReactNode; + showDivider?: boolean; +} + +export const TitleComponent = ({ title, icon, showDivider = false }: TitleProps) => ( + <> +
+ + {icon && {icon}} + + +

{title}

+
+
+
+
+ {showDivider ? : } + +); + +interface TitleContributionProps extends TitleItemContribution { + isSecondary?: boolean; +} + +export const Title = ({ title, isSecondary }: TitleContributionProps) => { + const dispatch = useTypedDispatch(); + + return ( + dispatch(setActiveItem(null))} /> + } + showDivider={isSecondary} + /> + ); +}; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts new file mode 100644 index 000000000000..3133fd4f39be --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPatternField } from 'src/plugins/data/common'; +import { FieldIconProps } from '../../../../../../../opensearch_dashboards_react/public'; +import { SelectContribution, InputContribution } from '../../common/items'; + +/** + * Types for contributions that relate to the config panel + */ + +export enum ITEM_TYPES { + DROPBOX = 'dropbox', + TITLE = 'title', +} + +export const DataTabItemTypes = { + ...ITEM_TYPES, +}; + +export type FieldContributions = SelectContribution | InputContribution; +export type MainItemContribution = TitleItemContribution | DropboxContribution | FieldContributions; +export type SecondaryItemContribution = TitleItemContribution | FieldContributions; + +export interface TitleItemContribution { + type: ITEM_TYPES.TITLE; + title: string; +} + +export interface DropboxDisplay { + label: string; + icon: FieldIconProps['type']; + id: string; +} +export interface DropboxFieldProps { + fieldName?: string; + [itemId: string]: any; +} + +export interface DropboxContribution { + type: ITEM_TYPES.DROPBOX; + id: string; + label: string; + limit?: number; + items: SecondaryItemContribution[]; + // Define how the IndexPatternField should be displayed on the dropbox + display?: ( + indexField: IndexPatternField, + state: DropboxFieldProps + ) => Pick; + // Defines how the initial state of a field should be set when a field is dropped onto it + onDrop?: (field: IndexPatternField) => DropboxFieldProps; + isDroppable?: (field: IndexPatternField) => boolean; +} + +export interface InstanceState { + instances: Array<{ + id: string; + properties: T; + }>; +} + +export type DropboxState = InstanceState; +export type InstanceItemStates = DropboxState; +export type ConfigItemState = InstanceItemStates | string | undefined; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/index.ts b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/index.ts new file mode 100644 index 000000000000..87b259c5a86e --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useDropbox } from './use_dropbox'; +export { useFormField } from './use_form_field'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx new file mode 100644 index 000000000000..250825f60d10 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx @@ -0,0 +1,182 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useCallback, useMemo } from 'react'; +import { IndexPatternField } from 'src/plugins/data/common'; +import { FieldDragDataType } from '../../../../../utils/drag_drop/types'; +import { useTypedDispatch, useTypedSelector } from '../../../../../utils/state_management'; +import { + addInstance, + reorderInstances, + setActiveItem, + updateInstance, +} from '../../../../../utils/state_management/config_slice'; +import { + DropboxContribution, + DropboxState, + ITEM_TYPES, + DropboxDisplay, + DropboxFieldProps, +} from '../types'; +import { DropboxProps } from '../dropbox'; +import { useDrop } from '../../../../../utils/drag_drop'; + +type DropboxInstanceState = DropboxState['instances'][number]; + +export const INITIAL_STATE: DropboxState = { + instances: [], +}; + +export const useDropbox = (dropboxContribution: DropboxContribution): DropboxProps => { + const { id: dropboxId, label, limit, display, onDrop, isDroppable } = dropboxContribution; + const dispatch = useTypedDispatch(); + const { items, availableFields } = useTypedSelector((state) => ({ + items: state.config.items, + availableFields: state.dataSource.visualizableFields, + })); + const configItemState = items[dropboxId]; + const dropboxState = + !configItemState || typeof configItemState === 'string' ? INITIAL_STATE : configItemState; + const filterPatrialInstances = useCallback( + ({ properties }: DropboxInstanceState) => !!properties.fieldName, + [] + ); + const mapInstanceToFieldDisplay = useCallback( + ({ id, properties }: DropboxInstanceState): DropboxDisplay => { + const indexPatternField = availableFields.find(({ name }) => name === properties.fieldName); + + if (!indexPatternField) throw new Error('Field to display missing in available fields'); + + return getDisplayField(id, indexPatternField, properties, display); + }, + [availableFields, display] + ); + + const displayFields: DropboxDisplay[] = useMemo( + () => dropboxState.instances.filter(filterPatrialInstances).map(mapInstanceToFieldDisplay), + [dropboxState.instances, filterPatrialInstances, mapInstanceToFieldDisplay] + ); + + // Event handlers for each dropbox action type + const onAddField = useCallback(() => { + dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX)); + }, [dispatch, dropboxId]); + + const onEditField = useCallback( + (instanceId) => { + dispatch( + setActiveItem({ + id: dropboxId, + type: ITEM_TYPES.DROPBOX, + instanceId, + }) + ); + }, + [dispatch, dropboxId] + ); + + const onDeleteField = useCallback( + (instanceId) => { + dispatch( + updateInstance({ + id: dropboxId, + instanceId, + instanceState: null, + }) + ); + }, + [dispatch, dropboxId] + ); + + const onDropField = useCallback( + (data: FieldDragDataType['value']) => { + if (!data) return; + + const { name: fieldName } = data; + const indexField = getIndexPatternField(fieldName, availableFields); + + if (!indexField) return; + + if (isDroppable && !isDroppable(indexField)) return; + + const newState: DropboxFieldProps = { + ...onDrop?.(indexField), + fieldName, + }; + + dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX, false, newState)); + }, + [availableFields, isDroppable, onDrop, dispatch, dropboxId] + ); + + const onReorderField = useCallback( + (reorderedInstanceIds: string[]) => { + dispatch( + reorderInstances({ + id: dropboxId, + reorderedInstanceIds, + }) + ); + }, + [dispatch, dropboxId] + ); + + const [dropProps, { isValidDropTarget, dragData, ...dropState }] = useDrop( + 'field-data', + onDropField + ); + + const isValidDropField = useMemo(() => { + if (!dragData) return false; + + const indexField = getIndexPatternField(dragData.name, availableFields); + + if (!indexField) return false; + + return isValidDropTarget && (isDroppable?.(indexField) ?? true); + }, [availableFields, dragData, isDroppable, isValidDropTarget]); + + return { + id: dropboxId, + label, + limit, + fields: displayFields, + onAddField, + onEditField, + onDeleteField, + onReorderField, + ...dropState, + dragData, + isValidDropTarget: isValidDropField, + dropProps, + }; +}; + +const getDisplayField = ( + instanceId: string, + indexField: IndexPatternField, + properties: DropboxFieldProps, + display: DropboxContribution['display'] +): DropboxDisplay => { + let displayField: DropboxDisplay = { + id: instanceId, + icon: indexField.type, + label: indexField.displayName, + }; + if (display) { + const { icon, label } = display(indexField, properties); + + displayField = { + ...displayField, + icon, + label, + }; + } + + return displayField; +}; + +const getIndexPatternField = (indexFieldName: string, availableFields: IndexPatternField[]) => + availableFields.find(({ name }) => name === indexFieldName); diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx new file mode 100644 index 000000000000..8f650afb62f0 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx @@ -0,0 +1,77 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import produce from 'immer'; +import { useCallback, useMemo } from 'react'; +import { + ConfigState, + updateConfigItemState, + updateInstance, +} from '../../../../../utils/state_management/config_slice'; +import { useTypedSelector, useTypedDispatch } from '../../../../../utils/state_management'; +import { FieldContributions } from '../types'; + +export const INDEX_FIELD_KEY = 'fieldName'; + +interface FieldProps { + onChange: Function; + value: string; +} + +export const useFormField = (id: string, onChange: FieldContributions['onChange']): FieldProps => { + const configState = useTypedSelector((state) => state.config); + const { activeItem, items } = configState; + const dispatch = useTypedDispatch(); + + const instanceState = useMemo(() => getInstanceState(configState) ?? {}, [configState]); + + const handleChange = useCallback( + (newValue: string) => { + onChange?.(newValue); + + // is a MainPanel field value + if (!activeItem) { + dispatch( + updateConfigItemState({ + id, + itemState: newValue, + }) + ); + return; + } + + const newInstanceState = produce(instanceState, (draftState) => { + draftState[id] = newValue; + }); + + dispatch( + updateInstance({ + id: activeItem.id, + instanceId: activeItem.instanceId, + instanceState: newInstanceState, + }) + ); + }, + [activeItem, dispatch, id, instanceState, onChange] + ); + + return { + value: activeItem ? instanceState[id] : items[id], + onChange: handleChange, + }; +}; + +function getInstanceState({ items, activeItem }: ConfigState) { + const { id: parentItemId, instanceId } = activeItem ?? {}; + const configItem = items[parentItemId ?? '']; + + if (!configItem || typeof configItem === 'string') return; + + const instanceItem = configItem.instances.find(({ id }) => id === instanceId); + + if (!instanceItem) return; + + return instanceItem.properties; +} diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx new file mode 100644 index 000000000000..db9dfdaf7dee --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { ItemTypes } from '../../../constants'; +import { + Title, + Dropbox, + FormField, + MainItemContribution, + SecondaryItemContribution, +} from '../items'; + +export const mapItemToPanelComponents = ( + items: Array, + isSecondary = false +) => { + const uniqueDict: { [key: string]: boolean } = {}; + + const [title, ...panelComponents] = items + .filter((item) => { + // Ensure unique item ID + const id = item.type !== ItemTypes.TITLE ? item.id : 'title'; + if (uniqueDict[id]) return false; + + uniqueDict[id] = true; + return true; + }) + .sort((itemA, itemB) => + // Ensure that the title is on top + itemA.type === ItemTypes.TITLE ? -1 : itemB.type === ItemTypes.TITLE ? 1 : 0 + ) + .map((item, index) => { + const { type } = item; + + switch (type) { + case ItemTypes.TITLE: + return ; + + case ItemTypes.DROPBOX: + return <Dropbox key={item.id} {...item} />; + + case ItemTypes.SELECT: + case ItemTypes.INPUT: + return <FormField key={item.id} {...item} />; + + default: + break; + } + }); + + return ( + <> + {title} + <div className="wizConfig__content">{panelComponents}</div> + </> + ); +}; diff --git a/src/plugins/wizard/public/application/contributions/containers/index.ts b/src/plugins/wizard/public/application/contributions/containers/index.ts new file mode 100644 index 000000000000..74d0a6b8f76e --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './data_tab'; +export * from './style_tab'; diff --git a/src/plugins/wizard/public/application/components/style_tab.tsx b/src/plugins/wizard/public/application/contributions/containers/style_tab/index.tsx similarity index 82% rename from src/plugins/wizard/public/application/components/style_tab.tsx rename to src/plugins/wizard/public/application/contributions/containers/style_tab/index.tsx index 3d1eb0d98b35..6da05f6715e7 100644 --- a/src/plugins/wizard/public/application/components/style_tab.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/style_tab/index.tsx @@ -5,6 +5,8 @@ import React from 'react'; +export const STYLE_TAB_ID = 'style_tab'; + export const StyleTab = () => { return <div>TODO: Layout styles come here.</div>; }; diff --git a/src/plugins/wizard/public/application/contributions/index.ts b/src/plugins/wizard/public/application/contributions/index.ts new file mode 100644 index 000000000000..6ea0ec832393 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export * from './containers'; +export * from './constants'; diff --git a/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx index a89226885d5d..c0f8725a501a 100644 --- a/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx +++ b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx @@ -3,31 +3,41 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { createContext, DragEvent, FC, ReactNode, useContext, useState } from 'react'; - -interface DrapDataType { - namespace: string; - value: any; -} +import React, { + createContext, + DragEvent, + FC, + ReactNode, + useContext, + useEffect, + useState, +} from 'react'; +import { DragDataType } from './types'; // TODO: Replace any with corret type // TODO: Split into separate files interface IDragDropContext { - data?: DrapDataType; - setData?: any; + data: DragDataType; + setData?: (data: DragDataType) => void; isDragging: boolean; setIsDragging?: any; } -const defaultContextProps = { +const EMPTY_DATA: DragDataType = { + namespace: null, + value: null, +}; + +const defaultContextProps: IDragDropContext = { isDragging: false, + data: EMPTY_DATA, }; const DragDropContext = createContext<IDragDropContext>(defaultContextProps); const DragDropProvider: FC<ReactNode> = ({ children }) => { const [isDragging, setIsDragging] = useState(false); - const [data, setData] = useState<DrapDataType>(); + const [data, setData] = useState<DragDataType>(EMPTY_DATA); return ( <DragDropContext.Provider value={{ @@ -45,41 +55,44 @@ const DragDropProvider: FC<ReactNode> = ({ children }) => { const useDragDropContext = () => useContext(DragDropContext); -const useDrag = (dragData: any, namespace: string) => { +function useDrag(dragData: DragDataType) { const { setData, setIsDragging } = useDragDropContext(); const dragElementProps = { draggable: true, onDragStart: (event: DragEvent) => { setIsDragging(true); - setData({ - namespace, - value: dragData, - }); + setData!(dragData); }, onDragEnd: (event: DragEvent) => { setIsDragging(false); - setData(null); + setData!({ + namespace: null, + value: null, + }); }, }; return [dragElementProps]; -}; +} -interface IDropAttributes { +export interface IDropAttributes { onDragOver: (event: DragEvent) => void; onDrop: (event: DragEvent) => void; onDragEnter: (event: DragEvent) => void; onDragLeave: (event: DragEvent) => void; } -interface IDropState { +export interface IDropState { isDragging: boolean; canDrop: boolean; isValidDropTarget: boolean; - dragData: any; + dragData: DragDataType['value']; } -const useDrop = (namespace: string, onDropCallback: Function): [IDropAttributes, IDropState] => { +const useDrop = ( + namespace: DragDataType['namespace'], + onDropCallback: (data: DragDataType['value']) => void +): [IDropAttributes, IDropState] => { const { data, isDragging, setIsDragging, setData } = useDragDropContext(); - const [canDrop, setCanDrop] = useState(false); + const [canDrop, setCanDrop] = useState(0); const dropAttributes: IDropAttributes = { onDragOver: (event) => { @@ -87,25 +100,34 @@ const useDrop = (namespace: string, onDropCallback: Function): [IDropAttributes, }, onDrop: (event) => { setIsDragging(false); - onDropCallback(data?.value); - setData(null); + setCanDrop(0); + onDropCallback(data.value); + setData!({ + namespace: null, + value: null, + }); }, onDragEnter: (event) => { if (data?.namespace === namespace) { - setCanDrop(true); + setCanDrop((state) => state + 1); } }, onDragLeave: (event) => { - setCanDrop(false); + setCanDrop((state) => state - 1); }, }; + + useEffect(() => { + if (!isDragging) setCanDrop(0); + }, [isDragging]); + return [ dropAttributes, { isDragging, - canDrop, + canDrop: canDrop > 0, isValidDropTarget: isDragging && data?.namespace === namespace, - dragData: data?.value, + dragData: data.value, }, ]; }; diff --git a/src/plugins/wizard/public/application/utils/drag_drop/types.ts b/src/plugins/wizard/public/application/utils/drag_drop/types.ts new file mode 100644 index 000000000000..0258a2315ec6 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/drag_drop/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPatternField } from 'src/plugins/data/common'; + +export interface EmptyDragDataType { + namespace: null; + value: null; +} +export interface FieldDragDataType { + namespace: 'field-data'; + value: Pick<IndexPatternField, 'name' | 'displayName' | 'type'> | null; +} + +export type DragDataType = EmptyDragDataType | FieldDragDataType; diff --git a/src/plugins/wizard/public/application/utils/state_management/config_slice.ts b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts index 5d8908596104..dc0c2ec1cb0c 100644 --- a/src/plugins/wizard/public/application/utils/state_management/config_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts @@ -3,60 +3,174 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { htmlIdGenerator } from '@elastic/eui'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { IndexPatternField } from '../../../../../data/public'; +import { WizardServices } from '../../../types'; +import { + ConfigItemState, + DATA_TAB_ID, + InstanceState, + MainItemContribution, +} from '../../contributions'; -interface ConfigSections { - [id: string]: { - title: string; - fields: IndexPatternField[]; - }; +// TODO: Move this into contributions and register the slice from there for better code splitting +// TODO: Reorganize slice for better readability +export interface ActiveItem { + id: string; + type: MainItemContribution['type']; + instanceId: string; } -interface ConfigState { - configSections: ConfigSections; + +export interface ConfigState { + items: { + [id: string]: ConfigItemState; + }; + activeItem: ActiveItem | null; } -// TODO: Temp. Remove once visualizations can be refgistered and editor configs can be passed along -// TODO: this is a placeholder while the config section is iorned out const initialState: ConfigState = { - configSections: { - x: { - title: 'X Axis', - fields: [], - }, - y: { - title: 'Y Axis', - fields: [], - }, - }, + items: {}, + activeItem: null, }; -interface SectionField { - sectionId: string; - field: IndexPatternField; +export const getPreloadedState = async ({ types }: WizardServices): Promise<ConfigState> => { + const preloadedState = { ...initialState }; + + const defaultVisualizationType = types.all()[0]; + + if (defaultVisualizationType) { + preloadedState.items = defaultVisualizationType.contributions.items?.[DATA_TAB_ID].filter( + ({ id }) => !!id + ).reduce((acc, { id, type }) => ({ ...acc, [id]: null }), {}); + } + + return preloadedState; +}; + +interface UpdateConfigPayload { + id: string; + itemState: any; +} + +interface AddInstancePayload extends ActiveItem { + properties?: any; + setActive: boolean; +} + +interface UpdateInstancePayload { + id: string; + instanceId: string; + instanceState: any; +} + +interface ReorderInstancePayload { + id: string; + reorderedInstanceIds: string[]; } export const slice = createSlice({ name: 'configuration', initialState, reducers: { - addConfigSectionField: (state, action: PayloadAction<SectionField>) => { - const { field, sectionId } = action.payload; - if (state.configSections[sectionId]) { - state.configSections[sectionId].fields.push(field); + updateConfigItemState: (state, action: PayloadAction<UpdateConfigPayload>) => { + const { id, itemState } = action.payload; + + if (state.items.hasOwnProperty(id)) { + state.items[id] = itemState; } }, - removeConfigSectionField: (state, action: PayloadAction<SectionField>) => { - const { field, sectionId } = action.payload; - if (state.configSections[sectionId]) { - const fieldIndex = state.configSections[sectionId].fields.findIndex( - (configField) => configField === field - ); - if (fieldIndex !== -1) state.configSections[sectionId].fields.splice(fieldIndex, 1); + setActiveItem: (state, { payload }: PayloadAction<ActiveItem | null>) => { + // On closing secondary menu + if (!payload) { + state.activeItem = null; + return; } + + state.activeItem = payload; + }, + addInstance: { + reducer: (state, action: PayloadAction<AddInstancePayload>) => { + const { id, instanceId, properties, setActive } = action.payload; + + if (!state.items.hasOwnProperty(id)) return; + + if (!state.items[id]) { + state.items[id] = { + instances: [], + }; + } + + (state.items[id] as InstanceState<unknown>).instances.push({ + id: instanceId, + properties: properties ?? {}, + }); + + if (setActive) { + state.activeItem = action.payload; + } + }, + prepare: ( + id: string, + type: MainItemContribution['type'], + setActive: boolean = true, + properties?: any + ) => { + const instanceId = htmlIdGenerator()(); + + return { + payload: { + id, + type, + instanceId, + properties, + setActive, + }, + }; + }, + }, + updateInstance: (state, action: PayloadAction<UpdateInstancePayload>) => { + const { id: parentItemId, instanceId, instanceState } = action.payload; + if (!state.items.hasOwnProperty(parentItemId)) return; + + // Typescript complains if we use state.items[parentItemId] directly since it cannot resolve the type correctly + const configItem = state.items[parentItemId]; + if (!configItem || typeof configItem === 'string') return; + + const instanceIndex = configItem.instances.findIndex(({ id }) => id === instanceId); + + if (instanceIndex < 0) return; + + if (instanceState === null) { + configItem.instances.splice(instanceIndex, 1); + return; + } + + configItem.instances[instanceIndex].properties = instanceState; + }, + reorderInstances: (state, action: PayloadAction<ReorderInstancePayload>) => { + const { id: parentItemId, reorderedInstanceIds } = action.payload; + + if (!state.items.hasOwnProperty(parentItemId)) return; + + // Typescript complains if we use state.items[parentItemId] directly since it cannot resolve the type correctly + const configItem = state.items[parentItemId]; + if (!configItem || typeof configItem === 'string') return; + + const orderDict: { [id: string]: number } = {}; + reorderedInstanceIds.forEach((instanceId, index) => { + orderDict[instanceId] = index; + }); + + configItem.instances.sort((a, b) => orderDict[a.id] - orderDict[b.id]); }, }, }); export const { reducer } = slice; -export const { addConfigSectionField, removeConfigSectionField } = slice.actions; +export const { + updateConfigItemState, + setActiveItem, + addInstance, + updateInstance, + reorderInstances, +} = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/state_management/preload.ts b/src/plugins/wizard/public/application/utils/state_management/preload.ts index ad78b642c23e..21ebd13ff82f 100644 --- a/src/plugins/wizard/public/application/utils/state_management/preload.ts +++ b/src/plugins/wizard/public/application/utils/state_management/preload.ts @@ -7,6 +7,7 @@ import { PreloadedState } from '@reduxjs/toolkit'; import { WizardServices } from '../../..'; import { getPreloadedState as getPreloadedDatasourceState } from './datasource_slice'; import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; +import { getPreloadedState as getPreloadedConfigState } from './config_slice'; import { RootState } from './store'; export const getPreloadedState = async ( @@ -14,9 +15,11 @@ export const getPreloadedState = async ( ): Promise<PreloadedState<RootState>> => { const dataSourceState = await getPreloadedDatasourceState(services); const visualizationState = await getPreloadedVisualizationState(services); + const configState = await getPreloadedConfigState(services); return { dataSource: dataSourceState, visualization: visualizationState, + config: configState, }; }; diff --git a/src/plugins/wizard/public/application/utils/use/index.ts b/src/plugins/wizard/public/application/utils/use/index.ts new file mode 100644 index 000000000000..d82ba978902d --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useVisualizationType } from './use_visualization_type'; diff --git a/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts b/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts new file mode 100644 index 000000000000..fb88c11b49da --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { VisualizationType } from '../../../services/type_service/visualization_type'; +import { WizardServices } from '../../../types'; +import { useTypedSelector } from '../state_management'; + +export const useVisualizationType = (): VisualizationType => { + const { activeVisualization } = useTypedSelector((state) => state.visualization); + const { + services: { types }, + } = useOpenSearchDashboards<WizardServices>(); + + const visualizationType = types.get(activeVisualization || ''); + + if (!visualizationType) { + throw new Error('Invalid visualization type ${activeVisualization}'); + } + + return visualizationType; +}; diff --git a/src/plugins/wizard/public/plugin.test.ts b/src/plugins/wizard/public/plugin.test.ts new file mode 100644 index 000000000000..c83f4c6a8458 --- /dev/null +++ b/src/plugins/wizard/public/plugin.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreMock, savedObjectsServiceMock } from '../../../core/public/mocks'; +import { dashboardPluginMock } from '../../../plugins/dashboard/public/mocks'; +import { dataPluginMock } from '../../../plugins/data/public/mocks'; +import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks'; +import { navigationPluginMock } from '../../../plugins/navigation/public/mocks'; +import { visualizationsPluginMock } from '../../../plugins/visualizations/public/mocks'; +import { WizardPlugin } from './plugin'; + +describe('WizardPlugin', () => { + describe('setup', () => { + it('initializes the plugin correctly and registers it as an alias vizualization', () => { + const plugin = new WizardPlugin(coreMock.createPluginInitializerContext()); + const pluginStartContract = { + data: dataPluginMock.createStartContract(), + savedObject: savedObjectsServiceMock.createStartContract(), + navigation: navigationPluginMock.createStartContract(), + dashboard: dashboardPluginMock.createStartContract(), + }; + const coreSetup = coreMock.createSetup({ + pluginStartContract, + }) as any; + const setupDeps = { + visualizations: visualizationsPluginMock.createSetupContract(), + embeddable: embeddablePluginMock.createSetupContract(), + }; + + const setup = plugin.setup(coreSetup, setupDeps); + expect(setup).toHaveProperty('createVisualizationType'); + expect(setupDeps.visualizations.registerAlias).toHaveBeenCalledWith( + // TODO: Update this once the properties are final + expect.objectContaining({ + name: 'wizard', + title: 'Wizard', + aliasPath: '#/', + }) + ); + }); + }); +}); diff --git a/src/plugins/wizard/public/services/type_service/index.ts b/src/plugins/wizard/public/services/type_service/index.ts index 1fae953fb9b8..9baeb6dbc9f4 100644 --- a/src/plugins/wizard/public/services/type_service/index.ts +++ b/src/plugins/wizard/public/services/type_service/index.ts @@ -4,3 +4,4 @@ */ export * from './type_service'; +export * from './types'; diff --git a/src/plugins/wizard/public/services/type_service/type_service.test.ts b/src/plugins/wizard/public/services/type_service/type_service.test.ts new file mode 100644 index 000000000000..7edf38e7e5e0 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/type_service.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisualizationTypeOptions } from './types'; +import { TypeService } from './type_service'; + +const DEFAULT_VIZ_PROPS = { + name: 'some-name', + icon: 'some-icon', + title: 'Some Title', + contributions: {}, +}; + +describe('TypeService', () => { + const createVizType = (props?: Partial<VisualizationTypeOptions>): VisualizationTypeOptions => { + return { + ...DEFAULT_VIZ_PROPS, + ...props, + }; + }; + + let service: TypeService; + + beforeEach(() => { + service = new TypeService(); + }); + + describe('#setup', () => { + test('should throw an error if two visualizzations of the same id are registered', () => { + const { createVisualizationType } = service.setup(); + + createVisualizationType(createVizType({ name: 'viz-type-1' })); + + expect(() => { + createVisualizationType(createVizType({ name: 'viz-type-1' })); + }).toThrowErrorMatchingInlineSnapshot( + `"A visualization with this the name viz-type-1 already exists!"` + ); + }); + }); + + describe('#start', () => { + test('should return registered visualization if it exists', () => { + const { createVisualizationType } = service.setup(); + createVisualizationType(createVizType({ name: 'viz-type-1' })); + + const { get } = service.start(); + expect(get('viz-type-1')).toEqual(expect.objectContaining({ name: 'viz-type-1' })); + expect(get('viz-type-no-exists')).toBeUndefined(); + }); + + test('should return all registered visualizations', () => { + const { createVisualizationType } = service.setup(); + createVisualizationType(createVizType({ name: 'viz-type-1' })); + createVisualizationType(createVizType({ name: 'viz-type-2' })); + + const { all } = service.start(); + const allRegisteredVisualizations = all(); + expect(allRegisteredVisualizations.map(({ name }) => name)).toEqual([ + 'viz-type-1', + 'viz-type-2', + ]); + }); + }); +}); diff --git a/src/plugins/wizard/public/services/type_service/type_service.ts b/src/plugins/wizard/public/services/type_service/type_service.ts index d43d779f75ea..38c13ebcfb3c 100644 --- a/src/plugins/wizard/public/services/type_service/type_service.ts +++ b/src/plugins/wizard/public/services/type_service/type_service.ts @@ -28,19 +28,21 @@ * under the License. */ -import { VisualizationType, VisualizationTypeOptions } from './visualization_type'; +import { CoreService } from 'src/core/types'; +import { VisualizationTypeOptions } from './types'; +import { VisualizationType } from './visualization_type'; /** - * Vis Types Service + * Visualization Types Service * * @internal */ -export class TypeService { +export class TypeService implements CoreService<TypeServiceSetup, TypeServiceStart> { private types: Record<string, VisualizationType> = {}; private registerVisualizationType(visDefinition: VisualizationType) { if (this.types[visDefinition.name]) { - throw new Error('type already exists!'); + throw new Error(`A visualization with this the name ${visDefinition.name} already exists!`); } this.types[visDefinition.name] = visDefinition; } diff --git a/src/plugins/wizard/public/services/type_service/types.ts b/src/plugins/wizard/public/services/type_service/types.ts new file mode 100644 index 000000000000..d722bf90dbfa --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IconType } from '@elastic/eui'; + +export enum ContributionTypes { + CONTAINER = 'CONTAINER', + ITEM = 'ITEM', +} + +export enum ContainerLocations { + SIDE_PANEL = 'sidePanel', + TOOLBAR = 'toolbar', +} + +export interface ContainerContribution { + id: string; + name: string; + Component: JSX.Element; +} + +type ContainerSchema = any; + +export type ContainerLocationContribution = { [K in ContainerLocations]: ContainerContribution[] }; + +export interface VisualizationTypeOptions { + readonly name: string; + readonly title: string; + readonly description?: string; + readonly icon: IconType; + readonly stage?: 'beta' | 'production'; + readonly contributions: { + containers?: Partial<ContainerLocationContribution>; + items?: { + [containerId: string]: ContainerSchema[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency + }; + }; + // pipeline: Expression; +} diff --git a/src/plugins/wizard/public/services/type_service/utils/index.ts b/src/plugins/wizard/public/services/type_service/utils/index.ts new file mode 100644 index 000000000000..4d0644b2d93e --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/utils/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './merge_arrays'; diff --git a/src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts b/src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts new file mode 100644 index 000000000000..01a95f240d5f --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { mergeArrays } from './merge_arrays'; + +describe('mergeArrays', () => { + const arrayA = ['a', 'b', 'c'].map((x) => ({ id: x, value: `${x} in arrayA` })); + const arrayB = ['a', 'c', 'd'].map((x) => ({ id: x, value: `${x} in arrayB` })); + test('should merge two object arrays based on id in order without duplicates', () => { + const mergedArrays = mergeArrays(arrayA, arrayB, 'id'); + expect(mergedArrays.map((x) => x.id)).toEqual(['a', 'b', 'c', 'd']); + expect(mergedArrays[0].value).toEqual('a in arrayB'); + }); + + test('should throw an error if key is not a string or number', () => { + const arr = [{ id: {} }]; + + expect(() => mergeArrays(arrayA, arr, 'id')).toThrowErrorMatchingInlineSnapshot( + `"Can only merge arrays with keys of type number or string"` + ); + }); +}); diff --git a/src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts b/src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts new file mode 100644 index 000000000000..3b5f9b0adfb8 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { uniq } from 'lodash'; + +export function mergeArrays<T extends object, K extends keyof T>(a: T[], b: T[], key: K): T[] { + const dict: Record<PropertyKey, T> = {}; + + const rawCombinedArrays = [...a, ...b]; + + // Create an ordered list of unique the array keys. Removing duplicates while keeping the order + const combinedOrder = uniq(rawCombinedArrays.map((entry) => entry[key])); + + // Create a map of all unique ID's and their values. + // If there is more than one entry with the same ID, the last entry is kept + rawCombinedArrays.forEach((entry) => { + if (!entry.hasOwnProperty(key)) { + throw new Error('Key not present in an object in one the arrays to merge'); + } + + const id = entry[key]; + + if (typeof id !== 'string' && typeof id !== 'number') { + throw new Error('Can only merge arrays with keys of type number or string'); + } + + dict[id] = entry; + }); + + // Return the combined array in order with unique keys keeping only the last entry for each unique id + return combinedOrder.map((entryId) => { + if (typeof entryId !== 'string' && typeof entryId !== 'number') { + throw new Error('Can only merge arrays with keys of type number or string'); + } + return dict[entryId]; + }); +} diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx new file mode 100644 index 000000000000..830d598af754 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { VisualizationTypeOptions } from './types'; +import { VisualizationType, DEFAULT_CONTAINERS } from './visualization_type'; + +describe('VisualizationType', () => { + const DEFAULT_VIZ_PROPS = { + name: 'some-name', + icon: 'some-icon', + title: 'Some Title', + contributions: {}, + }; + + const createVizType = (props?: Partial<VisualizationTypeOptions>): VisualizationTypeOptions => { + return { + ...DEFAULT_VIZ_PROPS, + ...props, + }; + }; + + test('should have default container contributions if none are provided', () => { + const viz = new VisualizationType(createVizType()); + + expect(viz.contributions.containers).toEqual(DEFAULT_CONTAINERS); + }); + + test('should have replace default container contributions when provided', () => { + const defaultContainer = DEFAULT_CONTAINERS.sidePanel[0]; + const viz = new VisualizationType( + createVizType({ + contributions: { + containers: { + sidePanel: [ + { + id: defaultContainer.id, + name: 'Test', + Component: <div>Test</div>, + }, + ], + }, + }, + }) + ); + + const container = viz.contributions.containers.sidePanel.find( + ({ id }) => id === defaultContainer.id + ); + expect(container).toMatchInlineSnapshot(` + Object { + "Component": <div> + Test + </div>, + "id": "data_tab", + "name": "Test", + } + `); + }); + + test('should register new container if provided', () => { + const viz = new VisualizationType( + createVizType({ + contributions: { + containers: { + sidePanel: [ + { + id: 'test_id', + name: 'Test', + Component: <div>Test</div>, + }, + ], + }, + }, + }) + ); + + const container = viz.contributions.containers.sidePanel.find(({ id }) => id === 'test_id'); + const containerNames = viz.contributions.containers.sidePanel.map(({ name }) => name); + const defaultContainerNames = DEFAULT_CONTAINERS.sidePanel.map(({ name }) => name); + + expect(containerNames).toEqual([...defaultContainerNames, 'Test']); + expect(container).toMatchInlineSnapshot(` + Object { + "Component": <div> + Test + </div>, + "id": "test_id", + "name": "Test", + } + `); + }); +}); diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.ts b/src/plugins/wizard/public/services/type_service/visualization_type.ts deleted file mode 100644 index cddb000f41db..000000000000 --- a/src/plugins/wizard/public/services/type_service/visualization_type.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { IconType } from '@elastic/eui'; - -export interface VisualizationTypeOptions { - readonly name: string; - readonly title: string; - readonly description?: string; - readonly icon: IconType; - readonly stage?: 'beta' | 'production'; - readonly contributions: { - containers?: { - // Define new or override existing view containers - name: string; - title: string; - location: 'panel' | 'toolbar'; - // render: (schemas: ContainerSchema[]) => {}; // recieves an array of items to render within the container - }; - items?: { - 'container-name': any[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency - // 'container-name': ContainerSchema[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency - }; - }; - // pipeline: Expression; -} - -export type IVisualizationType = Required<VisualizationTypeOptions>; - -export class VisualizationType implements IVisualizationType { - public readonly name; - public readonly title; - public readonly description; - public readonly icon; - public readonly stage; - public readonly contributions; - - constructor(options: VisualizationTypeOptions) { - this.name = options.name; - this.title = options.title; - this.description = options.description ?? ''; - this.icon = options.icon; - this.stage = options.stage ?? 'production'; - this.contributions = options.contributions; - } -} diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.tsx new file mode 100644 index 000000000000..873931db7e89 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -0,0 +1,77 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { DATA_TAB_ID, DataTab, STYLE_TAB_ID, StyleTab } from '../../application/contributions'; +import { + ContainerLocationContribution, + ContainerLocations, + VisualizationTypeOptions, +} from './types'; +import { mergeArrays } from './utils'; + +export const DEFAULT_CONTAINERS: ContainerLocationContribution = { + sidePanel: [ + { + id: DATA_TAB_ID, + name: 'Data', + Component: <DataTab />, + }, + { + id: STYLE_TAB_ID, + name: 'Style', + Component: <StyleTab />, + }, + ], + toolbar: [], +}; + +interface IVisualizationType extends Required<VisualizationTypeOptions> { + contributions: { + containers: ContainerLocationContribution; + }; +} +export class VisualizationType implements IVisualizationType { + public readonly name; + public readonly title; + public readonly description; + public readonly icon; + public readonly stage; + public readonly contributions; + + private processContributions(contributions: VisualizationTypeOptions['contributions']) { + const uiContainers: ContainerLocationContribution = { + sidePanel: [], + toolbar: [], + }; + const { containers, items } = contributions; + + // Validate and populate containers for each container location + Object.keys(uiContainers).forEach((location) => { + const typedLocation = location as ContainerLocations; + const vizContainers = containers?.[typedLocation]; + + const mergedContainers = mergeArrays( + DEFAULT_CONTAINERS[typedLocation], + vizContainers || [], + 'id' + ); + uiContainers[typedLocation] = mergedContainers; + }); + + return { + containers: uiContainers, + items, + }; + } + + constructor(options: VisualizationTypeOptions) { + this.name = options.name; + this.title = options.title; + this.description = options.description ?? ''; + this.icon = options.icon; + this.stage = options.stage ?? 'production'; + this.contributions = this.processContributions(options.contributions); + } +} diff --git a/src/plugins/wizard/public/visualizations/bar_chart/index.ts b/src/plugins/wizard/public/visualizations/bar_chart/index.ts index cc05f790993f..b135edf48ef6 100644 --- a/src/plugins/wizard/public/visualizations/bar_chart/index.ts +++ b/src/plugins/wizard/public/visualizations/bar_chart/index.ts @@ -3,14 +3,90 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { VisualizationTypeOptions } from '../../services/type_service/visualization_type'; +import { ItemTypes } from '../../application/contributions/constants'; +import { + DATA_TAB_ID, + DropboxContribution, + MainItemContribution as ConfigPanelItem, +} from '../../application/contributions'; +import { VisualizationTypeOptions } from '../../services/type_service'; export const createBarChartConfig = (): VisualizationTypeOptions => { + const configPanelItems: ConfigPanelItem[] = [ + { + type: ItemTypes.TITLE, + title: 'Bar Chart Configuration', + }, + createDropboxContribution('x_axis', 'X Axis', {}), + createDropboxContribution('y_axis', 'Y Axis', { limit: 5 }), + // { + // type: ItemTypes.INPUT, + // id: 'testLabel', + // label: 'Test Label', + // }, + ]; + return { name: 'bar_chart', title: 'Bar Chart', icon: 'visBarVertical', description: 'This is a bar chart', - contributions: {}, + contributions: { + items: { + [DATA_TAB_ID]: configPanelItems, + }, + }, }; }; + +const createDropboxContribution = ( + id: string, + label: string, + props?: Pick<DropboxContribution, 'limit'> +): DropboxContribution => ({ + type: ItemTypes.DROPBOX, + id, + label, + items: [ + { + type: ItemTypes.SELECT, + id: 'aggregation', + label: 'Select a Function', + options: (state, services) => { + const { config } = state; + // config.items[] + const { buckets } = services.data.search.aggs.types.getAll(); + return buckets.map(({ name, title, type }) => ({ + value: name, + inputDisplay: title, + })); + }, + }, + { + type: ItemTypes.INPUT, + id: 'label', + label: 'Name', + }, + ], + display: (indexField, dropboxState) => { + const dropboxField = { + icon: indexField.type, + label: indexField.displayName, + }; + + if (dropboxState?.label) { + dropboxField.label = dropboxState.label; + } + + return dropboxField; + }, + onDrop: (indexField) => { + return { + // label: indexField.displayName, + }; + }, + // isDroppable: (indexField) => { + // return indexField.displayName === 'geo.srcdest'; + // }, + ...props, +}); diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts index 604de170c8ab..cf3151cf1196 100644 --- a/src/plugins/wizard/public/visualizations/index.ts +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -7,10 +7,10 @@ import type { TypeServiceSetup } from '../services/type_service'; import { createBarChartConfig } from './bar_chart'; import { createPieChartConfig } from './pie_chart'; -export function registerDefaultTypes(typeServieSetup: TypeServiceSetup) { +export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { const visualizationTypes = [createBarChartConfig, createPieChartConfig]; visualizationTypes.forEach((createTypeConfig) => { - typeServieSetup.createVisualizationType(createTypeConfig()); + typeServiceSetup.createVisualizationType(createTypeConfig()); }); } diff --git a/src/plugins/wizard/public/visualizations/pie_chart/index.ts b/src/plugins/wizard/public/visualizations/pie_chart/index.ts index b47965bc1905..e71540852a32 100644 --- a/src/plugins/wizard/public/visualizations/pie_chart/index.ts +++ b/src/plugins/wizard/public/visualizations/pie_chart/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { VisualizationTypeOptions } from '../../services/type_service/visualization_type'; +import { VisualizationTypeOptions } from '../../services/type_service'; export const createPieChartConfig = (): VisualizationTypeOptions => { return { diff --git a/test/functional/apps/visualize/_chart_types.ts b/test/functional/apps/visualize/_chart_types.ts index 7ddeb6c9b9cd..63538541e813 100644 --- a/test/functional/apps/visualize/_chart_types.ts +++ b/test/functional/apps/visualize/_chart_types.ts @@ -65,6 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'Timeline', 'Vega', 'Vertical Bar', + 'Wizard', // TODO: Update to final name when ready ]; if (!isOss) { expectedChartTypes.push('Maps', 'Lens'); From d51d037afd281dc2ce21872e49eeb186a69fe26c Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Tue, 14 Jun 2022 13:43:04 -0700 Subject: [PATCH 03/47] [D&D] Refactor to use AggService and introduce metric visualization (#1734) * partial progress Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * simle workign metric using aggShemas Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * updated VisualizationTypeOptions to be a generic Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * partially working metric style options Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * all state objects are serializeable Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * working delete and reorder Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: cleanup agg service changes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../components/sidebar/state/reducers.ts | 1 + .../public/components/metric_vis_options.tsx | 2 +- src/plugins/vis_type_metric/public/index.ts | 3 + .../vis_type_metric/public/metric_vis_fn.ts | 2 +- .../public/legacy/build_pipeline.ts | 12 +- src/plugins/wizard/opensearch_dashboards.json | 6 +- .../application/components/side_nav.scss | 8 +- .../application/components/side_nav.tsx | 40 ++- .../public/application/components/top_nav.tsx | 4 +- .../application/components/workspace.tsx | 26 +- .../containers/data_tab/config_panel.tsx | 117 ++++---- .../containers/data_tab/field_search.tsx | 2 +- .../containers/data_tab/field_selector.tsx | 30 +- .../containers/data_tab/items/dropbox.scss | 17 +- .../containers/data_tab/items/dropbox.tsx | 29 +- .../containers/data_tab/items/title.tsx | 51 +--- .../data_tab/items/use/use_dropbox.tsx | 278 +++++++++++++----- .../data_tab/items/use/use_form_field.tsx | 40 +-- .../containers/data_tab/secondary_panel.tsx | 19 ++ .../data_tab/utils/schema_to_dropbox.tsx | 21 ++ .../utils/state_management/config_slice.ts | 176 ----------- .../state_management/datasource_slice.ts | 63 ---- .../utils/state_management/hooks.ts | 2 +- .../utils/state_management/preload.ts | 9 +- .../utils/state_management/store.ts | 6 +- .../utils/state_management/style_slice.ts | 47 +++ .../state_management/visualization_slice.ts | 81 ++++- .../public/application/utils/use/index.ts | 1 + .../utils/use/use_index_pattern.tsx | 30 ++ .../utils/use/use_visualization_type.ts | 4 +- src/plugins/wizard/public/plugin.ts | 27 +- src/plugins/wizard/public/plugin_services.ts | 15 + .../public/services/type_service/types.ts | 25 +- .../type_service/visualization_type.test.tsx | 143 ++++----- .../type_service/visualization_type.tsx | 62 +--- src/plugins/wizard/public/types.ts | 3 + .../wizard/public/visualizations/index.ts | 9 +- .../metric/components/metric_viz_options.tsx | 149 ++++++++++ .../public/visualizations/metric/index.ts | 6 + .../visualizations/metric/metric_viz_type.ts | 116 ++++++++ .../visualizations/metric/to_expression.ts | 177 +++++++++++ 41 files changed, 1223 insertions(+), 636 deletions(-) create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx create mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/utils/schema_to_dropbox.tsx delete mode 100644 src/plugins/wizard/public/application/utils/state_management/config_slice.ts delete mode 100644 src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/style_slice.ts create mode 100644 src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx create mode 100644 src/plugins/wizard/public/plugin_services.ts create mode 100644 src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx create mode 100644 src/plugins/wizard/public/visualizations/metric/index.ts create mode 100644 src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts create mode 100644 src/plugins/wizard/public/visualizations/metric/to_expression.ts diff --git a/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts index f41b07698df8..7d2f1cd0a9cd 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts +++ b/src/plugins/vis_default_editor/public/components/sidebar/state/reducers.ts @@ -57,6 +57,7 @@ const createEditorStateReducer = ({ !state.data.aggs!.aggs.find((agg) => agg.schema === schema.name) && schema.defaults ? (schema as any).defaults.slice(0, schema.max) : { schema: schema.name }; + const aggConfig = state.data.aggs!.createAggConfig(defaultConfig, { addToAggConfigs: false, }); diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx index 76533eae4da3..02f7b6cafb45 100644 --- a/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx +++ b/src/plugins/vis_type_metric/public/components/metric_vis_options.tsx @@ -97,7 +97,7 @@ function MetricVisOptions({ ); const setColorMode: EuiButtonGroupProps['onChange'] = useCallback( - (id) => setMetricValue('metricColorMode', id as ColorModes), + (id: string) => setMetricValue('metricColorMode', id as ColorModes), [setMetricValue] ); diff --git a/src/plugins/vis_type_metric/public/index.ts b/src/plugins/vis_type_metric/public/index.ts index 428b40f24acd..1b90e139b03c 100644 --- a/src/plugins/vis_type_metric/public/index.ts +++ b/src/plugins/vis_type_metric/public/index.ts @@ -34,3 +34,6 @@ import { MetricVisPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new Plugin(initializerContext); } + +/* Public Types */ +export { MetricVisExpressionFunctionDefinition } from './metric_vis_fn'; diff --git a/src/plugins/vis_type_metric/public/metric_vis_fn.ts b/src/plugins/vis_type_metric/public/metric_vis_fn.ts index fcdb10b74e23..e03571089fa1 100644 --- a/src/plugins/vis_type_metric/public/metric_vis_fn.ts +++ b/src/plugins/vis_type_metric/public/metric_vis_fn.ts @@ -54,7 +54,7 @@ interface Arguments { colorRange: Range[]; font: Style; metric: any[]; // these aren't typed yet - bucket: any; // these aren't typed yet + bucket?: any; // these aren't typed yet } export interface MetricVisRenderValue { diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index e0f2ee7e1cca..ae7f3bbe7a29 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -373,7 +373,8 @@ export const buildVislibDimensions = async (vis: any, params: BuildPipelineParam splitColumn: schemas.split_column, }; if (schemas.segment) { - const xAgg = vis.data.aggs.getResponseAggs()[dimensions.x.accessor]; + const a = vis.data.aggs.getResponseAggs(); + const xAgg = a[dimensions.x.accessor]; if (xAgg.type.name === 'date_histogram') { dimensions.x.params.date = true; const { opensearchUnit, opensearchValue } = xAgg.buckets.getInterval(); @@ -423,10 +424,10 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { // request handler if (vis.type.requestHandler === 'courier') { pipeline += `opensearchaggs - ${prepareString('index', indexPattern!.id)} - metricsAtAllLevels=${vis.isHierarchical()} - partialRows=${vis.params.showPartialRows || false} - ${prepareJson('aggConfigs', vis.data.aggs!.aggs)} | `; + ${prepareString('index', indexPattern!.id)} + metricsAtAllLevels=${vis.isHierarchical()} + partialRows=${vis.params.showPartialRows || false} + ${prepareJson('aggConfigs', vis.data.aggs!.aggs)} | `; } const schemas = getSchemas(vis, params); @@ -456,5 +457,6 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { } } } + return pipeline; }; diff --git a/src/plugins/wizard/opensearch_dashboards.json b/src/plugins/wizard/opensearch_dashboards.json index 5d4402106e0c..020ca5f2ab9b 100644 --- a/src/plugins/wizard/opensearch_dashboards.json +++ b/src/plugins/wizard/opensearch_dashboards.json @@ -6,13 +6,17 @@ "ui": true, "requiredPlugins": [ "navigation", + "charts", "data", "opensearchDashboardsReact", + "opensearchDashboardsUtils", "savedObjects", "embeddable", + "expressions", "dashboard", "visualizations", - "opensearchUiShared" + "opensearchUiShared", + "visDefaultEditor" ], "optionalPlugins": [] } diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss index 8da4b26d20e6..caa24e30b395 100644 --- a/src/plugins/wizard/public/application/components/side_nav.scss +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -2,6 +2,7 @@ .wizSidenav { @include scrollNavParent(auto 1fr); + grid-area: sideNav; border-right: $euiBorderThin; } @@ -11,8 +12,13 @@ } .wizSidenavTabs { + .euiTab__content { + text-transform: capitalize; + } + @include scrollNavParent(min-content 1fr); - &>[role="tabpanel"] { + + & > [role="tabpanel"] { @include scrollNavParent; } } diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx index 3bb1f1d76618..c74837ceab54 100644 --- a/src/plugins/wizard/public/application/components/side_nav.tsx +++ b/src/plugins/wizard/public/application/components/side_nav.tsx @@ -10,8 +10,10 @@ import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react import { WizardServices } from '../../types'; import './side_nav.scss'; import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; -import { setIndexPattern } from '../utils/state_management/datasource_slice'; +import { setIndexPattern } from '../utils/state_management/visualization_slice'; import { useVisualizationType } from '../utils/use'; +import { DataTab } from '../contributions'; +import { StyleTabConfig } from '../../services/type_service'; export const SideNav = () => { const { @@ -21,17 +23,32 @@ export const SideNav = () => { }, } = useOpenSearchDashboards<WizardServices>(); const { IndexPatternSelect } = data.ui; - const { indexPattern } = useTypedSelector((state) => state.dataSource); + const { indexPattern: indexPatternId } = useTypedSelector((state) => state.visualization); const dispatch = useTypedDispatch(); const { - contributions: { containers }, + ui: { containerConfig }, } = useVisualizationType(); - const tabs: EuiTabbedContentTab[] = containers.sidePanel.map(({ id, name, Component }) => ({ - id, - name, - content: Component, - })); + const tabs: EuiTabbedContentTab[] = Object.entries(containerConfig).map( + ([containerName, config]) => { + let content = null; + switch (containerName) { + case 'data': + content = <DataTab key="containerName" />; + break; + + case 'style': + content = (config as StyleTabConfig).render(); + break; + } + + return { + id: containerName, + name: containerName, + content, + }; + } + ); return ( <section className="wizSidenav"> @@ -46,10 +63,13 @@ export const SideNav = () => { placeholder={i18n.translate('wizard.nav.dataSource.selector.placeholder', { defaultMessage: 'Select index pattern', })} - indexPatternId={indexPattern?.id || ''} + indexPatternId={indexPatternId || ''} onChange={async (newIndexPatternId: any) => { const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); - dispatch(setIndexPattern(newIndexPattern)); + + if (newIndexPattern) { + dispatch(setIndexPattern(newIndexPatternId)); + } }} isClearable={false} /> diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx index 5afa39f7bafd..d63ceedb302f 100644 --- a/src/plugins/wizard/public/application/components/top_nav.tsx +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -10,7 +10,7 @@ import { getTopNavconfig } from '../utils/get_top_nav_config'; import { WizardServices } from '../../types'; import './top_nav.scss'; -import { useTypedSelector } from '../utils/state_management'; +import { useIndexPattern } from '../utils/use'; export const TopNav = () => { const { services } = useOpenSearchDashboards<WizardServices>(); @@ -22,7 +22,7 @@ export const TopNav = () => { } = services; const config = useMemo(() => getTopNavconfig(services), [services]); - const { indexPattern } = useTypedSelector((state) => state.dataSource); + const indexPattern = useIndexPattern(); return ( <div className="wizTopNav"> diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 6715c7fdf7c5..7747eef57cba 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -14,16 +14,34 @@ import { EuiPanel, EuiPopover, } from '@elastic/eui'; -import React, { FC, useState, useMemo } from 'react'; +import React, { FC, useState, useMemo, useEffect } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../types'; -import { useTypedDispatch } from '../utils/state_management'; +import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; import { setActiveVisualization } from '../utils/state_management/visualization_slice'; import { useVisualizationType } from '../utils/use'; import './workspace.scss'; export const Workspace: FC = ({ children }) => { + const { + services: { + expressions: { ReactExpressionRenderer }, + }, + } = useOpenSearchDashboards<WizardServices>(); + const { toExpression } = useVisualizationType(); + const [expression, setExpression] = useState<string>(); + const rootState = useTypedSelector((state) => state); + + useEffect(() => { + async function loadExpression() { + const exp = await toExpression(rootState); + setExpression(exp); + } + + loadExpression(); + }, [rootState, toExpression]); + return ( <section className="wizWorkspace"> <EuiFlexGroup className="wizCanvasControls"> @@ -32,8 +50,8 @@ export const Workspace: FC = ({ children }) => { </EuiFlexItem> </EuiFlexGroup> <EuiPanel className="wizCanvas"> - {children ? ( - children + {expression ? ( + <ReactExpressionRenderer expression={expression} /> ) : ( <EuiFlexItem className="wizWorkspace__empty"> <EuiEmptyPrompt diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx index ad1f49a7ff26..a5fc6a9fef31 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx @@ -4,7 +4,7 @@ */ import { EuiForm } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useVisualizationType } from '../../../utils/use'; import { DropboxContribution, @@ -20,51 +20,64 @@ import { SelectContribution } from '../common/items'; import { INDEX_FIELD_KEY } from './items/use/use_form_field'; import { DATA_TAB_ID } from '.'; import './config_panel.scss'; +import { mapSchemaToAggPanel } from './utils/schema_to_dropbox'; +import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../../types'; +import { SecondaryPanel } from './secondary_panel'; const DEFAULT_ITEMS: MainItemContribution[] = [getTitleContribution()]; export function ConfigPanel() { - const { - contributions: { items }, - } = useVisualizationType(); - const activeItem = useTypedSelector((state) => state.config.activeItem); - const configItemState = useTypedSelector((state) => state.config.items[activeItem?.id || '']); - - const hydratedItems: MainItemContribution[] = [...(items?.[DATA_TAB_ID] ?? []), ...DEFAULT_ITEMS]; - - const mainPanel = useMemo(() => mapItemToPanelComponents(hydratedItems), [hydratedItems]); - const secondaryPanel = useMemo(() => { - if (!activeItem || !configItemState || typeof configItemState === 'string') return; - - // Generate each secondary panel base on active item type - if (activeItem.type === ITEM_TYPES.DROPBOX) { - const activeDropboxContribution = hydratedItems.find( - (item: MainItemContribution) => - item.type === ITEM_TYPES.DROPBOX && item?.id === activeItem?.id - ) as DropboxContribution | undefined; - - if (!activeDropboxContribution) return null; - - let itemsToRender: SecondaryItemContribution[] = [ - getTitleContribution(activeDropboxContribution.label), - getFieldSelectorContribution(), - ]; - - const dropboxFieldInstance = configItemState.instances.find( - ({ id }) => id === activeItem.instanceId - ); - if (dropboxFieldInstance && dropboxFieldInstance.properties.fieldName) { - itemsToRender = [...itemsToRender, ...activeDropboxContribution.items]; - } - - return mapItemToPanelComponents(itemsToRender, true); - } - }, [activeItem, configItemState, hydratedItems]); + const vizType = useVisualizationType(); + const activeAgg = useTypedSelector((state) => state.visualization.activeVisualization?.activeAgg); + const schemas = vizType.ui.containerConfig.data.schemas; + + // TODO: Will cleanup when add and edit field support is re introduced + // const activeItem = useTypedSelector((state) => state.config.activeItem); + // const configItemState = useTypedSelector((state) => state.config.items[activeItem?.id || '']); + + // const hydratedItems: MainItemContribution[] = useMemo( + // () => [...(items?.[DATA_TAB_ID] ?? []), ...DEFAULT_ITEMS], + // [items] + // ); + + // const mainPanel = useMemo(() => mapItemToPanelComponents(hydratedItems), [hydratedItems]); + // const secondaryPanel = useMemo(() => { + // if (!activeItem || !configItemState || typeof configItemState === 'string') return; + + // // Generate each secondary panel base on active item type + // if (activeItem.type === ITEM_TYPES.DROPBOX) { + // const activeDropboxContribution = hydratedItems.find( + // (item: MainItemContribution) => + // item.type === ITEM_TYPES.DROPBOX && item?.id === activeItem?.id + // ) as DropboxContribution | undefined; + + // if (!activeDropboxContribution) return null; + + // let itemsToRender: SecondaryItemContribution[] = [ + // getTitleContribution(activeDropboxContribution.label), + // getFieldSelectorContribution(), + // ]; + + // const dropboxFieldInstance = configItemState.instances.find( + // ({ id }) => id === activeItem.instanceId + // ); + // if (dropboxFieldInstance && dropboxFieldInstance.properties.fieldName) { + // itemsToRender = [...itemsToRender, ...activeDropboxContribution.items]; + // } + + // return mapItemToPanelComponents(itemsToRender, true); + // } + // }, [activeItem, configItemState, hydratedItems]); + + if (!schemas) return null; + + const mainPanel = mapSchemaToAggPanel(schemas); return ( - <EuiForm className={`wizConfig ${activeItem ? 'showSecondary' : ''}`}> + <EuiForm className={`wizConfig ${activeAgg ? 'showSecondary' : ''}`}> <div className="wizConfig__section">{mainPanel}</div> - <div className="wizConfig__section wizConfig--secondary">{secondaryPanel}</div> + <SecondaryPanel /> </EuiForm> ); } @@ -76,16 +89,16 @@ function getTitleContribution(title?: string): TitleItemContribution { }; } -function getFieldSelectorContribution(): SelectContribution<string> { - return { - type: ItemTypes.SELECT, - id: INDEX_FIELD_KEY, - label: 'Select a Field', - options: (state) => { - return state.dataSource.visualizableFields.map((field) => ({ - value: field.name, - inputDisplay: field.displayName, - })); - }, - }; -} +// function getFieldSelectorContribution(): SelectContribution<string> { +// return { +// type: ItemTypes.SELECT, +// id: INDEX_FIELD_KEY, +// label: 'Select a Field', +// options: (state) => { +// return state.dataSource.visualizableFields.map((field) => ({ +// value: field.name, +// inputDisplay: field.displayName, +// })); +// }, +// }; +// } diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx index d086cfc1f362..772e308bbc9b 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@osd/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { setSearchField } from '../../../utils/state_management/datasource_slice'; +import { setSearchField } from '../../../utils/state_management/visualization_slice'; import { useTypedDispatch } from '../../../utils/state_management'; export interface Props { diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx index 9d1771fde2b5..70dac772a190 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx @@ -4,7 +4,7 @@ */ import React, { useCallback, useState, useEffect } from 'react'; -import { EuiFlexItem, EuiAccordion, EuiSpacer, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; +import { EuiFlexItem, EuiAccordion, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; import { FieldSearch } from './field_search'; import { @@ -16,6 +16,7 @@ import { FieldSelectorField } from './field_selector_field'; import './field_selector.scss'; import { useTypedSelector } from '../../../utils/state_management'; +import { useIndexPattern } from '../../../utils/use'; interface IFieldCategories { categorical: IndexPatternField[]; @@ -30,19 +31,32 @@ const META_FIELDS: string[] = [ OPENSEARCH_FIELD_TYPES._TYPE, ]; +const ALLOWED_FIELDS: string[] = [OSD_FIELD_TYPES.STRING, OSD_FIELD_TYPES.NUMBER]; + export const FieldSelector = () => { - const indexFields = useTypedSelector((state) => state.dataSource.visualizableFields); - const [filteredFields, setFilteredFields] = useState(indexFields); - const fieldSearchValue = useTypedSelector((state) => state.dataSource.searchField); + const indexPattern = useIndexPattern(); + const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); + const [filteredFields, setFilteredFields] = useState<IndexPatternField[]>([]); + + // TODO: Temporary validate function + // Need to identify how to get fieldCounts to use the standard filter and group functions + const isVisualizable = useCallback((field: IndexPatternField): boolean => { + const isAggregatable = field.aggregatable === true; + const isNotScripted = !field.scripted; + const isAllowed = ALLOWED_FIELDS.includes(field.type); + + return isAggregatable && isNotScripted && isAllowed; + }, []); useEffect(() => { - const filteredSubset = indexFields.filter((field) => - field.displayName.includes(fieldSearchValue) - ); + const indexFields = indexPattern?.fields ?? []; + const filteredSubset = indexFields + .filter(isVisualizable) + .filter((field) => field.displayName.includes(fieldSearchValue)); setFilteredFields(filteredSubset); return; - }, [indexFields, fieldSearchValue]); + }, [fieldSearchValue, indexPattern?.fields, isVisualizable]); const fields = filteredFields?.reduce<IFieldCategories>( (fieldGroups, currentField) => { diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss index b81ab2a07b33..dd2ab9c9d980 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss @@ -15,13 +15,15 @@ display: grid; grid-gap: $euiSizeXS; padding: $euiSizeS; - background-color: #E9EDF3; + background-color: #e9edf3; border-radius: $euiBorderRadius; } &__field { display: grid; - grid-template-columns: auto 1fr auto; + grid-template-columns: 1fr auto; + + // grid-template-columns: auto 1fr auto; grid-gap: $euiSizeS; padding: $euiSizeS $euiSizeM; align-items: center; @@ -41,15 +43,14 @@ grid-template-columns: 1fr auto; &.validField { - background-color: #A8D9E7; - border-color: #A8D9E7; + background-color: #a8d9e7; + border-color: #a8d9e7; &.canDrop { - background-color: rgba(0, 161, 201, 0.3); - border-color: #006BB4; + background-color: rgba(0, 161, 201, 30%); + border-color: #006bb4; border-style: dashed; } } - } -} \ No newline at end of file +} diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx index a9acf0c9ad71..8639340c3a49 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx @@ -12,13 +12,15 @@ import { EuiPanel, EuiText, euiDragDropReorder, + DropResult, } from '@elastic/eui'; import React, { useCallback } from 'react'; import { FieldIcon } from '../../../../../../../opensearch_dashboards_react/public'; import { IDropAttributes, IDropState } from '../../../../utils/drag_drop'; import './dropbox.scss'; -import { DropboxContribution, DropboxDisplay } from './types'; +import { DropboxDisplay } from './types'; import { useDropbox } from './use'; +import { UseDropboxProps } from './use/use_dropbox'; interface DropboxProps extends IDropState { id: string; @@ -28,7 +30,13 @@ interface DropboxProps extends IDropState { onAddField: () => void; onEditField: (id: string) => void; onDeleteField: (id: string) => void; - onReorderField: (reorderedIds: string[]) => void; + onReorderField: ({ + sourceAggId, + destinationAggId, + }: { + sourceAggId: string; + destinationAggId: string; + }) => void; dropProps: IDropAttributes; } @@ -46,13 +54,13 @@ const DropboxComponent = ({ dropProps, }: DropboxProps) => { const handleDragEnd = useCallback( - ({ source, destination }) => { - if (!source || !destination) return; + ({ source, destination }: DropResult) => { + if (!destination) return; - const instanceIds = fields.map(({ id }) => id); - const reorderedIds = euiDragDropReorder(instanceIds, source.index, destination.index); - - onReorderField(reorderedIds); + onReorderField({ + sourceAggId: fields[source.index].id, + destinationAggId: fields[destination.index].id, + }); }, [fields, onReorderField] ); @@ -65,7 +73,8 @@ const DropboxComponent = ({ {fields.map(({ id, label, icon }, index) => ( <EuiDraggable className="dropBox__draggable" key={id} draggableId={id} index={index}> <EuiPanel key={index} paddingSize="s" className="dropBox__field"> - <FieldIcon type={icon} /> + {/* TODO: Verify if field icon makes sense here */} + {/* <FieldIcon type={icon} /> */} <EuiText size="s" className="dropBox__field_text" onClick={() => onEditField(id)}> <a role="button" tabIndex={0}> {label} @@ -104,7 +113,7 @@ const DropboxComponent = ({ ); }; -const Dropbox = React.memo((dropBox: DropboxContribution) => { +const Dropbox = React.memo((dropBox: UseDropboxProps) => { const props = useDropbox(dropBox); return <DropboxComponent {...props} />; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx index 84720ae6ff1c..1f48db369669 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx @@ -11,46 +11,27 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { TitleItemContribution } from './types'; -import { useTypedDispatch } from '../../../../utils/state_management'; -import { setActiveItem } from '../../../../utils/state_management/config_slice'; - export interface TitleProps { title: string; - icon?: React.ReactNode; - showDivider?: boolean; -} - -export const TitleComponent = ({ title, icon, showDivider = false }: TitleProps) => ( - <> - <div className="wizConfig__title"> - <EuiFlexGroup gutterSize="s" alignItems="center"> - {icon && <EuiFlexItem grow={false}>{icon}</EuiFlexItem>} - <EuiFlexItem> - <EuiTitle size="xxs"> - <h2>{title}</h2> - </EuiTitle> - </EuiFlexItem> - </EuiFlexGroup> - </div> - {showDivider ? <EuiHorizontalRule margin="s" /> : <EuiSpacer size="s" />} - </> -); - -interface TitleContributionProps extends TitleItemContribution { isSecondary?: boolean; + closeMenu?: () => void; } -export const Title = ({ title, isSecondary }: TitleContributionProps) => { - const dispatch = useTypedDispatch(); - +export const Title = ({ title, isSecondary, closeMenu }: TitleProps) => { + const icon = isSecondary && <EuiIcon type="arrowLeft" onClick={closeMenu} />; return ( - <TitleComponent - title={title} - icon={ - isSecondary && <EuiIcon type="arrowLeft" onClick={() => dispatch(setActiveItem(null))} /> - } - showDivider={isSecondary} - /> + <> + <div className="wizConfig__title"> + <EuiFlexGroup gutterSize="s" alignItems="center"> + {icon && <EuiFlexItem grow={false}>{icon}</EuiFlexItem>} + <EuiFlexItem> + <EuiTitle size="xxs"> + <h2>{title}</h2> + </EuiTitle> + </EuiFlexItem> + </EuiFlexGroup> + </div> + {isSecondary ? <EuiHorizontalRule margin="s" /> : <EuiSpacer size="s" />} + </> ); }; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx index 250825f60d10..4ef732ad84bd 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx @@ -4,15 +4,11 @@ */ import { useCallback, useMemo } from 'react'; -import { IndexPatternField } from 'src/plugins/data/common'; +import { cloneDeep } from 'lodash'; +import { CreateAggConfigParams, IndexPatternField } from 'src/plugins/data/common'; +import { Schema } from '../../../../../../../../vis_default_editor/public'; import { FieldDragDataType } from '../../../../../utils/drag_drop/types'; import { useTypedDispatch, useTypedSelector } from '../../../../../utils/state_management'; -import { - addInstance, - reorderInstances, - setActiveItem, - updateInstance, -} from '../../../../../utils/state_management/config_slice'; import { DropboxContribution, DropboxState, @@ -22,72 +18,86 @@ import { } from '../types'; import { DropboxProps } from '../dropbox'; import { useDrop } from '../../../../../utils/drag_drop'; - -type DropboxInstanceState = DropboxState['instances'][number]; +import { + createAggConfigParams, + reorderAggConfigParams, + updateAggConfigParams, +} from '../../../../../utils/state_management/visualization_slice'; +import { useIndexPattern } from '../../../../../../application/utils/use/use_index_pattern'; +import { useOpenSearchDashboards } from '../../../../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../../../../types'; export const INITIAL_STATE: DropboxState = { instances: [], }; -export const useDropbox = (dropboxContribution: DropboxContribution): DropboxProps => { - const { id: dropboxId, label, limit, display, onDrop, isDroppable } = dropboxContribution; +export interface UseDropboxProps extends Pick<DropboxProps, 'id' | 'label'> { + schema: Schema; +} + +export const useDropbox = (props: UseDropboxProps): DropboxProps => { + const { id: dropboxId, label, schema } = props; const dispatch = useTypedDispatch(); - const { items, availableFields } = useTypedSelector((state) => ({ - items: state.config.items, - availableFields: state.dataSource.visualizableFields, - })); - const configItemState = items[dropboxId]; - const dropboxState = - !configItemState || typeof configItemState === 'string' ? INITIAL_STATE : configItemState; - const filterPatrialInstances = useCallback( - ({ properties }: DropboxInstanceState) => !!properties.fieldName, - [] + const indexPattern = useIndexPattern(); + const { + services: { + data: { + search: { aggs: aggService }, + }, + }, + } = useOpenSearchDashboards<WizardServices>(); + const aggConfigParams = useTypedSelector( + (state) => state.visualization.activeVisualization?.aggConfigParams ); - const mapInstanceToFieldDisplay = useCallback( - ({ id, properties }: DropboxInstanceState): DropboxDisplay => { - const indexPatternField = availableFields.find(({ name }) => name === properties.fieldName); - if (!indexPatternField) throw new Error('Field to display missing in available fields'); + const aggConfigs = useMemo(() => { + return indexPattern && aggService.createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + }, [aggConfigParams, aggService, indexPattern]); - return getDisplayField(id, indexPatternField, properties, display); - }, - [availableFields, display] - ); + const aggs = aggConfigs?.aggs ?? []; + + const dropboxAggs = aggs.filter((agg) => agg.schema === schema.name); const displayFields: DropboxDisplay[] = useMemo( - () => dropboxState.instances.filter(filterPatrialInstances).map(mapInstanceToFieldDisplay), - [dropboxState.instances, filterPatrialInstances, mapInstanceToFieldDisplay] + () => + dropboxAggs?.map( + (agg): DropboxDisplay => ({ + id: agg.id, + icon: 'number', // TODO: Check if we still need an icon here + label: agg.makeLabel(), + }) + ) || [], + [dropboxAggs] ); // Event handlers for each dropbox action type const onAddField = useCallback(() => { - dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX)); - }, [dispatch, dropboxId]); + const agg = aggConfigs?.createAggConfig( + { + type: (schema.defaults as any).aggType, + schema: schema.name, + }, + { + addToAggConfigs: false, + } + ); - const onEditField = useCallback( - (instanceId) => { - dispatch( - setActiveItem({ - id: dropboxId, - type: ITEM_TYPES.DROPBOX, - instanceId, - }) - ); - }, - [dispatch, dropboxId] - ); + if (agg) { + dispatch(createAggConfigParams(agg.serialize())); + } + }, [aggConfigs, dispatch, schema.defaults, schema.name]); + + const onEditField = useCallback((instanceId) => {}, []); const onDeleteField = useCallback( - (instanceId) => { - dispatch( - updateInstance({ - id: dropboxId, - instanceId, - instanceState: null, - }) - ); + (aggId: string) => { + const newAggs = aggConfigs?.aggs.filter((agg) => agg.id !== aggId); + + if (newAggs) { + dispatch(updateAggConfigParams(newAggs.map((agg) => agg.serialize()))); + } }, - [dispatch, dropboxId] + [aggConfigs?.aggs, dispatch] ); const onDropField = useCallback( @@ -95,32 +105,32 @@ export const useDropbox = (dropboxContribution: DropboxContribution): DropboxPro if (!data) return; const { name: fieldName } = data; - const indexField = getIndexPatternField(fieldName, availableFields); - - if (!indexField) return; - - if (isDroppable && !isDroppable(indexField)) return; - const newState: DropboxFieldProps = { - ...onDrop?.(indexField), - fieldName, - }; + aggConfigs?.createAggConfig({ + type: (schema.defaults as any).aggType, + schema: schema.name, + params: { + field: fieldName, + }, + }); - dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX, false, newState)); + if (aggConfigs) { + dispatch(updateAggConfigParams(aggConfigs.aggs.map((agg) => agg.serialize()))); + } }, - [availableFields, isDroppable, onDrop, dispatch, dropboxId] + [aggConfigs, dispatch, schema.defaults, schema.name] ); const onReorderField = useCallback( - (reorderedInstanceIds: string[]) => { + ({ sourceAggId, destinationAggId }) => { dispatch( - reorderInstances({ - id: dropboxId, - reorderedInstanceIds, + reorderAggConfigParams({ + sourceId: sourceAggId, + destinationId: destinationAggId, }) ); }, - [dispatch, dropboxId] + [dispatch] ); const [dropProps, { isValidDropTarget, dragData, ...dropState }] = useDrop( @@ -131,17 +141,19 @@ export const useDropbox = (dropboxContribution: DropboxContribution): DropboxPro const isValidDropField = useMemo(() => { if (!dragData) return false; - const indexField = getIndexPatternField(dragData.name, availableFields); + const indexField = getIndexPatternField(dragData.name, indexPattern?.fields ?? []); if (!indexField) return false; - return isValidDropTarget && (isDroppable?.(indexField) ?? true); - }, [availableFields, dragData, isDroppable, isValidDropTarget]); + return isValidDropTarget; + // TODO: Validate if the field is droppable from schema ref : src/plugins/vis_default_editor/public/components/agg_params.tsx + // return isValidDropTarget && (isDroppable?.(indexField) ?? true); + }, [dragData, indexPattern?.fields, isValidDropTarget]); return { id: dropboxId, label, - limit, + limit: schema.max, fields: displayFields, onAddField, onEditField, @@ -152,6 +164,124 @@ export const useDropbox = (dropboxContribution: DropboxContribution): DropboxPro isValidDropTarget: isValidDropField, dropProps, }; + + // TODO: Will cleanup once add and edit field support is reintroduced + // const configItemState = items[dropboxId]; + // const dropboxState = + // !configItemState || typeof configItemState === 'string' ? INITIAL_STATE : configItemState; + // const filterPatrialInstances = useCallback( + // ({ properties }: DropboxInstanceState) => !!properties.fieldName, + // [] + // ); + // const mapInstanceToFieldDisplay = useCallback( + // ({ id, properties }: DropboxInstanceState): DropboxDisplay => { + // const indexPatternField = availableFields.find(({ name }) => name === properties.fieldName); + + // if (!indexPatternField) throw new Error('Field to display missing in available fields'); + + // return getDisplayField(id, indexPatternField, properties, display); + // }, + // [availableFields, display] + // ); + + // const displayFields: DropboxDisplay[] = useMemo( + // () => dropboxState.instances.filter(filterPatrialInstances).map(mapInstanceToFieldDisplay), + // [dropboxState.instances, filterPatrialInstances, mapInstanceToFieldDisplay] + // ); + + // Event handlers for each dropbox action type + // const onAddField = useCallback(() => { + // dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX)); + // }, [dispatch, dropboxId]); + + // const onEditField = useCallback( + // (instanceId) => { + // dispatch( + // setActiveItem({ + // id: dropboxId, + // type: ITEM_TYPES.DROPBOX, + // instanceId, + // }) + // ); + // }, + // [dispatch, dropboxId] + // ); + + // const onDeleteField = useCallback( + // (instanceId) => { + // dispatch( + // updateInstance({ + // id: dropboxId, + // instanceId, + // instanceState: null, + // }) + // ); + // }, + // [dispatch, dropboxId] + // ); + + // const onDropField = useCallback( + // (data: FieldDragDataType['value']) => { + // if (!data) return; + + // const { name: fieldName } = data; + // const indexField = getIndexPatternField(fieldName, availableFields); + + // if (!indexField) return; + + // if (isDroppable && !isDroppable(indexField)) return; + + // const newState: DropboxFieldProps = { + // ...onDrop?.(indexField), + // fieldName, + // }; + + // dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX, false, newState)); + // }, + // [availableFields, isDroppable, onDrop, dispatch, dropboxId] + // ); + + // const onReorderField = useCallback( + // (reorderedInstanceIds: string[]) => { + // dispatch( + // reorderInstances({ + // id: dropboxId, + // reorderedInstanceIds, + // }) + // ); + // }, + // [dispatch, dropboxId] + // ); + + // const [dropProps, { isValidDropTarget, dragData, ...dropState }] = useDrop( + // 'field-data', + // onDropField + // ); + + // const isValidDropField = useMemo(() => { + // if (!dragData) return false; + + // const indexField = getIndexPatternField(dragData.name, availableFields); + + // if (!indexField) return false; + + // return isValidDropTarget && (isDroppable?.(indexField) ?? true); + // }, [availableFields, dragData, isDroppable, isValidDropTarget]); + + // return { + // id: dropboxId, + // label, + // limit, + // fields: displayFields, + // onAddField, + // onEditField, + // onDeleteField, + // onReorderField, + // ...dropState, + // dragData, + // isValidDropTarget: isValidDropField, + // dropProps, + // }; }; const getDisplayField = ( diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx index 8f650afb62f0..03d22dd0e7ff 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx @@ -5,11 +5,11 @@ import produce from 'immer'; import { useCallback, useMemo } from 'react'; -import { - ConfigState, - updateConfigItemState, - updateInstance, -} from '../../../../../utils/state_management/config_slice'; +// import { +// ConfigState, +// updateConfigItemState, +// updateInstance, +// } from '../../../../../utils/state_management/config_slice'; import { useTypedSelector, useTypedDispatch } from '../../../../../utils/state_management'; import { FieldContributions } from '../types'; @@ -31,14 +31,16 @@ export const useFormField = (id: string, onChange: FieldContributions['onChange' (newValue: string) => { onChange?.(newValue); + // TODO: Will cleanup once add and edit field support is reintroduced + // is a MainPanel field value if (!activeItem) { - dispatch( - updateConfigItemState({ - id, - itemState: newValue, - }) - ); + // dispatch( + // // updateConfigItemState({ + // // id, + // // itemState: newValue, + // // }) + // ); return; } @@ -46,15 +48,15 @@ export const useFormField = (id: string, onChange: FieldContributions['onChange' draftState[id] = newValue; }); - dispatch( - updateInstance({ - id: activeItem.id, - instanceId: activeItem.instanceId, - instanceState: newInstanceState, - }) - ); + // dispatch( + // updateInstance({ + // id: activeItem.id, + // instanceId: activeItem.instanceId, + // instanceState: newInstanceState, + // }) + // ); }, - [activeItem, dispatch, id, instanceState, onChange] + [activeItem, id, instanceState, onChange] ); return { diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx new file mode 100644 index 000000000000..de808c4c5fdc --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { useTypedSelector } from '../../../utils/state_management'; +import { Title } from './items'; + +export function SecondaryPanel() { + const activeAgg = useTypedSelector((state) => state.visualization.activeVisualization?.activeAgg); + + return ( + <div className="wizConfig__section wizConfig--secondary"> + <Title title="Test" isSecondary /> + <div>{JSON.stringify(activeAgg)}</div> + </div> + ); +} diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/schema_to_dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/schema_to_dropbox.tsx new file mode 100644 index 000000000000..b14a8bd13d85 --- /dev/null +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/schema_to_dropbox.tsx @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Schemas } from '../../../../../../../vis_default_editor/public'; +import { Title, Dropbox } from '../items'; + +export const mapSchemaToAggPanel = (schemas: Schemas) => { + const panelComponents = schemas.all.map((schema) => { + return <Dropbox key={schema.name} id={schema.name} label={schema.title} schema={schema} />; + }); + + return ( + <> + <Title title="Configuration" /> + <div className="wizConfig__content">{panelComponents}</div> + </> + ); +}; diff --git a/src/plugins/wizard/public/application/utils/state_management/config_slice.ts b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts deleted file mode 100644 index dc0c2ec1cb0c..000000000000 --- a/src/plugins/wizard/public/application/utils/state_management/config_slice.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { htmlIdGenerator } from '@elastic/eui'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { WizardServices } from '../../../types'; -import { - ConfigItemState, - DATA_TAB_ID, - InstanceState, - MainItemContribution, -} from '../../contributions'; - -// TODO: Move this into contributions and register the slice from there for better code splitting -// TODO: Reorganize slice for better readability -export interface ActiveItem { - id: string; - type: MainItemContribution['type']; - instanceId: string; -} - -export interface ConfigState { - items: { - [id: string]: ConfigItemState; - }; - activeItem: ActiveItem | null; -} - -const initialState: ConfigState = { - items: {}, - activeItem: null, -}; - -export const getPreloadedState = async ({ types }: WizardServices): Promise<ConfigState> => { - const preloadedState = { ...initialState }; - - const defaultVisualizationType = types.all()[0]; - - if (defaultVisualizationType) { - preloadedState.items = defaultVisualizationType.contributions.items?.[DATA_TAB_ID].filter( - ({ id }) => !!id - ).reduce((acc, { id, type }) => ({ ...acc, [id]: null }), {}); - } - - return preloadedState; -}; - -interface UpdateConfigPayload { - id: string; - itemState: any; -} - -interface AddInstancePayload extends ActiveItem { - properties?: any; - setActive: boolean; -} - -interface UpdateInstancePayload { - id: string; - instanceId: string; - instanceState: any; -} - -interface ReorderInstancePayload { - id: string; - reorderedInstanceIds: string[]; -} - -export const slice = createSlice({ - name: 'configuration', - initialState, - reducers: { - updateConfigItemState: (state, action: PayloadAction<UpdateConfigPayload>) => { - const { id, itemState } = action.payload; - - if (state.items.hasOwnProperty(id)) { - state.items[id] = itemState; - } - }, - setActiveItem: (state, { payload }: PayloadAction<ActiveItem | null>) => { - // On closing secondary menu - if (!payload) { - state.activeItem = null; - return; - } - - state.activeItem = payload; - }, - addInstance: { - reducer: (state, action: PayloadAction<AddInstancePayload>) => { - const { id, instanceId, properties, setActive } = action.payload; - - if (!state.items.hasOwnProperty(id)) return; - - if (!state.items[id]) { - state.items[id] = { - instances: [], - }; - } - - (state.items[id] as InstanceState<unknown>).instances.push({ - id: instanceId, - properties: properties ?? {}, - }); - - if (setActive) { - state.activeItem = action.payload; - } - }, - prepare: ( - id: string, - type: MainItemContribution['type'], - setActive: boolean = true, - properties?: any - ) => { - const instanceId = htmlIdGenerator()(); - - return { - payload: { - id, - type, - instanceId, - properties, - setActive, - }, - }; - }, - }, - updateInstance: (state, action: PayloadAction<UpdateInstancePayload>) => { - const { id: parentItemId, instanceId, instanceState } = action.payload; - if (!state.items.hasOwnProperty(parentItemId)) return; - - // Typescript complains if we use state.items[parentItemId] directly since it cannot resolve the type correctly - const configItem = state.items[parentItemId]; - if (!configItem || typeof configItem === 'string') return; - - const instanceIndex = configItem.instances.findIndex(({ id }) => id === instanceId); - - if (instanceIndex < 0) return; - - if (instanceState === null) { - configItem.instances.splice(instanceIndex, 1); - return; - } - - configItem.instances[instanceIndex].properties = instanceState; - }, - reorderInstances: (state, action: PayloadAction<ReorderInstancePayload>) => { - const { id: parentItemId, reorderedInstanceIds } = action.payload; - - if (!state.items.hasOwnProperty(parentItemId)) return; - - // Typescript complains if we use state.items[parentItemId] directly since it cannot resolve the type correctly - const configItem = state.items[parentItemId]; - if (!configItem || typeof configItem === 'string') return; - - const orderDict: { [id: string]: number } = {}; - reorderedInstanceIds.forEach((instanceId, index) => { - orderDict[instanceId] = index; - }); - - configItem.instances.sort((a, b) => orderDict[a.id] - orderDict[b.id]); - }, - }, -}); - -export const { reducer } = slice; -export const { - updateConfigItemState, - setActiveItem, - addInstance, - updateInstance, - reorderInstances, -} = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts deleted file mode 100644 index d51d463d68ee..000000000000 --- a/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { IndexPattern } from 'src/plugins/data/common'; -import { WizardServices } from '../../../types'; - -import { IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public'; - -const ALLOWED_FIELDS: string[] = [OSD_FIELD_TYPES.STRING, OSD_FIELD_TYPES.NUMBER]; - -interface DataSourceState { - indexPattern: IndexPattern | null; - visualizableFields: IndexPatternField[]; - searchField: string; -} - -const initialState: DataSourceState = { - indexPattern: null, - visualizableFields: [], - searchField: '', -}; - -export const getPreloadedState = async ({ data }: WizardServices): Promise<DataSourceState> => { - const preloadedState = { ...initialState }; - - const defaultIndexPattern = await data.indexPatterns.getDefault(); - if (defaultIndexPattern) { - preloadedState.indexPattern = defaultIndexPattern; - preloadedState.visualizableFields = defaultIndexPattern.fields.filter(isVisualizable); - } - - return preloadedState; -}; - -export const slice = createSlice({ - name: 'dataSource', - initialState, - reducers: { - setIndexPattern: (state, action: PayloadAction<IndexPattern>) => { - state.indexPattern = action.payload; - state.visualizableFields = action.payload.fields.filter(isVisualizable); - }, - setSearchField: (state, action: PayloadAction<string>) => { - state.searchField = action.payload; - }, - }, -}); - -export const { reducer } = slice; -export const { setIndexPattern, setSearchField } = slice.actions; - -// TODO: Temporary validate function -// Need to identify how to get fieldCounts to use the standard filter and group functions -function isVisualizable(field: IndexPatternField): boolean { - const isAggregatable = field.aggregatable === true; - const isNotScripted = !field.scripted; - const isAllowed = ALLOWED_FIELDS.includes(field.type); - - return isAggregatable && isNotScripted && isAllowed; -} diff --git a/src/plugins/wizard/public/application/utils/state_management/hooks.ts b/src/plugins/wizard/public/application/utils/state_management/hooks.ts index 823c34528c90..607fe05b1623 100644 --- a/src/plugins/wizard/public/application/utils/state_management/hooks.ts +++ b/src/plugins/wizard/public/application/utils/state_management/hooks.ts @@ -6,6 +6,6 @@ import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store'; -// Use throughout your app instead of plain `useDispatch` and `useSelector` +// Use throughout the app instead of plain `useDispatch` and `useSelector` export const useTypedDispatch = () => useDispatch<AppDispatch>(); export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector; diff --git a/src/plugins/wizard/public/application/utils/state_management/preload.ts b/src/plugins/wizard/public/application/utils/state_management/preload.ts index 21ebd13ff82f..d9cefa21a064 100644 --- a/src/plugins/wizard/public/application/utils/state_management/preload.ts +++ b/src/plugins/wizard/public/application/utils/state_management/preload.ts @@ -5,21 +5,18 @@ import { PreloadedState } from '@reduxjs/toolkit'; import { WizardServices } from '../../..'; -import { getPreloadedState as getPreloadedDatasourceState } from './datasource_slice'; +import { getPreloadedState as getPreloadedStyleState } from './style_slice'; import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; -import { getPreloadedState as getPreloadedConfigState } from './config_slice'; import { RootState } from './store'; export const getPreloadedState = async ( services: WizardServices ): Promise<PreloadedState<RootState>> => { - const dataSourceState = await getPreloadedDatasourceState(services); + const styleState = await getPreloadedStyleState(services); const visualizationState = await getPreloadedVisualizationState(services); - const configState = await getPreloadedConfigState(services); return { - dataSource: dataSourceState, + style: styleState, visualization: visualizationState, - config: configState, }; }; diff --git a/src/plugins/wizard/public/application/utils/state_management/store.ts b/src/plugins/wizard/public/application/utils/state_management/store.ts index 4fa56c1a7c97..29af0e9d73b5 100644 --- a/src/plugins/wizard/public/application/utils/state_management/store.ts +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -4,15 +4,13 @@ */ import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; -import { reducer as dataSourceReducer } from './datasource_slice'; -import { reducer as configReducer } from './config_slice'; +import { reducer as styleReducer } from './style_slice'; import { reducer as visualizationReducer } from './visualization_slice'; import { WizardServices } from '../../..'; import { getPreloadedState } from './preload'; const rootReducer = combineReducers({ - dataSource: dataSourceReducer, - config: configReducer, + style: styleReducer, visualization: visualizationReducer, }); diff --git a/src/plugins/wizard/public/application/utils/state_management/style_slice.ts b/src/plugins/wizard/public/application/utils/state_management/style_slice.ts new file mode 100644 index 000000000000..98a425184d45 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/style_slice.ts @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { WizardServices } from '../../../types'; + +type StyleState<T = any> = T; + +const initialState = {} as StyleState; + +export const getPreloadedState = async ({ types, data }: WizardServices): Promise<StyleState> => { + let preloadedState = initialState; + + const defaultVisualization = types.all()[0]; + const defaultState = defaultVisualization.ui.containerConfig.style.defaults; + if (defaultState) { + preloadedState = defaultState; + } + + return preloadedState; +}; + +export const styleSlice = createSlice({ + name: 'style', + initialState, + reducers: { + setState<T>(state: T, action: PayloadAction<StyleState<T>>) { + return action.payload; + }, + updateState<T>(state: T, action: PayloadAction<Partial<StyleState<T>>>) { + state = { + ...state, + ...action.payload, + }; + }, + }, +}); + +// Exposing the state functions as generics +export const setState = styleSlice.actions.setState as <T>(payload: T) => PayloadAction<T>; +export const updateState = styleSlice.actions.updateState as <T>( + payload: Partial<T> +) => PayloadAction<Partial<T>>; + +export const { reducer } = styleSlice; diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts index 692f9434c8de..b5a4b4e93326 100644 --- a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -4,22 +4,39 @@ */ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { CreateAggConfigParams } from 'src/plugins/data/common'; import { WizardServices } from '../../../types'; interface VisualizationState { - activeVisualization: string | null; + indexPattern?: string; + searchField: string; + activeVisualization?: { + name: string; + aggConfigParams: CreateAggConfigParams[]; + activeAgg?: CreateAggConfigParams; + }; } const initialState: VisualizationState = { - activeVisualization: null, + searchField: '', }; -export const getPreloadedState = async ({ types }: WizardServices): Promise<VisualizationState> => { +export const getPreloadedState = async ({ + types, + data, +}: WizardServices): Promise<VisualizationState> => { const preloadedState = { ...initialState }; const defaultVisualization = types.all()[0]; - if (defaultVisualization) { - preloadedState.activeVisualization = defaultVisualization.name; + const defaultIndexPattern = await data.indexPatterns.getDefault(); + const name = defaultVisualization.name; + if (name && defaultIndexPattern) { + preloadedState.activeVisualization = { + name, + aggConfigParams: [], + }; + + preloadedState.indexPattern = defaultIndexPattern.id; } return preloadedState; @@ -29,11 +46,61 @@ export const slice = createSlice({ name: 'visualization', initialState, reducers: { - setActiveVisualization: (state, action: PayloadAction<string>) => { + setActiveVisualization: ( + state, + action: PayloadAction<VisualizationState['activeVisualization']> + ) => { state.activeVisualization = action.payload; }, + setIndexPattern: (state, action: PayloadAction<string>) => { + state.indexPattern = action.payload; + state.activeVisualization!.aggConfigParams = []; + }, + setSearchField: (state, action: PayloadAction<string>) => { + state.searchField = action.payload; + }, + createAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams>) => { + state.activeVisualization!.activeAgg = action.payload; + }, + saveAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams>) => { + delete state.activeVisualization!.activeAgg; + + // TODO: Impliment reducer + }, + reorderAggConfigParams: ( + state, + action: PayloadAction<{ + sourceId: string; + destinationId: string; + }> + ) => { + const { sourceId, destinationId } = action.payload; + const aggParams = state.activeVisualization!.aggConfigParams; + const newAggs = [...aggParams]; + const destinationIndex = newAggs.findIndex((agg) => agg.id === destinationId); + newAggs.splice( + destinationIndex, + 0, + newAggs.splice( + aggParams.findIndex((agg) => agg.id === sourceId), + 1 + )[0] + ); + + state.activeVisualization!.aggConfigParams = newAggs; + }, + updateAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams[]>) => { + state.activeVisualization!.aggConfigParams = action.payload; + }, }, }); export const { reducer } = slice; -export const { setActiveVisualization } = slice.actions; +export const { + setActiveVisualization, + setIndexPattern, + setSearchField, + createAggConfigParams, + updateAggConfigParams, + reorderAggConfigParams, +} = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/use/index.ts b/src/plugins/wizard/public/application/utils/use/index.ts index d82ba978902d..2893ab0d11ff 100644 --- a/src/plugins/wizard/public/application/utils/use/index.ts +++ b/src/plugins/wizard/public/application/utils/use/index.ts @@ -4,3 +4,4 @@ */ export { useVisualizationType } from './use_visualization_type'; +export { useIndexPattern } from './use_index_pattern'; diff --git a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx new file mode 100644 index 000000000000..e02f543d6c15 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { useCallback, useEffect, useState } from 'react'; +import { IndexPattern } from '../../../../../data/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../types'; +import { useTypedSelector } from '../state_management'; + +export const useIndexPattern = (): IndexPattern | undefined => { + const { indexPattern: indexId = '' } = useTypedSelector((state) => state.visualization); + const [indexPattern, setIndexPattern] = useState<IndexPattern>(); + const { + services: { + data: { indexPatterns }, + }, + } = useOpenSearchDashboards<WizardServices>(); + + const handleIndexUpdate = useCallback(async () => { + const currentIndex = await indexPatterns.get(indexId); + setIndexPattern(currentIndex); + }, [indexId, indexPatterns]); + + useEffect(() => { + handleIndexUpdate(); + }, [handleIndexUpdate]); + + return indexPattern; +}; diff --git a/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts b/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts index fb88c11b49da..002c83759b3c 100644 --- a/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts +++ b/src/plugins/wizard/public/application/utils/use/use_visualization_type.ts @@ -14,10 +14,10 @@ export const useVisualizationType = (): VisualizationType => { services: { types }, } = useOpenSearchDashboards<WizardServices>(); - const visualizationType = types.get(activeVisualization || ''); + const visualizationType = types.get(activeVisualization?.name ?? ''); if (!visualizationType) { - throw new Error('Invalid visualization type ${activeVisualization}'); + throw new Error(`Invalid visualization type ${activeVisualization}`); } return visualizationType; diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 5b309080a872..67b6dea6dc7d 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -21,6 +21,7 @@ import { import { PLUGIN_NAME } from '../common'; import { TypeService } from './services/type_service'; import { getPreloadedStore } from './application/utils/state_management'; +import { setAggService, setIndexPatterns } from './plugin_services'; export class WizardPlugin implements @@ -42,12 +43,25 @@ export class WizardPlugin async mount(params: AppMountParameters) { // Load application bundle const { renderApp } = await import('./application'); + // Get start services as specified in opensearch_dashboards.json const [coreStart, pluginsStart] = await core.getStartServices(); - const { data, savedObjects, navigation } = pluginsStart; + const { data, savedObjects, navigation, expressions } = pluginsStart; + + // make sure the index pattern list is up to date + data.indexPatterns.clearCache(); + // make sure a default index pattern exists + // if not, the page will be redirected to management and visualize won't be rendered + // TODO: Add the redirect + await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); + // Register plugin services + setAggService(data.search.aggs); + setIndexPatterns(data.indexPatterns); + + // Register Default Visualizations const { registerDefaultTypes } = await import('./visualizations'); - registerDefaultTypes(typeService.setup()); + registerDefaultTypes(typeService.setup(), pluginsStart); const services: WizardServices = { ...coreStart, @@ -55,17 +69,12 @@ export class WizardPlugin data, savedObjectsPublic: savedObjects, navigation, + expressions, setHeaderActionMenu: params.setHeaderActionMenu, types: typeService.start(), }; - // make sure the index pattern list is up to date - data.indexPatterns.clearCache(); - // make sure a default index pattern exists - // if not, the page will be redirected to management and visualize won't be rendered - // TODO: Add the redirect - await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); - + // Instantiate the store const store = await getPreloadedStore(services); // Render the application diff --git a/src/plugins/wizard/public/plugin_services.ts b/src/plugins/wizard/public/plugin_services.ts new file mode 100644 index 000000000000..67b562d6eca9 --- /dev/null +++ b/src/plugins/wizard/public/plugin_services.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createGetterSetter } from '../../opensearch_dashboards_utils/common'; +import { DataPublicPluginStart } from '../../data/public'; + +export const [getAggService, setAggService] = createGetterSetter< + DataPublicPluginStart['search']['aggs'] +>('data.search.aggs'); + +export const [getIndexPatterns, setIndexPatterns] = createGetterSetter< + DataPublicPluginStart['indexPatterns'] +>('data.indexPatterns'); diff --git a/src/plugins/wizard/public/services/type_service/types.ts b/src/plugins/wizard/public/services/type_service/types.ts index d722bf90dbfa..fae6cdf1c093 100644 --- a/src/plugins/wizard/public/services/type_service/types.ts +++ b/src/plugins/wizard/public/services/type_service/types.ts @@ -2,8 +2,10 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - +import { ReactElement } from 'react'; import { IconType } from '@elastic/eui'; +import { RootState } from '../../application/utils/state_management'; +import { Schemas } from '../../../../vis_default_editor/public'; export enum ContributionTypes { CONTAINER = 'CONTAINER', @@ -25,17 +27,26 @@ type ContainerSchema = any; export type ContainerLocationContribution = { [K in ContainerLocations]: ContainerContribution[] }; -export interface VisualizationTypeOptions { +export interface DataTabConfig { + schemas: Schemas; +} + +export interface StyleTabConfig<T = any> { + defaults: T; + render: () => ReactElement; +} + +export interface VisualizationTypeOptions<T = any> { readonly name: string; readonly title: string; readonly description?: string; readonly icon: IconType; readonly stage?: 'beta' | 'production'; - readonly contributions: { - containers?: Partial<ContainerLocationContribution>; - items?: { - [containerId: string]: ContainerSchema[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency + readonly ui: { + containerConfig: { + data: DataTabConfig; + style: StyleTabConfig<T>; }; }; - // pipeline: Expression; + readonly toExpression: (state: RootState) => Promise<string | undefined>; } diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx index 830d598af754..6d362a229d23 100644 --- a/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx +++ b/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx @@ -5,8 +5,9 @@ import React from 'react'; import { VisualizationTypeOptions } from './types'; -import { VisualizationType, DEFAULT_CONTAINERS } from './visualization_type'; +import { VisualizationType } from './visualization_type'; +// TODO: Update service tests describe('VisualizationType', () => { const DEFAULT_VIZ_PROPS = { name: 'some-name', @@ -15,81 +16,81 @@ describe('VisualizationType', () => { contributions: {}, }; - const createVizType = (props?: Partial<VisualizationTypeOptions>): VisualizationTypeOptions => { - return { - ...DEFAULT_VIZ_PROPS, - ...props, - }; - }; + // const createVizType = (props?: Partial<VisualizationTypeOptions>): VisualizationTypeOptions => { + // return { + // ...DEFAULT_VIZ_PROPS, + // ...props, + // }; + // }; - test('should have default container contributions if none are provided', () => { - const viz = new VisualizationType(createVizType()); + // test('should have default container contributions if none are provided', () => { + // const viz = new VisualizationType(createVizType()); - expect(viz.contributions.containers).toEqual(DEFAULT_CONTAINERS); - }); + // expect(viz.contributions.containers).toEqual(DEFAULT_CONTAINERS); + // }); - test('should have replace default container contributions when provided', () => { - const defaultContainer = DEFAULT_CONTAINERS.sidePanel[0]; - const viz = new VisualizationType( - createVizType({ - contributions: { - containers: { - sidePanel: [ - { - id: defaultContainer.id, - name: 'Test', - Component: <div>Test</div>, - }, - ], - }, - }, - }) - ); + // test('should have replace default container contributions when provided', () => { + // const defaultContainer = DEFAULT_CONTAINERS.sidePanel[0]; + // const viz = new VisualizationType( + // createVizType({ + // contributions: { + // containers: { + // sidePanel: [ + // { + // id: defaultContainer.id, + // name: 'Test', + // Component: <div>Test</div>, + // }, + // ], + // }, + // }, + // }) + // ); - const container = viz.contributions.containers.sidePanel.find( - ({ id }) => id === defaultContainer.id - ); - expect(container).toMatchInlineSnapshot(` - Object { - "Component": <div> - Test - </div>, - "id": "data_tab", - "name": "Test", - } - `); - }); + // const container = viz.contributions.containers.sidePanel.find( + // ({ id }) => id === defaultContainer.id + // ); + // expect(container).toMatchInlineSnapshot(` + // Object { + // "Component": <div> + // Test + // </div>, + // "id": "data_tab", + // "name": "Test", + // } + // `); + // }); - test('should register new container if provided', () => { - const viz = new VisualizationType( - createVizType({ - contributions: { - containers: { - sidePanel: [ - { - id: 'test_id', - name: 'Test', - Component: <div>Test</div>, - }, - ], - }, - }, - }) - ); + // test('should register new container if provided', () => { + // const viz = new VisualizationType( + // createVizType({ + // contributions: { + // containers: { + // sidePanel: [ + // { + // id: 'test_id', + // name: 'Test', + // Component: <div>Test</div>, + // }, + // ], + // }, + // }, + // }) + // ); - const container = viz.contributions.containers.sidePanel.find(({ id }) => id === 'test_id'); - const containerNames = viz.contributions.containers.sidePanel.map(({ name }) => name); - const defaultContainerNames = DEFAULT_CONTAINERS.sidePanel.map(({ name }) => name); + // const container = viz.contributions.containers.sidePanel.find(({ id }) => id === 'test_id'); + // const containerNames = viz.contributions.containers.sidePanel.map(({ name }) => name); + // const defaultContainerNames = DEFAULT_CONTAINERS.sidePanel.map(({ name }) => name); - expect(containerNames).toEqual([...defaultContainerNames, 'Test']); - expect(container).toMatchInlineSnapshot(` - Object { - "Component": <div> - Test - </div>, - "id": "test_id", - "name": "Test", - } - `); - }); + // expect(containerNames).toEqual([...defaultContainerNames, 'Test']); + // expect(container).toMatchInlineSnapshot(` + // Object { + // "Component": <div> + // Test + // </div>, + // "id": "test_id", + // "name": "Test", + // } + // `); + // }); }); diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.tsx index 873931db7e89..2ae1d25a4d96 100644 --- a/src/plugins/wizard/public/services/type_service/visualization_type.tsx +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -2,69 +2,18 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { DATA_TAB_ID, DataTab, STYLE_TAB_ID, StyleTab } from '../../application/contributions'; -import { - ContainerLocationContribution, - ContainerLocations, - VisualizationTypeOptions, -} from './types'; -import { mergeArrays } from './utils'; +import { VisualizationTypeOptions } from './types'; -export const DEFAULT_CONTAINERS: ContainerLocationContribution = { - sidePanel: [ - { - id: DATA_TAB_ID, - name: 'Data', - Component: <DataTab />, - }, - { - id: STYLE_TAB_ID, - name: 'Style', - Component: <StyleTab />, - }, - ], - toolbar: [], -}; +type IVisualizationType = VisualizationTypeOptions; -interface IVisualizationType extends Required<VisualizationTypeOptions> { - contributions: { - containers: ContainerLocationContribution; - }; -} export class VisualizationType implements IVisualizationType { public readonly name; public readonly title; public readonly description; public readonly icon; public readonly stage; - public readonly contributions; - - private processContributions(contributions: VisualizationTypeOptions['contributions']) { - const uiContainers: ContainerLocationContribution = { - sidePanel: [], - toolbar: [], - }; - const { containers, items } = contributions; - - // Validate and populate containers for each container location - Object.keys(uiContainers).forEach((location) => { - const typedLocation = location as ContainerLocations; - const vizContainers = containers?.[typedLocation]; - - const mergedContainers = mergeArrays( - DEFAULT_CONTAINERS[typedLocation], - vizContainers || [], - 'id' - ); - uiContainers[typedLocation] = mergedContainers; - }); - - return { - containers: uiContainers, - items, - }; - } + public readonly ui; + public readonly toExpression; constructor(options: VisualizationTypeOptions) { this.name = options.name; @@ -72,6 +21,7 @@ export class VisualizationType implements IVisualizationType { this.description = options.description ?? ''; this.icon = options.icon; this.stage = options.stage ?? 'production'; - this.contributions = this.processContributions(options.contributions); + this.ui = options.ui; + this.toExpression = options.toExpression; } } diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts index 07b1e5141c61..14f13d56bdf3 100644 --- a/src/plugins/wizard/public/types.ts +++ b/src/plugins/wizard/public/types.ts @@ -8,6 +8,7 @@ import { AppMountParameters, CoreStart, ToastsStart } from 'opensearch-dashboard import { EmbeddableSetup } from 'src/plugins/embeddable/public'; import { DashboardStart } from 'src/plugins/dashboard/public'; import { VisualizationsSetup } from 'src/plugins/visualizations/public'; +import { ExpressionsStart } from 'src/plugins/expressions/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { DataPublicPluginStart } from '../../data/public'; import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; @@ -23,6 +24,7 @@ export interface WizardPluginStartDependencies { data: DataPublicPluginStart; savedObjects: SavedObjectsStart; dashboard: DashboardStart; + expressions: ExpressionsStart; } export interface WizardServices extends CoreStart { @@ -32,4 +34,5 @@ export interface WizardServices extends CoreStart { navigation: NavigationPublicPluginStart; data: DataPublicPluginStart; types: TypeServiceStart; + expressions: ExpressionsStart; } diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts index cf3151cf1196..b9574fd9a771 100644 --- a/src/plugins/wizard/public/visualizations/index.ts +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -4,11 +4,16 @@ */ import type { TypeServiceSetup } from '../services/type_service'; +import { createMetricConfig } from './metric'; import { createBarChartConfig } from './bar_chart'; import { createPieChartConfig } from './pie_chart'; +import { WizardPluginStartDependencies } from '../types'; -export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { - const visualizationTypes = [createBarChartConfig, createPieChartConfig]; +export function registerDefaultTypes( + typeServiceSetup: TypeServiceSetup, + pluginsStart: WizardPluginStartDependencies +) { + const visualizationTypes = [createMetricConfig]; visualizationTypes.forEach((createTypeConfig) => { typeServiceSetup.createVisualizationType(createTypeConfig()); diff --git a/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx b/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx new file mode 100644 index 000000000000..acfaab045800 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx @@ -0,0 +1,149 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from 'react-intl'; +import produce from 'immer'; +import { Draft } from 'immer'; +import { + ColorRanges, + RangeOption, + SetColorRangeValue, + SwitchOption, +} from '../../../../../charts/public'; +import { useTypedDispatch, useTypedSelector } from '../../../application/utils/state_management'; +import { MetricOptionsDefaults } from '../metric_viz_type'; +import { setState } from '../../../application/utils/state_management/style_slice'; + +function MetricVizOptions() { + const styleState = useTypedSelector((state) => state.style) as MetricOptionsDefaults; + const dispatch = useTypedDispatch(); + const { metric } = styleState; + + const setOption = useCallback( + (callback: (draft: Draft<typeof styleState>) => void) => { + const newState = produce(styleState, callback); + dispatch(setState<MetricOptionsDefaults>(newState)); + }, + [dispatch, styleState] + ); + + return ( + <div> + <EuiPanel paddingSize="s"> + <EuiTitle size="xs"> + <h3> + <FormattedMessage id="visTypeMetric.params.settingsTitle" defaultMessage="Settings" /> + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + + <SwitchOption + label={i18n.translate('visTypeMetric.params.percentageModeLabel', { + defaultMessage: 'Percentage mode', + })} + paramName="percentageMode" + value={metric.percentageMode} + setValue={(_, value) => + setOption((draft) => { + draft.metric.percentageMode = value; + }) + } + /> + + <SwitchOption + label={i18n.translate('visTypeMetric.params.showTitleLabel', { + defaultMessage: 'Show title', + })} + paramName="show" + value={metric.labels.show} + setValue={(_, value) => + setOption((draft) => { + draft.metric.labels.show = value; + }) + } + /> + </EuiPanel> + + <EuiSpacer size="s" /> + + {/* TODO: Reintroduce the other style properties */} + {/* <EuiPanel paddingSize="s"> + <EuiTitle size="xs"> + <h3> + <FormattedMessage id="visTypeMetric.params.rangesTitle" defaultMessage="Ranges" /> + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + + <ColorRanges + data-test-subj="metricColorRange" + colorsRange={metric.colorsRange} + setValue={setMetric('labels') as SetColorRangeValue} + setTouched={setMetric('touched')} + setValidity={setMetric('validity')} + /> + + <EuiFormRow fullWidth display="rowCompressed" label={metricColorModeLabel}> + <EuiButtonGroup + buttonSize="compressed" + idSelected={metric.metricColorMode} + isDisabled={metric.colorsRange.length === 1} + isFullWidth={true} + legend={metricColorModeLabel} + options={vis.type.editorConfig.collections.metricColorMode} + onChange={setColorMode} + /> + </EuiFormRow> + + <ColorSchemaOptions + colorSchema={metric.colorSchema} + colorSchemas={vis.type.editorConfig.collections.colorSchemas} + disabled={ + metric.colorsRange.length === 1 || + metric.metricColorMode === ColorModes.NONE + } + invertColors={metric.invertColors} + setValue={setMetricValue as SetColorSchemaOptionsValue} + showHelpText={false} + uiState={uiState} + /> + </EuiPanel> + + <EuiSpacer size="s" /> */} + + <EuiPanel paddingSize="s"> + <EuiTitle size="xs"> + <h3> + <FormattedMessage id="visTypeMetric.params.style.styleTitle" defaultMessage="Style" /> + </h3> + </EuiTitle> + <EuiSpacer size="s" /> + + <RangeOption + label={i18n.translate('visTypeMetric.params.style.fontSizeLabel', { + defaultMessage: 'Metric font size in points', + })} + min={12} + max={120} + paramName="fontSize" + value={metric.style.fontSize} + setValue={(_, value) => + setOption((draft) => { + draft.metric.style.fontSize = value; + }) + } + showInput={true} + showLabels={true} + showValue={false} + /> + </EuiPanel> + </div> + ); +} + +export { MetricVizOptions }; diff --git a/src/plugins/wizard/public/visualizations/metric/index.ts b/src/plugins/wizard/public/visualizations/metric/index.ts new file mode 100644 index 000000000000..8efccb2639d7 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { createMetricConfig } from './metric_viz_type'; diff --git a/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts new file mode 100644 index 000000000000..eabd9df1cb2f --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { Schemas } from '../../../../vis_default_editor/public'; +import { AggGroupNames } from '../../../../data/public'; +import { ColorModes, ColorSchemas } from '../../../../charts/public'; +import { MetricVizOptions } from './components/metric_viz_options'; +import { VisualizationTypeOptions } from '../../services/type_service'; +import { toExpression } from './to_expression'; + +export interface MetricOptionsDefaults { + addTooltip: boolean; + addLegend: boolean; + type: 'metric'; + metric: { + percentageMode: boolean; + useRanges: boolean; + colorSchema: ColorSchemas; + metricColorMode: ColorModes; + colorsRange: [{ from: number; to: number }]; + labels: { + show: boolean; + }; + invertColors: boolean; + style: { + bgFill: string; + bgColor: boolean; + labelColor: boolean; + subText: string; + fontSize: number; + }; + }; +} + +export const createMetricConfig = (): VisualizationTypeOptions<MetricOptionsDefaults> => ({ + name: 'metric', + title: 'Metric', + icon: 'visMetric', + description: 'Display metric visualizations', + toExpression, + ui: { + containerConfig: { + data: { + schemas: new Schemas([ + { + group: AggGroupNames.Metrics, + name: 'metric', + title: i18n.translate('visTypeMetric.schemas.metricTitle', { + defaultMessage: 'Metric', + }), + min: 1, + aggFilter: [ + '!std_dev', + '!geo_centroid', + '!derivative', + '!serial_diff', + '!moving_avg', + '!cumulative_sum', + '!geo_bounds', + ], + aggSettings: { + top_hits: { + allowStrings: true, + }, + }, + defaults: { + aggType: 'avg', + }, + }, + { + group: AggGroupNames.Buckets, + name: 'group', + title: i18n.translate('visTypeMetric.schemas.splitGroupTitle', { + defaultMessage: 'Split group', + }), + min: 0, + max: 1, + aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], + defaults: { + aggType: 'count', + }, + }, + ]), + }, + style: { + defaults: { + addTooltip: true, + addLegend: false, + type: 'metric', + metric: { + percentageMode: false, + useRanges: false, + colorSchema: ColorSchemas.GreenToRed, + metricColorMode: ColorModes.NONE, + colorsRange: [{ from: 0, to: 10000 }], + labels: { + show: true, + }, + invertColors: false, + style: { + bgFill: '#000', + bgColor: false, + labelColor: false, + subText: '', + fontSize: 60, + }, + }, + }, + render: MetricVizOptions, + }, + }, + }, +}); diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts new file mode 100644 index 000000000000..f0614801493e --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -0,0 +1,177 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { cloneDeep } from 'lodash'; +import { SchemaConfig } from '../../../../visualizations/public'; +import { MetricVisExpressionFunctionDefinition } from '../../../../vis_type_metric/public'; +import { + AggConfigs, + IAggConfig, + OpenSearchaggsExpressionFunctionDefinition, +} from '../../../../data/common'; +import { buildExpression, buildExpressionFunction } from '../../../../expressions/public'; +import { RootState } from '../../application/utils/state_management'; +import { MetricOptionsDefaults } from './metric_viz_type'; +import { getAggService, getIndexPatterns } from '../../plugin_services'; + +const prepareDimension = (params: SchemaConfig) => { + const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor }); + + if (params.format) { + visdimension.addArgument('format', params.format.id); + visdimension.addArgument('formatParams', JSON.stringify(params.format.params)); + } + + return buildExpression([visdimension]); +}; + +// TODO: Update to the common getShemas from src/plugins/visualizations/public/legacy/build_pipeline.ts +// And move to a common location accessible by all the visualizations +const getVisSchemas = (aggConfigs: AggConfigs): any => { + const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { + const hasSubAgg = [ + 'derivative', + 'moving_avg', + 'serial_diff', + 'cumulative_sum', + 'sum_bucket', + 'avg_bucket', + 'min_bucket', + 'max_bucket', + ].includes(agg.type.name); + + const formatAgg = hasSubAgg + ? agg.params.customMetric || agg.aggConfigs.getRequestAggById(agg.params.metricAgg) + : agg; + + const params = {}; + + const label = agg.makeLabel && agg.makeLabel(); + + return { + accessor, + format: formatAgg.toSerializedFieldFormat(), + params, + label, + aggType: agg.type.name, + }; + }; + + let cnt = 0; + const schemas: any = { + metric: [], + }; + + if (!aggConfigs) { + return schemas; + } + + const responseAggs = aggConfigs.getResponseAggs(); + responseAggs.forEach((agg) => { + const schemaName = agg.schema; + + if (!schemaName) { + cnt++; + return; + } + + if (!schemas[schemaName]) { + schemas[schemaName] = []; + } + + schemas[schemaName]!.push(createSchemaConfig(cnt++, agg)); + }); + + return schemas; +}; + +interface MetricRootState extends RootState { + style: MetricOptionsDefaults; +} + +export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { + const { activeVisualization, indexPattern: indexId = '' } = visualization; + const { aggConfigParams } = activeVisualization || {}; + + if (!aggConfigParams || !aggConfigParams.length) return; + + const indexPatternsService = getIndexPatterns(); + const indexPattern = await indexPatternsService.get(indexId); + const aggConfigs = getAggService().createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + + // soon this becomes: const opensearchaggs = vis.data.aggs!.toExpressionAst(); + const opensearchaggs = buildExpressionFunction<OpenSearchaggsExpressionFunctionDefinition>( + 'opensearchaggs', + { + index: indexId, + metricsAtAllLevels: false, + partialRows: false, + aggConfigs: JSON.stringify(aggConfigs.aggs), + includeFormatHints: false, + } + ); + + // TODO: Update to use the getVisSchemas function from the Visualizations plugin + // const schemas = getVisSchemas(vis, params); + + const { + percentageMode, + useRanges, + colorSchema, + metricColorMode, + colorsRange, + labels, + invertColors, + style, + } = styleState.metric; + + const schemas = getVisSchemas(aggConfigs); + + // fix formatter for percentage mode + if (percentageMode === true) { + schemas.metric.forEach((metric: SchemaConfig) => { + metric.format = { id: 'percent' }; + }); + } + + // TODO: ExpressionFunctionDefinitions mark all arguments as required even though the function marks most as optional + // Update buildExpressionFunction to correctly handle optional arguments + // @ts-expect-error + const metricVis = buildExpressionFunction<MetricVisExpressionFunctionDefinition>('metricVis', { + percentageMode, + colorSchema, + colorMode: metricColorMode, + useRanges, + invertColors, + showLabels: labels && labels.show, + }); + + if (style) { + metricVis.addArgument('bgFill', style.bgFill); + metricVis.addArgument('font', buildExpression(`font size=${style.fontSize}`)); + metricVis.addArgument('subText', style.subText); + } + + if (colorsRange) { + colorsRange.forEach((range: any) => { + metricVis.addArgument( + 'colorRange', + buildExpression(`range from=${range.from} to=${range.to}`) + ); + }); + } + + if (schemas.group) { + metricVis.addArgument('bucket', prepareDimension(schemas.group[0])); + } + + schemas.metric.forEach((metric: SchemaConfig) => { + metricVis.addArgument('metric', prepareDimension(metric)); + }); + + const ast = buildExpression([opensearchaggs, metricVis]); + + return ast.toString(); +}; From 40dba533e3ff8a3da5c82103abdf29fe0ac09d84 Mon Sep 17 00:00:00 2001 From: Brooke <97559014+CPTNB@users.noreply.github.com> Date: Wed, 22 Jun 2022 17:55:31 -0700 Subject: [PATCH 04/47] Adds the data source selector and a useIndexPatterns hook (#1763) Signed-off-by: Brooke Green <cptn@amazon.com> --- .../wizard/public/application/_variables.scss | 4 +- .../wizard/public/application/app.scss | 2 +- .../components/data_source_select.tsx | 51 ++++++ .../components/searchable_dropdown.scss | 26 +++ .../components/searchable_dropdown.tsx | 170 ++++++++++++++++++ .../application/components/side_nav.scss | 10 +- .../application/components/side_nav.tsx | 44 +---- .../public/application/utils/use/index.ts | 2 +- .../utils/use/use_index_pattern.tsx | 44 +++++ .../wizard/public/assets/index_pattern.svg | 4 + src/plugins/wizard/public/types.ts | 2 +- src/plugins/wizard/tsconfig.json | 74 ++++++++ 12 files changed, 387 insertions(+), 46 deletions(-) create mode 100644 src/plugins/wizard/public/application/components/data_source_select.tsx create mode 100644 src/plugins/wizard/public/application/components/searchable_dropdown.scss create mode 100644 src/plugins/wizard/public/application/components/searchable_dropdown.tsx create mode 100644 src/plugins/wizard/public/assets/index_pattern.svg create mode 100644 src/plugins/wizard/tsconfig.json diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss index c1b3646e8e49..6ffa04d0eb4a 100644 --- a/src/plugins/wizard/public/application/_variables.scss +++ b/src/plugins/wizard/public/application/_variables.scss @@ -1,3 +1,5 @@ @import '@elastic/eui/src/global_styling/variables/header'; +@import '@elastic/eui/src/global_styling/variables/form'; -$osdHeaderOffset: $euiHeaderHeightCompensation * 2; \ No newline at end of file +$osdHeaderOffset: $euiHeaderHeightCompensation * 2; +$wizSideNavWidth: 470px; diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss index 4f05c6ecd7a2..3e2a28aca700 100644 --- a/src/plugins/wizard/public/application/app.scss +++ b/src/plugins/wizard/public/application/app.scss @@ -4,7 +4,7 @@ padding: 0; display: grid; grid-template-rows: min-content 1fr; - grid-template-columns: 470px 1fr; + grid-template-columns: $wizSideNavWidth 1fr; grid-template-areas: "topNav topNav" "sideNav workspace" diff --git a/src/plugins/wizard/public/application/components/data_source_select.tsx b/src/plugins/wizard/public/application/components/data_source_select.tsx new file mode 100644 index 000000000000..3bbae08573b9 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_source_select.tsx @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiIcon } from '@elastic/eui'; +import { SearchableDropdown, SearchableDropdownOption } from './searchable_dropdown'; +import { useIndexPatterns } from '../utils/use'; +import indexPatternSvg from '../../assets/index_pattern.svg'; +import { useTypedDispatch } from '../utils/state_management'; +import { setIndexPattern } from '../utils/state_management/visualization_slice'; +import { IndexPattern } from '../../../../data/public'; + +function indexPatternEquality(A?: SearchableDropdownOption, B?: SearchableDropdownOption): boolean { + return !A || !B ? false : A.id === B.id; +} + +function toSearchableDropdownOption(indexPattern: IndexPattern): SearchableDropdownOption { + return { + id: indexPattern.id || '', + label: indexPattern.title, + searchableLabel: indexPattern.title, + prepend: <EuiIcon type={indexPatternSvg} />, + }; +} + +export const DataSourceSelect = () => { + const { indexPatterns, loading, error, selected } = useIndexPatterns(); + const dispatch = useTypedDispatch(); + + return ( + <SearchableDropdown + selected={selected !== undefined ? toSearchableDropdownOption(selected) : undefined} + onChange={(option) => { + const foundOption = indexPatterns.filter((s) => s.id === option.id)[0]; + if (foundOption !== undefined && typeof foundOption.id === 'string') { + dispatch(setIndexPattern(foundOption.id)); + } + }} + prepend={i18n.translate('wizard.nav.dataSource.selector.title', { + defaultMessage: 'Data Source', + })} + error={error} + loading={loading} + options={indexPatterns.map(toSearchableDropdownOption)} + equality={indexPatternEquality} + /> + ); +}; diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss new file mode 100644 index 000000000000..6de99e91356d --- /dev/null +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -0,0 +1,26 @@ +@import "../variables"; + +.searchableDropdown { + overflow: "hidden"; +} + +.searchableDropdown .euiPopover, +.searchableDropdown .euiPopover__anchor { + width: 100%; +} + +.searchableDropdown--fixedWidthChild { + width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2) ; +} + +.searchableDropdown--topDisplay { + padding-right: $euiSizeL; +} + + +.searchableDropdown--selectableWrapper .euiSelectableList { + // When clicking on the selectable content it will "highlight" itself with a box shadow + // This turns that off + box-shadow: none !important; + margin: ($euiFormControlPadding * -1) - 4; +} diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx new file mode 100644 index 000000000000..da6795335088 --- /dev/null +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx @@ -0,0 +1,170 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect } from 'react'; +import { + EuiLoadingSpinner, + EuiFormControlLayout, + EuiPopoverTitle, + EuiButtonEmpty, + EuiPopover, + EuiSelectable, + EuiTextColor, +} from '@elastic/eui'; +import './searchable_dropdown.scss'; + +export interface SearchableDropdownOption { + id: string; + label: string; + searchableLabel: string; + prepend: any; +} + +interface SearchableDropdownProps { + selected?: SearchableDropdownOption; + onChange: (selection) => void; + options: SearchableDropdownOption[]; + loading: boolean; + error?: Error; + prepend: string; + // not just the first time! + onOpen?: () => void; + equality: (A, B) => boolean; +} + +type DisplayError = any; + +function displayError(error: DisplayError) { + return typeof error === 'object' ? error.toString() : <>{error}</>; +} + +export const SearchableDropdown = ({ + onChange, + equality, + selected, + options, + error, + loading, + prepend, + onOpen, +}: SearchableDropdownProps) => { + const [localOptions, setLocalOptions] = useState<any[] | undefined>(undefined); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onButtonClick = () => { + if (!isPopoverOpen && typeof onOpen === 'function') { + onOpen(); + } + setIsPopoverOpen(!isPopoverOpen); + }; + const closePopover = () => setIsPopoverOpen(false); + + function selectNewOption(newOptions) { + // alright, the EUI Selectable is pretty ratchet + // this is as smarmy as it is because it needs to be + + // first go through and count all the "checked" options + const selectedCount = newOptions.filter((o) => o.checked === 'on').length; + + // if the count is 0, the user just "unchecked" our selection and we can just do nothing + if (selectedCount === 0) { + setIsPopoverOpen(false); + return; + } + + // then, if there's more than two selections, the Selectable left the previous selection as "checked" + // so we need to go and "uncheck" it + for (let i = 0; i < newOptions.length; i++) { + if (equality(newOptions[i], selected) && selectedCount > 1) { + delete newOptions[i].checked; + } + } + + // finally, we can pick the checked option as the actual selection + const newSelection = newOptions.filter((o) => o.checked === 'on')[0]; + + setLocalOptions(newOptions); + setIsPopoverOpen(false); + onChange(newSelection); + } + + useEffect(() => { + setLocalOptions( + options.map((o) => ({ + ...o, + checked: equality(o, selected) ? 'on' : undefined, + })) + ); + }, [selected, options, equality]); + + const listDisplay = (list, search) => + loading ? ( + <div style={{ textAlign: 'center' }}> + <EuiLoadingSpinner /> + </div> + ) : error !== undefined ? ( + displayError(error) + ) : ( + <> + <EuiPopoverTitle paddingSize="s" className="wizPopoverTitle"> + {search} + </EuiPopoverTitle> + {list} + </> + ); + + const selectable = ( + <div className="searchableDropdown--selectableWrapper"> + <EuiSelectable + aria-label="Selectable options" + searchable + options={localOptions} + onChange={selectNewOption} + listProps={{ + showIcons: false, + }} + > + {listDisplay} + </EuiSelectable> + </div> + ); + + const selectedText = + selected === undefined ? ( + <EuiTextColor color="subdued">{loading ? 'Loading' : 'Select an option'}</EuiTextColor> + ) : ( + <> + {selected.prepend} {selected.label} + </> + ); + + const selectedView = ( + <EuiButtonEmpty + color="text" + style={{ textAlign: 'left' }} + className="searchableDropdown--topDisplay" + onClick={onButtonClick} + > + {selectedText} + </EuiButtonEmpty> + ); + + const formControl = <EuiFormControlLayout + title={selected === undefined ? "Select an option" : selected.label} + isLoading={loading} + fullWidth={true} + style={{ cursor: 'pointer' }} + prepend={prepend} + icon={{ type: 'arrowDown', side: 'right' }} + readOnly={true} + >{selectedView}</EuiFormControlLayout> + + return ( + <div className="searchableDropdown"> + <EuiPopover button={formControl} isOpen={isPopoverOpen} closePopover={closePopover}> + <div className="searchableDropdown--fixedWidthChild">{selectable}</div> + </EuiPopover> + </div> + ); +}; diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss index caa24e30b395..7291a79eb726 100644 --- a/src/plugins/wizard/public/application/components/side_nav.scss +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -1,4 +1,5 @@ @import "../util"; +@import "../variables"; .wizSidenav { @include scrollNavParent(auto 1fr); @@ -7,10 +8,6 @@ border-right: $euiBorderThin; } -.wizDatasourceSelector { - padding: $euiSize $euiSize 0 $euiSize; -} - .wizSidenavTabs { .euiTab__content { text-transform: capitalize; @@ -22,3 +19,8 @@ @include scrollNavParent; } } + +.wizDatasourceSelect { + max-width: $wizSideNavWidth; + padding: $euiSize $euiSize 0 $euiSize; +} diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx index c74837ceab54..c79f63d7b5c0 100644 --- a/src/plugins/wizard/public/application/components/side_nav.tsx +++ b/src/plugins/wizard/public/application/components/side_nav.tsx @@ -3,35 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { i18n } from '@osd/i18n'; -import { EuiFormLabel, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; -import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { WizardServices } from '../../types'; +import React, { ReactElement } from 'react'; +import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; import './side_nav.scss'; -import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; -import { setIndexPattern } from '../utils/state_management/visualization_slice'; import { useVisualizationType } from '../utils/use'; +import { DataSourceSelect } from './data_source_select'; import { DataTab } from '../contributions'; import { StyleTabConfig } from '../../services/type_service'; export const SideNav = () => { - const { - services: { - data, - savedObjects: { client: savedObjectsClient }, - }, - } = useOpenSearchDashboards<WizardServices>(); - const { IndexPatternSelect } = data.ui; - const { indexPattern: indexPatternId } = useTypedSelector((state) => state.visualization); - const dispatch = useTypedDispatch(); const { ui: { containerConfig }, } = useVisualizationType(); const tabs: EuiTabbedContentTab[] = Object.entries(containerConfig).map( ([containerName, config]) => { - let content = null; + let content: null | ReactElement = null; switch (containerName) { case 'data': content = <DataTab key="containerName" />; @@ -52,27 +39,8 @@ export const SideNav = () => { return ( <section className="wizSidenav"> - <div className="wizDatasourceSelector"> - <EuiFormLabel> - {i18n.translate('wizard.nav.dataSource.selector.title', { - defaultMessage: 'Index Pattern', - })} - </EuiFormLabel> - <IndexPatternSelect - savedObjectsClient={savedObjectsClient} - placeholder={i18n.translate('wizard.nav.dataSource.selector.placeholder', { - defaultMessage: 'Select index pattern', - })} - indexPatternId={indexPatternId || ''} - onChange={async (newIndexPatternId: any) => { - const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); - - if (newIndexPattern) { - dispatch(setIndexPattern(newIndexPatternId)); - } - }} - isClearable={false} - /> + <div className="wizDatasourceSelect"> + <DataSourceSelect /> </div> <EuiTabbedContent tabs={tabs} className="wizSidenavTabs" /> </section> diff --git a/src/plugins/wizard/public/application/utils/use/index.ts b/src/plugins/wizard/public/application/utils/use/index.ts index 2893ab0d11ff..542ae073cde5 100644 --- a/src/plugins/wizard/public/application/utils/use/index.ts +++ b/src/plugins/wizard/public/application/utils/use/index.ts @@ -4,4 +4,4 @@ */ export { useVisualizationType } from './use_visualization_type'; -export { useIndexPattern } from './use_index_pattern'; +export { useIndexPattern, useIndexPatterns } from './use_index_pattern'; diff --git a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx index e02f543d6c15..d5f255fcb10c 100644 --- a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx +++ b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx @@ -28,3 +28,47 @@ export const useIndexPattern = (): IndexPattern | undefined => { return indexPattern; }; + +export const useIndexPatterns = () => { + const { indexPattern: indexId = '' } = useTypedSelector((state) => state.visualization); + const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>([]); + const [loading, setLoading] = useState<boolean>(true); + const [error, setError] = useState<Error | undefined>(undefined); + const { + services: { data }, + } = useOpenSearchDashboards<WizardServices>(); + + let foundSelected; + if (!loading && !error) { + foundSelected = indexPatterns.filter((p) => p.id === indexId)[0]; + if (foundSelected === undefined) { + setError( + new Error("Attempted to select an index pattern that wasn't in the index pattern list") + ); + } + } + + useEffect(() => { + const handleUpdate = async () => { + try { + const ids = await data.indexPatterns.getIds(indexId); + const patterns = await Promise.all(ids.map((id) => data.indexPatterns.get(id))); + setIndexPatterns(patterns); + } catch (e) { + setError(e); + } finally { + setLoading(false); + } + }; + handleUpdate(); + // we want to run this hook exactly once, which you do by an empty dep array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { + indexPatterns, + error, + loading, + selected: foundSelected!, + }; +}; diff --git a/src/plugins/wizard/public/assets/index_pattern.svg b/src/plugins/wizard/public/assets/index_pattern.svg new file mode 100644 index 000000000000..b1f140a38632 --- /dev/null +++ b/src/plugins/wizard/public/assets/index_pattern.svg @@ -0,0 +1,4 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 6.125H8.75C9.47487 6.125 10.0625 5.53737 10.0625 4.8125V1.3125C10.0625 0.587626 9.47487 0 8.75 0H5.25C4.52513 0 3.9375 0.587626 3.9375 1.3125V4.8125C3.9375 5.53737 4.52513 6.125 5.25 6.125ZM4.8125 1.3125C4.8125 1.07088 5.00838 0.875 5.25 0.875H8.75C8.99162 0.875 9.1875 1.07088 9.1875 1.3125V4.8125C9.1875 5.05412 8.99162 5.25 8.75 5.25H5.25C5.00838 5.25 4.8125 5.05412 4.8125 4.8125V1.3125ZM7.4375 7V7.875H12.6875V10.5H11.8125V8.75H7.4375V10.5H6.5625V8.75H2.1875V10.5H1.3125V7.875H6.5625V7H7.4375Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3125 1.75H5.6875V2.625H8.3125V1.75ZM1.75 14C1.02513 14 0.4375 13.4124 0.4375 12.6875C0.4375 11.9626 1.02513 11.375 1.75 11.375C2.47487 11.375 3.0625 11.9626 3.0625 12.6875C3.0625 13.4124 2.47487 14 1.75 14ZM1.75 12.25C1.50838 12.25 1.3125 12.4459 1.3125 12.6875C1.3125 12.9291 1.50838 13.125 1.75 13.125C1.99162 13.125 2.1875 12.9291 2.1875 12.6875C2.1875 12.4459 1.99162 12.25 1.75 12.25ZM7 14C6.27513 14 5.6875 13.4124 5.6875 12.6875C5.6875 11.9626 6.27513 11.375 7 11.375C7.72487 11.375 8.3125 11.9626 8.3125 12.6875C8.3125 13.4124 7.72487 14 7 14ZM7 12.25C6.75838 12.25 6.5625 12.4459 6.5625 12.6875C6.5625 12.9291 6.75838 13.125 7 13.125C7.24162 13.125 7.4375 12.9291 7.4375 12.6875C7.4375 12.4459 7.24162 12.25 7 12.25ZM12.25 14C11.5251 14 10.9375 13.4124 10.9375 12.6875C10.9375 11.9626 11.5251 11.375 12.25 11.375C12.9749 11.375 13.5625 11.9626 13.5625 12.6875C13.5625 13.4124 12.9749 14 12.25 14ZM12.25 12.25C12.0084 12.25 11.8125 12.4459 11.8125 12.6875C11.8125 12.9291 12.0084 13.125 12.25 13.125C12.4916 13.125 12.6875 12.9291 12.6875 12.6875C12.6875 12.4459 12.4916 12.25 12.25 12.25ZM5.6875 3.5H8.3125V4.375H5.6875V3.5Z" fill="#017D73"/> +</svg> diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts index 14f13d56bdf3..529ebc58a60e 100644 --- a/src/plugins/wizard/public/types.ts +++ b/src/plugins/wizard/public/types.ts @@ -10,7 +10,7 @@ import { DashboardStart } from 'src/plugins/dashboard/public'; import { VisualizationsSetup } from 'src/plugins/visualizations/public'; import { ExpressionsStart } from 'src/plugins/expressions/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; -import { DataPublicPluginStart } from '../../data/public'; +import { DataPublicPluginStart, IndexPatternField } from '../../data/public'; import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; export type WizardSetup = TypeServiceSetup; diff --git a/src/plugins/wizard/tsconfig.json b/src/plugins/wizard/tsconfig.json new file mode 100644 index 000000000000..ab8821948be5 --- /dev/null +++ b/src/plugins/wizard/tsconfig.json @@ -0,0 +1,74 @@ +{ + "compilerOptions": { + "baseUrl": "./src/plugins/wizard", + "paths": { + // Allows for importing from `opensearch-dashboards` package for the exported types. + "opensearch-dashboards": ["./opensearch_dashboards"], + "opensearch-dashboards/public": ["src/core/public"], + "opensearch-dashboards/server": ["src/core/server"], + "plugins/*": ["src/legacy/core_plugins/*/public/"], + "test_utils/*": [ + "src/test_utils/public/*" + ], + "fixtures/*": ["src/fixtures/*"], + "@opensearch-project/opensearch": ["node_modules/@opensearch-project/opensearch/api/new"] + }, + // Support .tsx files and transform JSX into calls to React.createElement + "jsx": "react", + // Enables all strict type checking options. + "strict": true, + // save information about the project graph on disk + "incremental": true, + // enables "core language features" + "lib": [ + "esnext", + // includes support for browser APIs + "dom" + ], + // Node 8 should support everything output by esnext, we override this + // in webpack with loader-level compiler options + "target": "esnext", + // Use commonjs for node, overridden in webpack to keep import statements + // to maintain support for things like `await import()` + "module": "commonjs", + // Allows default imports from modules with no default export. This does not affect code emit, just type checking. + // We have to enable this option explicitly since `esModuleInterop` doesn't enable it automatically when ES2015 or + // ESNext module format is used. + "allowSyntheticDefaultImports": true, + // Emits __importStar and __importDefault helpers for runtime babel ecosystem compatibility. + "esModuleInterop": true, + // Resolve modules in the same way as Node.js. Aka make `require` works the + // same in TypeScript as it does in Node.js. + "moduleResolution": "node", + // "resolveJsonModule" allows for importing, extracting types from and generating .json files. + "resolveJsonModule": true, + // Disallow inconsistently-cased references to the same file. + "forceConsistentCasingInFileNames": true, + // Forbid unused local variables as the rule was deprecated by ts-lint + "noUnusedLocals": true, + // Provide full support for iterables in for..of, spread and destructuring when targeting ES5 or ES3. + "downlevelIteration": true, + // import tslib helpers rather than inlining helpers for iteration or spreading, for instance + "importHelpers": true, + // adding global typings + "noImplicitAny": false, + "types": [ + "node", + "jest", + "react", + "flot", + "@testing-library/jest-dom", + "resize-observer-polyfill" + ] + }, + "include": [ + "public/**/*.ts", + "public/**/*.tsx", + "server/**/*.ts", + "../../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../core/tsconfig.json" } + ] +} From 17ccd284f3db268d54253ca02a9b7b3633da7345 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Tue, 28 Jun 2022 10:48:06 -0700 Subject: [PATCH 05/47] [D&D] Adding and editing an aggregation + metric color ranges (#1781) * edit and add agg works Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * edit agg using draft state Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * cleanup dropbox field icon Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * Adds other metric style props Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * Updated comment Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../public/components/agg_common_props.ts | 2 +- .../vis_default_editor/public/index.ts | 1 + .../containers/data_tab/config_panel.scss | 6 +- .../containers/data_tab/config_panel.tsx | 81 +------ .../containers/data_tab/items/dropbox.tsx | 6 +- .../containers/data_tab/items/types.ts | 4 +- .../data_tab/items/use/use_dropbox.tsx | 198 ++++-------------- .../containers/data_tab/secondary_panel.tsx | 79 ++++++- .../state_management/visualization_slice.ts | 32 ++- .../metric/components/metric_viz_options.tsx | 76 +++++-- .../visualizations/metric/metric_viz_type.ts | 4 +- 11 files changed, 204 insertions(+), 285 deletions(-) diff --git a/src/plugins/vis_default_editor/public/components/agg_common_props.ts b/src/plugins/vis_default_editor/public/components/agg_common_props.ts index 5364be4df6de..0ef9c16f617d 100644 --- a/src/plugins/vis_default_editor/public/components/agg_common_props.ts +++ b/src/plugins/vis_default_editor/public/components/agg_common_props.ts @@ -43,7 +43,7 @@ export interface DefaultEditorCommonProps { formIsTouched: boolean; groupName: AggGroupName; metricAggs: IAggConfig[]; - state: EditorVisState; + state: Partial<EditorVisState>; setAggParamValue: <T extends keyof AggParams>( aggId: AggId, paramName: T, diff --git a/src/plugins/vis_default_editor/public/index.ts b/src/plugins/vis_default_editor/public/index.ts index af0b54956a97..88769ac71f9f 100644 --- a/src/plugins/vis_default_editor/public/index.ts +++ b/src/plugins/vis_default_editor/public/index.ts @@ -31,6 +31,7 @@ export { DefaultEditorController } from './default_editor_controller'; export { useValidation } from './components/controls/utils'; export { RangesParamEditor, RangeValues } from './components/controls/ranges'; +export { DefaultEditorAggParams } from './components/agg_params'; export * from './editor_size'; export * from './vis_options_props'; export * from './utils'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss index 90dd89f1e3bb..b088dca2822c 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss @@ -22,6 +22,10 @@ padding: $euiSizeS; } + &__aggEditor { + padding: 0 $euiSizeM; + } + &--secondary { position: absolute; top: 0; @@ -31,4 +35,4 @@ &.showSecondary > .wizConfig__section { transform: translateX(-100%); } -} \ No newline at end of file +} diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx index a5fc6a9fef31..07683666eb79 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx @@ -4,101 +4,26 @@ */ import { EuiForm } from '@elastic/eui'; -import React, { useMemo, useState } from 'react'; +import React from 'react'; import { useVisualizationType } from '../../../utils/use'; -import { - DropboxContribution, - MainItemContribution, - SecondaryItemContribution, - TitleItemContribution, - ITEM_TYPES, -} from './items'; import { useTypedSelector } from '../../../utils/state_management'; -import { mapItemToPanelComponents } from './utils/item_to_panel'; -import { ItemTypes } from '../../constants'; -import { SelectContribution } from '../common/items'; -import { INDEX_FIELD_KEY } from './items/use/use_form_field'; -import { DATA_TAB_ID } from '.'; import './config_panel.scss'; import { mapSchemaToAggPanel } from './utils/schema_to_dropbox'; -import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; -import { WizardServices } from '../../../../types'; import { SecondaryPanel } from './secondary_panel'; -const DEFAULT_ITEMS: MainItemContribution[] = [getTitleContribution()]; - export function ConfigPanel() { const vizType = useVisualizationType(); - const activeAgg = useTypedSelector((state) => state.visualization.activeVisualization?.activeAgg); + const draftAgg = useTypedSelector((state) => state.visualization.activeVisualization?.draftAgg); const schemas = vizType.ui.containerConfig.data.schemas; - // TODO: Will cleanup when add and edit field support is re introduced - // const activeItem = useTypedSelector((state) => state.config.activeItem); - // const configItemState = useTypedSelector((state) => state.config.items[activeItem?.id || '']); - - // const hydratedItems: MainItemContribution[] = useMemo( - // () => [...(items?.[DATA_TAB_ID] ?? []), ...DEFAULT_ITEMS], - // [items] - // ); - - // const mainPanel = useMemo(() => mapItemToPanelComponents(hydratedItems), [hydratedItems]); - // const secondaryPanel = useMemo(() => { - // if (!activeItem || !configItemState || typeof configItemState === 'string') return; - - // // Generate each secondary panel base on active item type - // if (activeItem.type === ITEM_TYPES.DROPBOX) { - // const activeDropboxContribution = hydratedItems.find( - // (item: MainItemContribution) => - // item.type === ITEM_TYPES.DROPBOX && item?.id === activeItem?.id - // ) as DropboxContribution | undefined; - - // if (!activeDropboxContribution) return null; - - // let itemsToRender: SecondaryItemContribution[] = [ - // getTitleContribution(activeDropboxContribution.label), - // getFieldSelectorContribution(), - // ]; - - // const dropboxFieldInstance = configItemState.instances.find( - // ({ id }) => id === activeItem.instanceId - // ); - // if (dropboxFieldInstance && dropboxFieldInstance.properties.fieldName) { - // itemsToRender = [...itemsToRender, ...activeDropboxContribution.items]; - // } - - // return mapItemToPanelComponents(itemsToRender, true); - // } - // }, [activeItem, configItemState, hydratedItems]); - if (!schemas) return null; const mainPanel = mapSchemaToAggPanel(schemas); return ( - <EuiForm className={`wizConfig ${activeAgg ? 'showSecondary' : ''}`}> + <EuiForm className={`wizConfig ${draftAgg ? 'showSecondary' : ''}`}> <div className="wizConfig__section">{mainPanel}</div> <SecondaryPanel /> </EuiForm> ); } - -function getTitleContribution(title?: string): TitleItemContribution { - return { - type: ITEM_TYPES.TITLE, - title: [title, 'Configuration'].join(' '), - }; -} - -// function getFieldSelectorContribution(): SelectContribution<string> { -// return { -// type: ItemTypes.SELECT, -// id: INDEX_FIELD_KEY, -// label: 'Select a Field', -// options: (state) => { -// return state.dataSource.visualizableFields.map((field) => ({ -// value: field.name, -// inputDisplay: field.displayName, -// })); -// }, -// }; -// } diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx index 8639340c3a49..29dda2cfd9c4 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx @@ -11,11 +11,9 @@ import { EuiFormRow, EuiPanel, EuiText, - euiDragDropReorder, DropResult, } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { FieldIcon } from '../../../../../../../opensearch_dashboards_react/public'; import { IDropAttributes, IDropState } from '../../../../utils/drag_drop'; import './dropbox.scss'; import { DropboxDisplay } from './types'; @@ -70,11 +68,9 @@ const DropboxComponent = ({ <EuiFormRow label={boxLabel} className="dropBox" fullWidth> <div className="dropBox__container"> <EuiDroppable droppableId={dropboxId}> - {fields.map(({ id, label, icon }, index) => ( + {fields.map(({ id, label }, index) => ( <EuiDraggable className="dropBox__draggable" key={id} draggableId={id} index={index}> <EuiPanel key={index} paddingSize="s" className="dropBox__field"> - {/* TODO: Verify if field icon makes sense here */} - {/* <FieldIcon type={icon} /> */} <EuiText size="s" className="dropBox__field_text" onClick={() => onEditField(id)}> <a role="button" tabIndex={0}> {label} diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts index 3133fd4f39be..5a5c14e4e2d1 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts @@ -4,7 +4,6 @@ */ import { IndexPatternField } from 'src/plugins/data/common'; -import { FieldIconProps } from '../../../../../../../opensearch_dashboards_react/public'; import { SelectContribution, InputContribution } from '../../common/items'; /** @@ -31,7 +30,6 @@ export interface TitleItemContribution { export interface DropboxDisplay { label: string; - icon: FieldIconProps['type']; id: string; } export interface DropboxFieldProps { @@ -49,7 +47,7 @@ export interface DropboxContribution { display?: ( indexField: IndexPatternField, state: DropboxFieldProps - ) => Pick<DropboxDisplay, 'icon' | 'label'>; + ) => Pick<DropboxDisplay, 'label'>; // Defines how the initial state of a field should be set when a field is dropped onto it onDrop?: (field: IndexPatternField) => DropboxFieldProps; isDroppable?: (field: IndexPatternField) => boolean; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx index 4ef732ad84bd..c966767b4852 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx @@ -5,22 +5,16 @@ import { useCallback, useMemo } from 'react'; import { cloneDeep } from 'lodash'; -import { CreateAggConfigParams, IndexPatternField } from 'src/plugins/data/common'; +import { IndexPatternField } from 'src/plugins/data/common'; import { Schema } from '../../../../../../../../vis_default_editor/public'; import { FieldDragDataType } from '../../../../../utils/drag_drop/types'; import { useTypedDispatch, useTypedSelector } from '../../../../../utils/state_management'; -import { - DropboxContribution, - DropboxState, - ITEM_TYPES, - DropboxDisplay, - DropboxFieldProps, -} from '../types'; +import { DropboxState, DropboxDisplay } from '../types'; import { DropboxProps } from '../dropbox'; import { useDrop } from '../../../../../utils/drag_drop'; import { - createAggConfigParams, - reorderAggConfigParams, + editAgg, + reorderAgg, updateAggConfigParams, } from '../../../../../utils/state_management/visualization_slice'; import { useIndexPattern } from '../../../../../../application/utils/use/use_index_pattern'; @@ -54,7 +48,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { return indexPattern && aggService.createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); }, [aggConfigParams, aggService, indexPattern]); - const aggs = aggConfigs?.aggs ?? []; + const aggs = useMemo(() => aggConfigs?.aggs ?? [], [aggConfigs?.aggs]); const dropboxAggs = aggs.filter((agg) => agg.schema === schema.name); @@ -63,7 +57,6 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { dropboxAggs?.map( (agg): DropboxDisplay => ({ id: agg.id, - icon: 'number', // TODO: Check if we still need an icon here label: agg.makeLabel(), }) ) || [], @@ -72,22 +65,45 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { // Event handlers for each dropbox action type const onAddField = useCallback(() => { - const agg = aggConfigs?.createAggConfig( + if (!aggConfigs || !indexPattern) { + throw new Error('Cannot create new field, missing parameters'); + } + + const aggConfig = aggConfigs.createAggConfig( { - type: (schema.defaults as any).aggType, schema: schema.name, - }, + // using any since createAggConfig requires the type property but when an agg is brandNew, this has to be skipped. + // TODO: Update createAggConfig typing to correctly handle missing type field + } as any, { addToAggConfigs: false, } ); - if (agg) { - dispatch(createAggConfigParams(agg.serialize())); + aggConfig.brandNew = true; + const newAggs = [...aggs, aggConfig]; + const newAggConfigs = aggService.createAggConfigs(indexPattern, cloneDeep(newAggs)); + const newAggConfig = newAggConfigs.aggs.find((agg) => agg.brandNew); + + if (!newAggConfig) { + throw new Error('Missing new aggConfig'); } - }, [aggConfigs, dispatch, schema.defaults, schema.name]); - const onEditField = useCallback((instanceId) => {}, []); + dispatch(editAgg(newAggConfig.serialize())); + }, [aggConfigs, aggService, aggs, dispatch, indexPattern, schema.name]); + + const onEditField = useCallback( + (aggId) => { + const aggConfig = aggConfigs?.aggs.find((agg) => agg.id === aggId); + + if (!aggConfig) { + throw new Error('Could not find agg in aggConfigs'); + } + + dispatch(editAgg(aggConfig.serialize())); + }, + [aggConfigs?.aggs, dispatch] + ); const onDeleteField = useCallback( (aggId: string) => { @@ -124,7 +140,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { const onReorderField = useCallback( ({ sourceAggId, destinationAggId }) => { dispatch( - reorderAggConfigParams({ + reorderAgg({ sourceId: sourceAggId, destinationId: destinationAggId, }) @@ -164,148 +180,6 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { isValidDropTarget: isValidDropField, dropProps, }; - - // TODO: Will cleanup once add and edit field support is reintroduced - // const configItemState = items[dropboxId]; - // const dropboxState = - // !configItemState || typeof configItemState === 'string' ? INITIAL_STATE : configItemState; - // const filterPatrialInstances = useCallback( - // ({ properties }: DropboxInstanceState) => !!properties.fieldName, - // [] - // ); - // const mapInstanceToFieldDisplay = useCallback( - // ({ id, properties }: DropboxInstanceState): DropboxDisplay => { - // const indexPatternField = availableFields.find(({ name }) => name === properties.fieldName); - - // if (!indexPatternField) throw new Error('Field to display missing in available fields'); - - // return getDisplayField(id, indexPatternField, properties, display); - // }, - // [availableFields, display] - // ); - - // const displayFields: DropboxDisplay[] = useMemo( - // () => dropboxState.instances.filter(filterPatrialInstances).map(mapInstanceToFieldDisplay), - // [dropboxState.instances, filterPatrialInstances, mapInstanceToFieldDisplay] - // ); - - // Event handlers for each dropbox action type - // const onAddField = useCallback(() => { - // dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX)); - // }, [dispatch, dropboxId]); - - // const onEditField = useCallback( - // (instanceId) => { - // dispatch( - // setActiveItem({ - // id: dropboxId, - // type: ITEM_TYPES.DROPBOX, - // instanceId, - // }) - // ); - // }, - // [dispatch, dropboxId] - // ); - - // const onDeleteField = useCallback( - // (instanceId) => { - // dispatch( - // updateInstance({ - // id: dropboxId, - // instanceId, - // instanceState: null, - // }) - // ); - // }, - // [dispatch, dropboxId] - // ); - - // const onDropField = useCallback( - // (data: FieldDragDataType['value']) => { - // if (!data) return; - - // const { name: fieldName } = data; - // const indexField = getIndexPatternField(fieldName, availableFields); - - // if (!indexField) return; - - // if (isDroppable && !isDroppable(indexField)) return; - - // const newState: DropboxFieldProps = { - // ...onDrop?.(indexField), - // fieldName, - // }; - - // dispatch(addInstance(dropboxId, ITEM_TYPES.DROPBOX, false, newState)); - // }, - // [availableFields, isDroppable, onDrop, dispatch, dropboxId] - // ); - - // const onReorderField = useCallback( - // (reorderedInstanceIds: string[]) => { - // dispatch( - // reorderInstances({ - // id: dropboxId, - // reorderedInstanceIds, - // }) - // ); - // }, - // [dispatch, dropboxId] - // ); - - // const [dropProps, { isValidDropTarget, dragData, ...dropState }] = useDrop( - // 'field-data', - // onDropField - // ); - - // const isValidDropField = useMemo(() => { - // if (!dragData) return false; - - // const indexField = getIndexPatternField(dragData.name, availableFields); - - // if (!indexField) return false; - - // return isValidDropTarget && (isDroppable?.(indexField) ?? true); - // }, [availableFields, dragData, isDroppable, isValidDropTarget]); - - // return { - // id: dropboxId, - // label, - // limit, - // fields: displayFields, - // onAddField, - // onEditField, - // onDeleteField, - // onReorderField, - // ...dropState, - // dragData, - // isValidDropTarget: isValidDropField, - // dropProps, - // }; -}; - -const getDisplayField = ( - instanceId: string, - indexField: IndexPatternField, - properties: DropboxFieldProps, - display: DropboxContribution['display'] -): DropboxDisplay => { - let displayField: DropboxDisplay = { - id: instanceId, - icon: indexField.type, - label: indexField.displayName, - }; - if (display) { - const { icon, label } = display(indexField, properties); - - displayField = { - ...displayField, - icon, - label, - }; - } - - return displayField; }; const getIndexPatternField = (indexFieldName: string, availableFields: IndexPatternField[]) => diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx index de808c4c5fdc..d896116a445d 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx @@ -3,17 +3,86 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { useTypedSelector } from '../../../utils/state_management'; +import React, { useCallback, useMemo, useState } from 'react'; +import { cloneDeep } from 'lodash'; +import { useTypedDispatch, useTypedSelector } from '../../../utils/state_management'; +import { DefaultEditorAggParams } from '../../../../../../vis_default_editor/public'; import { Title } from './items'; +import { useIndexPattern, useVisualizationType } from '../../../utils/use'; +import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../../types'; +import { IAggType } from '../../../../../../data/public'; +import { saveAgg, editAgg } from '../../../utils/state_management/visualization_slice'; export function SecondaryPanel() { - const activeAgg = useTypedSelector((state) => state.visualization.activeVisualization?.activeAgg); + const draftAgg = useTypedSelector((state) => state.visualization.activeVisualization!.draftAgg); + const [valid, setValid] = useState(true); + const [touched, setTouched] = useState(false); + const dispatch = useTypedDispatch(); + const vizType = useVisualizationType(); + const indexPattern = useIndexPattern(); + const { + services: { + data: { + search: { aggs: aggService }, + }, + }, + } = useOpenSearchDashboards<WizardServices>(); + const schemas = vizType.ui.containerConfig.data.schemas.all; + + const aggConfigs = useMemo(() => { + return ( + indexPattern && draftAgg && aggService.createAggConfigs(indexPattern, [cloneDeep(draftAgg)]) + ); + }, [draftAgg, aggService, indexPattern]); + + const aggConfig = aggConfigs?.aggs[0]; + + const groupName = useMemo( + () => schemas.find((schema) => schema.name === aggConfig?.schema)?.group, + [aggConfig?.schema, schemas] + ); + + const showAggParamEditor = !!(aggConfig && indexPattern); + + const closeMenu = useCallback(() => { + // Save the agg if valid else discard + dispatch(saveAgg(valid)); + }, [dispatch, valid]); return ( <div className="wizConfig__section wizConfig--secondary"> - <Title title="Test" isSecondary /> - <div>{JSON.stringify(activeAgg)}</div> + <Title title="Test" isSecondary closeMenu={closeMenu} /> + {showAggParamEditor && ( + <DefaultEditorAggParams + className="wizConfig__aggEditor" + agg={aggConfig!} + indexPattern={indexPattern!} + setValidity={setValid} + setTouched={setTouched} + schemas={schemas} + formIsTouched={false} + groupName={groupName ?? 'none'} + metricAggs={[]} + state={{ + data: {}, + description: 'Falalala', + title: 'Title for the aggParams', + }} + setAggParamValue={function <T extends string | number | symbol>( + aggId: string, + paramName: T, + value: any + ): void { + aggConfig.params[paramName] = value; + dispatch(editAgg(aggConfig.serialize())); + }} + onAggTypeChange={function (aggId: string, aggType: IAggType): void { + aggConfig.type = aggType; + dispatch(editAgg(aggConfig.serialize())); + }} + /> + )} </div> ); } diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts index b5a4b4e93326..d18396197a2f 100644 --- a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -13,7 +13,7 @@ interface VisualizationState { activeVisualization?: { name: string; aggConfigParams: CreateAggConfigParams[]; - activeAgg?: CreateAggConfigParams; + draftAgg?: CreateAggConfigParams; }; } @@ -59,15 +59,28 @@ export const slice = createSlice({ setSearchField: (state, action: PayloadAction<string>) => { state.searchField = action.payload; }, - createAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams>) => { - state.activeVisualization!.activeAgg = action.payload; + editAgg: (state, action: PayloadAction<CreateAggConfigParams>) => { + state.activeVisualization!.draftAgg = action.payload; }, - saveAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams>) => { - delete state.activeVisualization!.activeAgg; + saveAgg: (state, action: PayloadAction<boolean>) => { + const saveDraft = action.payload; + const draftAgg = state.activeVisualization!.draftAgg; - // TODO: Impliment reducer + // Delete the aggConfigParam if the save is not true + if (saveDraft && draftAgg) { + const aggIndex = state.activeVisualization!.aggConfigParams.findIndex( + (agg) => agg.id === draftAgg.id + ); + + if (aggIndex === -1) { + state.activeVisualization!.aggConfigParams.push(draftAgg); + } else { + state.activeVisualization!.aggConfigParams.splice(aggIndex, 1, draftAgg); + } + } + delete state.activeVisualization!.draftAgg; }, - reorderAggConfigParams: ( + reorderAgg: ( state, action: PayloadAction<{ sourceId: string; @@ -100,7 +113,8 @@ export const { setActiveVisualization, setIndexPattern, setSearchField, - createAggConfigParams, + editAgg, updateAggConfigParams, - reorderAggConfigParams, + saveAgg, + reorderAgg, } = slice.actions; diff --git a/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx b/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx index acfaab045800..b5ef3bdb75cb 100644 --- a/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx +++ b/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx @@ -5,19 +5,43 @@ import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; -import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiButtonGroup, EuiFormRow, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from 'react-intl'; import produce from 'immer'; import { Draft } from 'immer'; import { + ColorModes, ColorRanges, + ColorSchemaOptions, + colorSchemas, RangeOption, - SetColorRangeValue, SwitchOption, } from '../../../../../charts/public'; import { useTypedDispatch, useTypedSelector } from '../../../application/utils/state_management'; import { MetricOptionsDefaults } from '../metric_viz_type'; import { setState } from '../../../application/utils/state_management/style_slice'; +import { PersistedState } from '../../../../../visualizations/public'; + +const METRIC_COLOR_MODES = [ + { + id: ColorModes.NONE, + label: i18n.translate('visTypeMetric.colorModes.noneOptionLabel', { + defaultMessage: 'None', + }), + }, + { + id: ColorModes.LABELS, + label: i18n.translate('visTypeMetric.colorModes.labelsOptionLabel', { + defaultMessage: 'Labels', + }), + }, + { + id: ColorModes.BACKGROUND, + label: i18n.translate('visTypeMetric.colorModes.backgroundOptionLabel', { + defaultMessage: 'Background', + }), + }, +]; function MetricVizOptions() { const styleState = useTypedSelector((state) => state.style) as MetricOptionsDefaults; @@ -32,8 +56,12 @@ function MetricVizOptions() { [dispatch, styleState] ); + const metricColorModeLabel = i18n.translate('visTypeMetric.params.color.useForLabel', { + defaultMessage: 'Use color for', + }); + return ( - <div> + <EuiPanel paddingSize="s" hasShadow={false} hasBorder={false} color="transparent"> <EuiPanel paddingSize="s"> <EuiTitle size="xs"> <h3> @@ -71,8 +99,7 @@ function MetricVizOptions() { <EuiSpacer size="s" /> - {/* TODO: Reintroduce the other style properties */} - {/* <EuiPanel paddingSize="s"> + <EuiPanel paddingSize="s"> <EuiTitle size="xs"> <h3> <FormattedMessage id="visTypeMetric.params.rangesTitle" defaultMessage="Ranges" /> @@ -83,9 +110,13 @@ function MetricVizOptions() { <ColorRanges data-test-subj="metricColorRange" colorsRange={metric.colorsRange} - setValue={setMetric('labels') as SetColorRangeValue} - setTouched={setMetric('touched')} - setValidity={setMetric('validity')} + setValue={(_, value) => + setOption((draft) => { + draft.metric.colorsRange = value; + }) + } + setTouched={() => {}} + setValidity={() => {}} /> <EuiFormRow fullWidth display="rowCompressed" label={metricColorModeLabel}> @@ -95,26 +126,33 @@ function MetricVizOptions() { isDisabled={metric.colorsRange.length === 1} isFullWidth={true} legend={metricColorModeLabel} - options={vis.type.editorConfig.collections.metricColorMode} - onChange={setColorMode} + options={METRIC_COLOR_MODES} + onChange={(value) => + setOption((draft) => { + draft.metric.metricColorMode = value as ColorModes; + }) + } /> </EuiFormRow> <ColorSchemaOptions colorSchema={metric.colorSchema} - colorSchemas={vis.type.editorConfig.collections.colorSchemas} - disabled={ - metric.colorsRange.length === 1 || - metric.metricColorMode === ColorModes.NONE - } + colorSchemas={colorSchemas} + disabled={metric.colorsRange.length === 1 || metric.metricColorMode === ColorModes.NONE} invertColors={metric.invertColors} - setValue={setMetricValue as SetColorSchemaOptionsValue} + setValue={(paramName, value) => + setOption((draft) => { + // The paramName and associated value are expected to pair correctly but will be messy to type correctly + draft.metric[paramName] = value as any; + }) + } showHelpText={false} - uiState={uiState} + // uistate here is used for custom colors which is not currently supported. Update when supported + uiState={new PersistedState({})} /> </EuiPanel> - <EuiSpacer size="s" /> */} + <EuiSpacer size="s" /> <EuiPanel paddingSize="s"> <EuiTitle size="xs"> @@ -142,7 +180,7 @@ function MetricVizOptions() { showValue={false} /> </EuiPanel> - </div> + </EuiPanel> ); } diff --git a/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts index eabd9df1cb2f..406984f87ece 100644 --- a/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts +++ b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts @@ -4,7 +4,7 @@ */ import { i18n } from '@osd/i18n'; -import { Schemas } from '../../../../vis_default_editor/public'; +import { RangeValues, Schemas } from '../../../../vis_default_editor/public'; import { AggGroupNames } from '../../../../data/public'; import { ColorModes, ColorSchemas } from '../../../../charts/public'; import { MetricVizOptions } from './components/metric_viz_options'; @@ -20,7 +20,7 @@ export interface MetricOptionsDefaults { useRanges: boolean; colorSchema: ColorSchemas; metricColorMode: ColorModes; - colorsRange: [{ from: number; to: number }]; + colorsRange: RangeValues[]; labels: { show: boolean; }; From 16eefd03aacad04db4b2766fded55b62e3c797ab Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Thu, 30 Jun 2022 15:44:21 -0700 Subject: [PATCH 06/47] [D&D] Enable basic saved object management (#1816) * [D&D] Enable basic saved object management - Create README stub for saved_objects_management plugin - Register wizard saved object loader with management plugin - Add management methods to SavedObjectsType - Add capabilities provider to wizard - Add saved wizard vis SavedObjectClass and SavedObjectLoader - Add public plugin start method partially addresses #1620 Signed-off-by: Josh Romero <rmerqg@amazon.com> * [Doc] Add clarifications to README for save objects management plugin Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../saved_objects_management/README.md | 41 ++++++++++++++ .../opensearch_dashboards.json | 2 +- .../saved_objects_management/public/plugin.ts | 3 ++ .../public/register_services.ts | 10 +++- src/plugins/wizard/public/index.ts | 2 +- src/plugins/wizard/public/plugin.ts | 19 ++++++- .../public/saved_visualizations/_saved_vis.ts | 53 +++++++++++++++++++ .../public/saved_visualizations/index.ts | 6 +++ .../saved_visualizations.ts | 18 +++++++ .../services/type_service/type_service.ts | 2 +- src/plugins/wizard/public/types.ts | 18 ++++--- .../wizard/server/capabilities_provider.ts | 17 ++++++ src/plugins/wizard/server/plugin.ts | 12 +++-- .../wizard/server/saved_objects/index.ts | 2 +- .../wizard/server/saved_objects/wizard_app.ts | 17 ++++-- 15 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 src/plugins/saved_objects_management/README.md create mode 100644 src/plugins/wizard/public/saved_visualizations/_saved_vis.ts create mode 100644 src/plugins/wizard/public/saved_visualizations/index.ts create mode 100644 src/plugins/wizard/public/saved_visualizations/saved_visualizations.ts create mode 100644 src/plugins/wizard/server/capabilities_provider.ts diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md new file mode 100644 index 000000000000..9fc21786c66b --- /dev/null +++ b/src/plugins/saved_objects_management/README.md @@ -0,0 +1,41 @@ +# Save objects management + +Provides a UI (via the `management` plugin) to find and manage all saved objects in one place (you can see the primary page by navigating to `/app/management/opensearch-dashboards/objects`). Not to be confused with the `savedObjects` plugin, which provides all the core capabilities of saved objects. + +From the primary UI page, this plugin allows you to: +1. Search/view/delete saved objects and their relationships +2. Import/export saved objects +3. Inspect/edit raw saved object values without validation + +For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are alos free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly). +## Making a new saved object type manageable + +1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See `SavedObjectsTypeManagementDefinition` for explanation of its properties: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285) +2. Register saved object type via `core.savedObjects.registerType(...)` as part of plugin server setup method +3. Implement a way to save the object via `savedObjectsClient.create(...)` +4. After these steps, you should be able to save objects and view/search for them in Saved Objects management (`/app/management/opensearch-dashboards/objects`) + +## Enabling edit links from saved objects management + +1. Make sure `management.getInAppUrl` method of the `SavedObjectsType` is defined with a `path` (which will specify the link target) and the `uiCapabilitiesPath` +2. For `uiCapabilitiesPath` to work without additional hardcoding, it should be in the format `{plugin}.show`, so that [the default logic of `src/plugins/saved_objects_management/public/lib/in_app_url.ts`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L48-L50) will correctly match. Otherwise, you'll need to [add a case for your `uiCapabilities` path](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L45-L47) to that function +3. Create default plugin capabilities provider +3. Register plugin capabilities via `core.capabilities.registerProvider(...);` as part of plugin server setup method + +## Using saved objects management to inspect/edit new plugin objects + +You'll notice that when clicking on the "Inspect" button from the saved objects management table, you'll usually be routed to something like `/app/management/opensearch-dashboards/objects/savedVisualizations/` (where the route itself is determined by the `management.getEditUrl` method of the `SavedObjectsType`). But to register a similar route for a new saved object type, you'll need to create a new `savedObjectLoader` and register it with the management plugin. + +### Creating `savedObjectLoader` + +1. In your plugin's public directory, create a class for your saved object that extends `SavedObjectClass`. The mapping should match the `mappings` defined in your `SavedObjectsType`. +2. Create a `savedObjectLoader` creation function that returns a `new SavedObjectLoader(YourSavedObjectClass, savedObjectsClient)` +3. Return that `savedObjectLoader` as part of your public plugin `start` method + +### Registering + +Ideally, we'd allow plugins to self-register their `savedObjectLoader` and (declare a dependency on this plugin). However, as currently implemented, any plugins that want this plugin to handle their inpect routes need to be added as optional dependencies and registered here. + +1. Add your plugin to the `optionalPlugins` array in `./opensearch_dashboards.json` +2. Update the `StartDependencies` interface of this plugin to include the public plugin start type +3. Update `registerServices` to register a new type of `SavedObjectLoader`, where `id` will be the route, `title` will likely match your saved object type, and `service` is your `SavedObjectLoader` that is defined in your plugin start. diff --git a/src/plugins/saved_objects_management/opensearch_dashboards.json b/src/plugins/saved_objects_management/opensearch_dashboards.json index deca4778e34c..21a0b00860b4 100644 --- a/src/plugins/saved_objects_management/opensearch_dashboards.json +++ b/src/plugins/saved_objects_management/opensearch_dashboards.json @@ -4,7 +4,7 @@ "server": true, "ui": true, "requiredPlugins": ["management", "data"], - "optionalPlugins": ["dashboard", "visualizations", "discover", "home"], + "optionalPlugins": ["dashboard", "visualizations", "discover", "home", "wizard"], "extraPublicDirs": ["public/lib"], "requiredBundles": ["opensearchDashboardsReact", "home"] } diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index fd9eebba9016..74d03a150470 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -30,6 +30,8 @@ import { i18n } from '@osd/i18n'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; + +import { WizardStart } from '../../wizard/public'; import { ManagementSetup } from '../../management/public'; import { DataPublicPluginStart } from '../../data/public'; import { DashboardStart } from '../../dashboard/public'; @@ -69,6 +71,7 @@ export interface StartDependencies { dashboard?: DashboardStart; visualizations?: VisualizationsStart; discover?: DiscoverStart; + wizard?: WizardStart; } export class SavedObjectsManagementPlugin diff --git a/src/plugins/saved_objects_management/public/register_services.ts b/src/plugins/saved_objects_management/public/register_services.ts index 757876b8407b..8ce6ed20bc9f 100644 --- a/src/plugins/saved_objects_management/public/register_services.ts +++ b/src/plugins/saved_objects_management/public/register_services.ts @@ -36,7 +36,7 @@ export const registerServices = async ( registry: ISavedObjectsManagementServiceRegistry, getStartServices: StartServicesAccessor<StartDependencies, SavedObjectsManagementPluginStart> ) => { - const [, { dashboard, visualizations, discover }] = await getStartServices(); + const [, { dashboard, visualizations, discover, wizard }] = await getStartServices(); if (dashboard) { registry.register({ @@ -61,4 +61,12 @@ export const registerServices = async ( service: discover.savedSearchLoader, }); } + + if (wizard) { + registry.register({ + id: 'savedWizard', + title: 'wizard', + service: wizard.savedWizardLoader, + }); + } }; diff --git a/src/plugins/wizard/public/index.ts b/src/plugins/wizard/public/index.ts index 97f9007549a0..713e9448b933 100644 --- a/src/plugins/wizard/public/index.ts +++ b/src/plugins/wizard/public/index.ts @@ -11,4 +11,4 @@ import { WizardPlugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new WizardPlugin(initializerContext); } -export { WizardServices, WizardPluginStartDependencies } from './types'; +export { WizardServices, WizardPluginStartDependencies, WizardStart } from './types'; diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 67b6dea6dc7d..d238603cbfa3 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -17,15 +17,17 @@ import { WizardPluginStartDependencies, WizardServices, WizardSetup, + WizardStart, } from './types'; import { PLUGIN_NAME } from '../common'; import { TypeService } from './services/type_service'; import { getPreloadedStore } from './application/utils/state_management'; import { setAggService, setIndexPatterns } from './plugin_services'; +import { createSavedWizardLoader } from './saved_visualizations'; export class WizardPlugin implements - Plugin<WizardSetup, void, WizardPluginSetupDependencies, WizardPluginStartDependencies> { + Plugin<WizardSetup, WizardStart, WizardPluginSetupDependencies, WizardPluginStartDependencies> { private typeService = new TypeService(); constructor(public initializerContext: PluginInitializerContext) {} @@ -101,7 +103,20 @@ export class WizardPlugin }; } - public start(core: CoreStart) {} + public start(core: CoreStart, { data }: WizardPluginStartDependencies): WizardStart { + const typeService = this.typeService; + + return { + ...typeService.start(), + savedWizardLoader: createSavedWizardLoader({ + savedObjectsClient: core.savedObjects.client, + indexPatterns: data.indexPatterns, + search: data.search, + chrome: core.chrome, + overlays: core.overlays, + }), + }; + } public stop() {} } diff --git a/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts new file mode 100644 index 000000000000..8ff823812f73 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + createSavedObjectClass, + SavedObjectOpenSearchDashboardsServices, +} from '../../../saved_objects/public'; + +export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboardsServices) { + const SavedObjectClass = createSavedObjectClass(services); + + class SavedWizardVis extends SavedObjectClass { + public static type = 'wizard'; + + // if type:wizard has no mapping, we push this mapping into OpenSearch + public static mapping = { + title: 'text', + description: 'text', + state: 'text', + // savedSearchId: 'keyword', + // version: 'integer', + }; + + // Order these fields to the top, the rest are alphabetical + static fieldOrder = ['title', 'description']; + + // ID is optional, without it one will be generated on save. + constructor(id: string) { + super({ + type: SavedWizardVis.type, + mapping: SavedWizardVis.mapping, + + // if this is null/undefined then the SavedObject will be assigned the defaults + id, + + // default values that will get assigned if the doc is new + defaults: { + title: '', + description: '', + state: '{}', + // savedSearchId, + // version: 1, + }, + }); + this.showInRecentlyAccessed = true; + this.getFullPath = () => `/app/wizard#/edit/${this.id}`; + } + } + + return SavedWizardVis; +} diff --git a/src/plugins/wizard/public/saved_visualizations/index.ts b/src/plugins/wizard/public/saved_visualizations/index.ts new file mode 100644 index 000000000000..442d5107ea05 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './saved_visualizations'; diff --git a/src/plugins/wizard/public/saved_visualizations/saved_visualizations.ts b/src/plugins/wizard/public/saved_visualizations/saved_visualizations.ts new file mode 100644 index 000000000000..f07dfd940312 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/saved_visualizations.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SavedObjectLoader, + SavedObjectOpenSearchDashboardsServices, +} from '../../../saved_objects/public'; +import { createSavedWizardVisClass } from './_saved_vis'; + +export type SavedWizardLoader = ReturnType<typeof createSavedWizardLoader>; +export function createSavedWizardLoader(services: SavedObjectOpenSearchDashboardsServices) { + const { savedObjectsClient } = services; + const SavedWizardVisClass = createSavedWizardVisClass(services); + + return new SavedObjectLoader(SavedWizardVisClass, savedObjectsClient); +} diff --git a/src/plugins/wizard/public/services/type_service/type_service.ts b/src/plugins/wizard/public/services/type_service/type_service.ts index 38c13ebcfb3c..ddbd735fb9e8 100644 --- a/src/plugins/wizard/public/services/type_service/type_service.ts +++ b/src/plugins/wizard/public/services/type_service/type_service.ts @@ -28,7 +28,7 @@ * under the License. */ -import { CoreService } from 'src/core/types'; +import { CoreService } from '../../../../../core/types'; import { VisualizationTypeOptions } from './types'; import { VisualizationType } from './visualization_type'; diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts index 529ebc58a60e..8ebcea1e19b8 100644 --- a/src/plugins/wizard/public/types.ts +++ b/src/plugins/wizard/public/types.ts @@ -3,17 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectsStart } from 'src/plugins/saved_objects/public'; -import { AppMountParameters, CoreStart, ToastsStart } from 'opensearch-dashboards/public'; -import { EmbeddableSetup } from 'src/plugins/embeddable/public'; -import { DashboardStart } from 'src/plugins/dashboard/public'; -import { VisualizationsSetup } from 'src/plugins/visualizations/public'; -import { ExpressionsStart } from 'src/plugins/expressions/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; +import { EmbeddableSetup } from '../../embeddable/public'; +import { DashboardStart } from '../../dashboard/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { ExpressionsStart } from '../../expressions/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; -import { DataPublicPluginStart, IndexPatternField } from '../../data/public'; +import { DataPublicPluginStart } from '../../data/public'; import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; +import { SavedObjectLoader } from '../../saved_objects/public'; +import { AppMountParameters, CoreStart, ToastsStart } from '../../../core/public'; export type WizardSetup = TypeServiceSetup; +export interface WizardStart extends TypeServiceStart { + savedWizardLoader: SavedObjectLoader; +} export interface WizardPluginSetupDependencies { embeddable: EmbeddableSetup; diff --git a/src/plugins/wizard/server/capabilities_provider.ts b/src/plugins/wizard/server/capabilities_provider.ts new file mode 100644 index 000000000000..9bbede2d53a9 --- /dev/null +++ b/src/plugins/wizard/server/capabilities_provider.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const capabilitiesProvider = () => ({ + wizard: { + // TODO: investigate which capabilities we need to provide + // createNew: true, + // createShortUrl: true, + // delete: true, + show: true, + // showWriteControls: true, + // save: true, + // saveQuery: true, + }, +}); diff --git a/src/plugins/wizard/server/plugin.ts b/src/plugins/wizard/server/plugin.ts index d45e4081cce9..26781a236f84 100644 --- a/src/plugins/wizard/server/plugin.ts +++ b/src/plugins/wizard/server/plugin.ts @@ -12,8 +12,9 @@ import { } from '../../../core/server'; import { WizardPluginSetup, WizardPluginStart } from './types'; +import { capabilitiesProvider } from './capabilities_provider'; import { defineRoutes } from './routes'; -import { wizardApp } from './saved_objects'; +import { wizardSavedObjectType } from './saved_objects'; export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart> { private readonly logger: Logger; @@ -22,7 +23,7 @@ export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart this.logger = initializerContext.logger.get(); } - public setup({ http, savedObjects }: CoreSetup) { + public setup({ capabilities, http, savedObjects }: CoreSetup) { this.logger.debug('wizard: Setup'); const router = http.createRouter(); @@ -30,12 +31,15 @@ export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart defineRoutes(router); // Register saved object types - savedObjects.registerType(wizardApp); + savedObjects.registerType(wizardSavedObjectType); + + // Register capabilities + capabilities.registerProvider(capabilitiesProvider); return {}; } - public start(core: CoreStart) { + public start(_core: CoreStart) { this.logger.debug('wizard: Started'); return {}; } diff --git a/src/plugins/wizard/server/saved_objects/index.ts b/src/plugins/wizard/server/saved_objects/index.ts index aa90fcea911b..eabc7abf2761 100644 --- a/src/plugins/wizard/server/saved_objects/index.ts +++ b/src/plugins/wizard/server/saved_objects/index.ts @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { wizardApp } from './wizard_app'; +export { wizardSavedObjectType } from './wizard_app'; diff --git a/src/plugins/wizard/server/saved_objects/wizard_app.ts b/src/plugins/wizard/server/saved_objects/wizard_app.ts index 138bea03b22a..7060dfafe18c 100644 --- a/src/plugins/wizard/server/saved_objects/wizard_app.ts +++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectsType } from 'src/core/server'; -import { WIZARD_SAVED_OBJECT } from '../../common'; +import { SavedObject, SavedObjectsType } from '../../../../core/server'; +import { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from '../../common'; -export const wizardApp: SavedObjectsType = { +export const wizardSavedObjectType: SavedObjectsType = { name: WIZARD_SAVED_OBJECT, hidden: false, namespaceType: 'single', @@ -14,8 +14,15 @@ export const wizardApp: SavedObjectsType = { icon: 'visVisualBuilder', // TODO: Need a custom icon here defaultSearchField: 'title', importableAndExportable: true, - getTitle: (obj: { attributes: { title: string } }) => obj.attributes.title, - // getInAppUrl: TODO: Enable once editing is supported + getTitle: ({ attributes: { title } }: SavedObject<WizardSavedObjectAttributes>) => title, + getEditUrl: ({ id }: SavedObject) => + `/management/opensearch-dashboards/objects/savedWizard/${encodeURIComponent(id)}`, + getInAppUrl({ id }: SavedObject) { + return { + path: `/app/wizard#/edit/${encodeURIComponent(id)}`, + uiCapabilitiesPath: 'wizard.show', + }; + }, }, migrations: {}, mappings: { From 5f920b181762a86d6ea2ba601d7faebca89f1afd Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Thu, 30 Jun 2022 17:08:43 -0700 Subject: [PATCH 07/47] [D&D] Adds drop validation (#1833) * edit and add agg works Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * edit agg using draft state Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * Adds other metric style props Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * feat: Adds agg type validation and defaults on drop Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: refactor filter Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../public/components/agg_params_helper.ts | 2 +- .../data_tab/items/use/use_dropbox.tsx | 54 ++++++++++++++----- .../application/utils/drag_drop/types.ts | 2 +- .../visualizations/metric/metric_viz_type.ts | 4 +- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts index 8255c2945411..a16071f51c08 100644 --- a/src/plugins/vis_default_editor/public/components/agg_params_helper.ts +++ b/src/plugins/vis_default_editor/public/components/agg_params_helper.ts @@ -51,7 +51,7 @@ interface ParamInstanceBase { agg: IAggConfig; editorConfig: EditorConfig; metricAggs: IAggConfig[]; - state: EditorVisState; + state: Partial<EditorVisState>; schemas: Schema[]; hideCustomLabel?: boolean; } diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx index c966767b4852..ebabb90eccbf 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx +++ b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { cloneDeep } from 'lodash'; -import { IndexPatternField } from 'src/plugins/data/common'; +import { BucketAggType, IndexPatternField, propFilter } from '../../../../../../../../data/common'; import { Schema } from '../../../../../../../../vis_default_editor/public'; import { FieldDragDataType } from '../../../../../utils/drag_drop/types'; import { useTypedDispatch, useTypedSelector } from '../../../../../utils/state_management'; @@ -21,6 +21,9 @@ import { useIndexPattern } from '../../../../../../application/utils/use/use_ind import { useOpenSearchDashboards } from '../../../../../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../../../../../types'; +const filterByName = propFilter('name'); +const filterByType = propFilter('type'); + export const INITIAL_STATE: DropboxState = { instances: [], }; @@ -31,6 +34,7 @@ export interface UseDropboxProps extends Pick<DropboxProps, 'id' | 'label'> { export const useDropbox = (props: UseDropboxProps): DropboxProps => { const { id: dropboxId, label, schema } = props; + const [validAggTypes, setValidAggTypes] = useState<string[]>([]); const dispatch = useTypedDispatch(); const indexPattern = useIndexPattern(); const { @@ -118,12 +122,16 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { const onDropField = useCallback( (data: FieldDragDataType['value']) => { - if (!data) return; + if (!data || !validAggTypes.length) return; const { name: fieldName } = data; + const schemaAggTypes = (schema.defaults as any).aggTypes; + const allowedAggTypes = schemaAggTypes + ? schemaAggTypes.filter((type) => validAggTypes.includes(type)) + : []; aggConfigs?.createAggConfig({ - type: (schema.defaults as any).aggType, + type: allowedAggTypes[0] || validAggTypes[0], schema: schema.name, params: { field: fieldName, @@ -134,7 +142,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { dispatch(updateAggConfigParams(aggConfigs.aggs.map((agg) => agg.serialize()))); } }, - [aggConfigs, dispatch, schema.defaults, schema.name] + [aggConfigs, dispatch, schema.defaults, schema.name, validAggTypes] ); const onReorderField = useCallback( @@ -154,17 +162,35 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { onDropField ); - const isValidDropField = useMemo(() => { - if (!dragData) return false; + useEffect(() => { + const getValidAggTypes = () => { + if (!dragData || schema.group === 'none') return []; + + const indexField = getIndexPatternField(dragData.name, indexPattern?.fields ?? []); + + if (!indexField) return []; - const indexField = getIndexPatternField(dragData.name, indexPattern?.fields ?? []); + // Get all aggTypes allowed by the schema and get a list of all the aggTypes that the dragged index field can use + const aggTypes = aggService.types.getAll(); + const allowedAggTypes = filterByName(aggTypes[schema.group], schema.aggFilter); + + return ( + allowedAggTypes + .filter((aggType) => { + const allowedFieldTypes = aggType.paramByName('field')?.filterFieldTypes; + return filterByType([indexField], allowedFieldTypes).length !== 0; + }) + // `types` can be either a Bucket or Metric aggType, but both types have the name property. + .map((agg) => (agg as BucketAggType).name) + ); + }; - if (!indexField) return false; + setValidAggTypes(getValidAggTypes()); - return isValidDropTarget; - // TODO: Validate if the field is droppable from schema ref : src/plugins/vis_default_editor/public/components/agg_params.tsx - // return isValidDropTarget && (isDroppable?.(indexField) ?? true); - }, [dragData, indexPattern?.fields, isValidDropTarget]); + return () => { + setValidAggTypes([]); + }; + }, [aggService.types, dragData, indexPattern?.fields, schema.aggFilter, schema.group]); return { id: dropboxId, @@ -177,7 +203,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { onReorderField, ...dropState, dragData, - isValidDropTarget: isValidDropField, + isValidDropTarget: validAggTypes.length > 0, dropProps, }; }; diff --git a/src/plugins/wizard/public/application/utils/drag_drop/types.ts b/src/plugins/wizard/public/application/utils/drag_drop/types.ts index 0258a2315ec6..8ac8deb73e44 100644 --- a/src/plugins/wizard/public/application/utils/drag_drop/types.ts +++ b/src/plugins/wizard/public/application/utils/drag_drop/types.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IndexPatternField } from 'src/plugins/data/common'; +import { IndexPatternField } from '../../../../../data/common'; export interface EmptyDragDataType { namespace: null; diff --git a/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts index 406984f87ece..ce85db45c51b 100644 --- a/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts +++ b/src/plugins/wizard/public/visualizations/metric/metric_viz_type.ts @@ -67,7 +67,7 @@ export const createMetricConfig = (): VisualizationTypeOptions<MetricOptionsDefa }, }, defaults: { - aggType: 'avg', + aggTypes: ['avg', 'cardinality'], }, }, { @@ -80,7 +80,7 @@ export const createMetricConfig = (): VisualizationTypeOptions<MetricOptionsDefa max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], defaults: { - aggType: 'count', + aggTypes: ['terms'], }, }, ]), From b0018d2e7919230f8957155f50117e02bbd2a835 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Mon, 4 Jul 2022 00:17:18 -0700 Subject: [PATCH 08/47] chore: updates D&D icon (#1844) Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../wizard/public/assets/wizard_icon.svg | 25 +++++++++++++++++++ src/plugins/wizard/public/plugin.ts | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/plugins/wizard/public/assets/wizard_icon.svg diff --git a/src/plugins/wizard/public/assets/wizard_icon.svg b/src/plugins/wizard/public/assets/wizard_icon.svg new file mode 100644 index 000000000000..69da8016eb12 --- /dev/null +++ b/src/plugins/wizard/public/assets/wizard_icon.svg @@ -0,0 +1,25 @@ +<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10 3C10 2.44771 10.4477 2 11 2H29C29.5523 2 30 2.44772 30 3V12C30 12.5523 29.5523 13 29 13H11C10.4477 13 10 12.5523 10 12V3Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M29 3H11V12H29V3ZM11 2C10.4477 2 10 2.44771 10 3V12C10 12.5523 10.4477 13 11 13H29C29.5523 13 30 12.5523 30 12V3C30 2.44772 29.5523 2 29 2H11Z" fill="#343741"/> +<path d="M2 10C2 9.44771 2.44772 9 3 9H21C21.5523 9 22 9.44772 22 10V19C22 19.5523 21.5523 20 21 20H3C2.44772 20 2 19.5523 2 19V10Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M21 10H3V19H21V10ZM3 9C2.44772 9 2 9.44771 2 10V19C2 19.5523 2.44772 20 3 20H21C21.5523 20 22 19.5523 22 19V10C22 9.44772 21.5523 9 21 9H3Z" fill="#343741"/> +<g filter="url(#filter0_d_38_310)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4285 29L22.5714 27.2857L21.7143 29H18.2857V28.1116C18.2857 27.4098 15.5427 25.4544 15.5427 25.4544C15.1641 25.173 14.8571 24.5666 14.8571 24.0889V21C14.8571 20.0182 15.6247 19.2222 16.5714 19.2222V21.8889H17.4285V17.7409C17.4285 17.3316 17.7483 17 18.143 17C18.932 17 19.5714 17.6631 19.5714 18.4813V18.1853C19.5714 17.776 19.8911 17.4444 20.2858 17.4444C21.0748 17.4444 21.7143 18.1076 21.7143 18.9258V18.6298C21.7143 18.2204 22.034 17.8889 22.4287 17.8571C23.2177 17.8889 23.8571 18.552 23.8571 19.3702V19.0742C23.8571 18.6649 24.1768 18.3333 24.5715 18.3333C25.3605 18.3333 26 18.9964 26 19.8147V23.8608C26 24.3322 25.7966 25.041 25.5496 25.4491C25.5496 25.4491 24.2857 27.4098 24.2857 28.1116V29H23.4285ZM20 22.1428H20.8571V25.5714H20V22.1428V22.1428ZM21.7142 22.1428H22.5714V25.5714H21.7142V22.1428V22.1428ZM23.4286 22.1428H24.2857V25.5714H23.4286V22.1428V22.1428Z" fill="white"/> +</g> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5714 27.2857L23.4285 29H24.2857V28.1116C24.2857 27.7716 24.5823 27.1362 24.8881 26.5714C25.0278 26.3133 25.1695 26.07 25.2857 25.8763C25.4402 25.6188 25.5496 25.4491 25.5496 25.4491C25.7966 25.041 26 24.3322 26 23.8608V19.8147C26 19.0189 25.3952 18.3699 24.6362 18.3348C24.6148 18.3338 24.5932 18.3333 24.5715 18.3333C24.1768 18.3333 23.8571 18.6649 23.8571 19.0742V19.3702C23.8571 19.2689 23.8473 19.1699 23.8286 19.0742C23.7475 18.6589 23.4991 18.3055 23.1595 18.0879C22.959 17.9595 22.7267 17.8784 22.4782 17.86C22.4618 17.8588 22.4453 17.8578 22.4287 17.8571C22.034 17.8889 21.7143 18.2204 21.7143 18.6298V18.9258C21.7143 18.8244 21.7044 18.7254 21.6857 18.6298C21.6041 18.2122 21.3532 17.8585 21.0104 17.6489C20.8151 17.5294 20.5899 17.4568 20.3493 17.4459C20.3283 17.4449 20.3071 17.4444 20.2858 17.4444C19.8911 17.4444 19.5714 17.776 19.5714 18.1853V18.4813C19.5714 18.38 19.5616 18.281 19.5429 18.1853C19.4614 17.7685 19.2112 17.4153 18.8694 17.2055C18.6566 17.0749 18.4082 17 18.143 17C17.7483 17 17.4285 17.3316 17.4285 17.7409V21.8889H16.5714V19.2222C16.5233 19.2222 16.4756 19.2243 16.4285 19.2283C16.1102 19.2555 15.8166 19.373 15.5714 19.5559C15.1388 19.8786 14.8571 20.4052 14.8571 21V24.0889C14.8571 24.5666 15.1641 25.173 15.5427 25.4544C15.5427 25.4544 18.2857 27.4098 18.2857 28.1116V29H21.7143L22.5714 27.2857ZM21.7142 22.1428V25.5714H22.5714V22.1428H21.7142ZM20.8571 22.1428H20V25.5714H20.8571V22.1428ZM23.4286 22.1428V25.5714H24.2857V22.1428H23.4286ZM14.9622 26.2686L14.9542 26.2629L14.9462 26.257C14.6003 25.9999 14.3387 25.6406 14.1647 25.2949C13.9905 24.9489 13.8571 24.5232 13.8571 24.0889V21C13.8571 19.5475 14.9657 18.3024 16.4285 18.2259V17.7409C16.4285 16.8139 17.162 16 18.143 16C18.7325 16 19.2629 16.2105 19.6765 16.5563C19.864 16.4843 20.0689 16.4444 20.2858 16.4444C20.8702 16.4444 21.3963 16.6512 21.8083 16.9915C21.9775 16.9208 22.1593 16.8756 22.3485 16.8604L22.4086 16.8555L22.4689 16.858C23.0398 16.8809 23.5592 17.0977 23.9666 17.4435C24.1529 17.3725 24.3563 17.3333 24.5715 17.3333C25.9468 17.3333 27 18.4787 27 19.8147V23.8608C27 24.2194 26.9264 24.6111 26.8292 24.9524C26.7317 25.2951 26.5886 25.6638 26.405 25.967L26.3977 25.979M26.3901 25.9908L26.3879 25.9943L26.3788 26.0086L26.3419 26.0669C26.3096 26.1183 26.2628 26.1934 26.2064 26.2861C26.0931 26.4724 25.9435 26.7255 25.795 26.9969C25.6449 27.2715 25.505 27.5487 25.4053 27.7863C25.3291 27.9679 25.3015 28.0694 25.2914 28.1063C25.2859 28.1263 25.2857 28.1272 25.2857 28.1116V30H22.8105L22.5714 29.5218L22.3323 30H17.2857V28.2755C17.2502 28.2266 17.1959 28.1594 17.1182 28.0741C16.9049 27.8403 16.601 27.563 16.2706 27.2852C15.9454 27.0118 15.6172 26.7567 15.3685 26.5688C15.2446 26.4752 15.1417 26.3992 15.0703 26.347C15.0347 26.3209 15.0069 26.3008 14.9885 26.2875L14.9679 26.2727L14.9622 26.2686M17.3337 28.3513C17.3337 28.3513 17.3305 28.3463 17.3265 28.337C17.3321 28.3467 17.3337 28.3513 17.3337 28.3513Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9 15H5V14H9V15Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M17 8H13V7H17V8Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20 15H11V14H20V15Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M28 8H19V7H28V8Z" fill="#343741"/> +<defs> +<filter id="filter0_d_38_310" x="11.8571" y="15" width="17.1429" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_38_310"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_38_310" result="shape"/> +</filter> +</defs> +</svg> diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index d238603cbfa3..db8b95244093 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -19,6 +19,7 @@ import { WizardSetup, WizardStart, } from './types'; +import wizardIcon from './assets/wizard_icon.svg'; import { PLUGIN_NAME } from '../common'; import { TypeService } from './services/type_service'; import { getPreloadedStore } from './application/utils/state_management'; @@ -92,7 +93,7 @@ export class WizardPlugin defaultMessage: 'TODO...', }), // TODO: Replace with actual icon once available - icon: 'vector', + icon: wizardIcon, stage: 'beta', aliasApp: 'wizard', aliasPath: '#/', From 147560c12e1847bb176954dd406ddb186dd39b86 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Tue, 5 Jul 2022 16:21:55 -0700 Subject: [PATCH 09/47] Chore/remove contributions (#1843) * adds min/max validation Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * relocated contributions Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * Some more cleanup Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * minor fixes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * one more unnecessary file Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: Seconday panel title Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../components/data_source_select.tsx | 3 +- .../data_tab/config_panel.scss | 0 .../data_tab/config_panel.tsx | 6 +- .../data_tab}/dropbox.scss | 0 .../items => components/data_tab}/dropbox.tsx | 8 +- .../data_tab/field_search.tsx | 4 +- .../data_tab/field_selector.scss | 6 +- .../data_tab/field_selector.tsx | 6 +- .../data_tab/field_selector_field.scss | 1 + .../data_tab/field_selector_field.tsx | 6 +- .../data_tab/index.scss | 3 +- .../data_tab/index.tsx | 2 - .../data_tab}/schema_to_dropbox.tsx | 5 +- .../data_tab/secondary_panel.tsx | 24 ++--- .../items => components/data_tab}/title.tsx | 0 .../data_tab}/use/index.ts | 1 - .../data_tab}/use/use_dropbox.tsx | 29 +++--- .../components/searchable_dropdown.scss | 4 +- .../components/searchable_dropdown.tsx | 21 ++-- .../application/components/side_nav.tsx | 2 +- .../application/components/workspace.tsx | 20 +++- .../application/contributions/constants.ts | 12 --- .../containers/common/items/index.tsx | 9 -- .../containers/common/items/select.tsx | 47 --------- .../containers/common/items/text_input.tsx | 37 ------- .../containers/common/items/types.ts | 43 --------- .../containers/data_tab/items/form_field.tsx | 20 ---- .../containers/data_tab/items/index.tsx | 9 -- .../containers/data_tab/items/types.ts | 65 ------------- .../data_tab/items/use/use_form_field.tsx | 79 --------------- .../data_tab/utils/item_to_panel.tsx | 60 ------------ .../contributions/containers/index.ts | 7 -- .../containers/style_tab/index.tsx | 12 --- .../public/application/contributions/index.ts | 6 -- .../application/utils/async_search/index.ts | 48 ---------- .../state_management/visualization_slice.ts | 2 +- .../utils/validate_schema_state.ts | 37 +++++++ .../type_service/type_service.test.ts | 9 +- .../public/services/type_service/types.ts | 20 ---- .../type_service/visualization_type.test.tsx | 96 ------------------- .../type_service/visualization_type.tsx | 16 ++-- .../public/visualizations/bar_chart/index.ts | 92 ------------------ .../wizard/public/visualizations/index.ts | 2 - .../visualizations/metric/to_expression.ts | 2 - .../public/visualizations/pie_chart/index.ts | 15 --- 45 files changed, 138 insertions(+), 758 deletions(-) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/config_panel.scss (100%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/config_panel.tsx (79%) rename src/plugins/wizard/public/application/{contributions/containers/data_tab/items => components/data_tab}/dropbox.scss (100%) rename src/plugins/wizard/public/application/{contributions/containers/data_tab/items => components/data_tab}/dropbox.tsx (95%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/field_search.tsx (88%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/field_selector.scss (92%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/field_selector.tsx (95%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/field_selector_field.scss (99%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/field_selector_field.tsx (92%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/index.scss (79%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/index.tsx (94%) rename src/plugins/wizard/public/application/{contributions/containers/data_tab/utils => components/data_tab}/schema_to_dropbox.tsx (77%) rename src/plugins/wizard/public/application/{contributions/containers => components}/data_tab/secondary_panel.tsx (75%) rename src/plugins/wizard/public/application/{contributions/containers/data_tab/items => components/data_tab}/title.tsx (100%) rename src/plugins/wizard/public/application/{contributions/containers/data_tab/items => components/data_tab}/use/index.ts (72%) rename src/plugins/wizard/public/application/{contributions/containers/data_tab/items => components/data_tab}/use/use_dropbox.tsx (86%) delete mode 100644 src/plugins/wizard/public/application/contributions/constants.ts delete mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/containers/common/items/types.ts delete mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts delete mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/containers/index.ts delete mode 100644 src/plugins/wizard/public/application/contributions/containers/style_tab/index.tsx delete mode 100644 src/plugins/wizard/public/application/contributions/index.ts delete mode 100644 src/plugins/wizard/public/application/utils/async_search/index.ts create mode 100644 src/plugins/wizard/public/application/utils/validate_schema_state.ts delete mode 100644 src/plugins/wizard/public/services/type_service/visualization_type.test.tsx delete mode 100644 src/plugins/wizard/public/visualizations/bar_chart/index.ts delete mode 100644 src/plugins/wizard/public/visualizations/pie_chart/index.ts diff --git a/src/plugins/wizard/public/application/components/data_source_select.tsx b/src/plugins/wizard/public/application/components/data_source_select.tsx index 3bbae08573b9..08c4f86008bf 100644 --- a/src/plugins/wizard/public/application/components/data_source_select.tsx +++ b/src/plugins/wizard/public/application/components/data_source_select.tsx @@ -8,7 +8,6 @@ import { i18n } from '@osd/i18n'; import { EuiIcon } from '@elastic/eui'; import { SearchableDropdown, SearchableDropdownOption } from './searchable_dropdown'; import { useIndexPatterns } from '../utils/use'; -import indexPatternSvg from '../../assets/index_pattern.svg'; import { useTypedDispatch } from '../utils/state_management'; import { setIndexPattern } from '../utils/state_management/visualization_slice'; import { IndexPattern } from '../../../../data/public'; @@ -22,7 +21,7 @@ function toSearchableDropdownOption(indexPattern: IndexPattern): SearchableDropd id: indexPattern.id || '', label: indexPattern.title, searchableLabel: indexPattern.title, - prepend: <EuiIcon type={indexPatternSvg} />, + prepend: <EuiIcon type="indexPatternApp" />, }; } diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss similarity index 100% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.scss rename to src/plugins/wizard/public/application/components/data_tab/config_panel.scss diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx similarity index 79% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx rename to src/plugins/wizard/public/application/components/data_tab/config_panel.tsx index 07683666eb79..1c0e67693471 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/config_panel.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx @@ -5,10 +5,10 @@ import { EuiForm } from '@elastic/eui'; import React from 'react'; -import { useVisualizationType } from '../../../utils/use'; -import { useTypedSelector } from '../../../utils/state_management'; +import { useVisualizationType } from '../../utils/use'; +import { useTypedSelector } from '../../utils/state_management'; import './config_panel.scss'; -import { mapSchemaToAggPanel } from './utils/schema_to_dropbox'; +import { mapSchemaToAggPanel } from './schema_to_dropbox'; import { SecondaryPanel } from './secondary_panel'; export function ConfigPanel() { diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss similarity index 100% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.scss rename to src/plugins/wizard/public/application/components/data_tab/dropbox.scss diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx similarity index 95% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx rename to src/plugins/wizard/public/application/components/data_tab/dropbox.tsx index 29dda2cfd9c4..451bbe4e52eb 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx @@ -14,12 +14,16 @@ import { DropResult, } from '@elastic/eui'; import React, { useCallback } from 'react'; -import { IDropAttributes, IDropState } from '../../../../utils/drag_drop'; +import { IDropAttributes, IDropState } from '../../utils/drag_drop'; import './dropbox.scss'; -import { DropboxDisplay } from './types'; import { useDropbox } from './use'; import { UseDropboxProps } from './use/use_dropbox'; +export interface DropboxDisplay { + label: string; + id: string; +} + interface DropboxProps extends IDropState { id: string; label: string; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx similarity index 88% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx rename to src/plugins/wizard/public/application/components/data_tab/field_search.tsx index 772e308bbc9b..62dcf2c2b953 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_search.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { i18n } from '@osd/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { setSearchField } from '../../../utils/state_management/visualization_slice'; -import { useTypedDispatch } from '../../../utils/state_management'; +import { setSearchField } from '../../utils/state_management/visualization_slice'; +import { useTypedDispatch } from '../../utils/state_management'; export interface Props { /** diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss similarity index 92% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.scss rename to src/plugins/wizard/public/application/components/data_tab/field_selector.scss index e2c44387126a..a7d43f6464f7 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.scss +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss @@ -1,11 +1,13 @@ -@import "../../../util"; +@import "../../util"; .wizFieldSelector { @include scrollNavParent(auto 1fr); + padding: $euiSizeS; &__fieldGroups { @include euiYScrollWithShadows; + overflow-y: auto; margin-right: -$euiSizeS; padding-right: $euiSizeS; @@ -14,7 +16,7 @@ &__fieldGroup { margin-top: $euiSizeS; - + &:first-child { margin-top: 0; } diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx similarity index 95% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx rename to src/plugins/wizard/public/application/components/data_tab/field_selector.tsx index 70dac772a190..2361e8d1d073 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx @@ -11,12 +11,12 @@ import { IndexPatternField, OPENSEARCH_FIELD_TYPES, OSD_FIELD_TYPES, -} from '../../../../../../data/public'; +} from '../../../../../data/public'; import { FieldSelectorField } from './field_selector_field'; import './field_selector.scss'; -import { useTypedSelector } from '../../../utils/state_management'; -import { useIndexPattern } from '../../../utils/use'; +import { useTypedSelector } from '../../utils/state_management'; +import { useIndexPattern } from '../../utils/use'; interface IFieldCategories { categorical: IndexPatternField[]; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss similarity index 99% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.scss rename to src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss index 0ace9a914b37..e2f9919af3a8 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.scss +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss @@ -1,5 +1,6 @@ .wizFieldSelectorField { @include euiBottomShadowSmall; + padding: $euiSizeXS; background-color: $euiColorEmptyShade; border: $euiBorderThin; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx similarity index 92% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.tsx rename to src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx index f000f9395280..cfa2220eed6e 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/field_selector_field.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx @@ -29,9 +29,9 @@ */ import React, { useState } from 'react'; -import { IndexPatternField } from 'src/plugins/data/public'; -import { FieldButton, FieldIcon } from '../../../../../../opensearch_dashboards_react/public'; -import { useDrag } from '../../../utils/drag_drop/drag_drop_context'; +import { IndexPatternField } from '../../../../../data/public'; +import { FieldButton, FieldIcon } from '../../../../../opensearch_dashboards_react/public'; +import { useDrag } from '../../utils/drag_drop/drag_drop_context'; import './field_selector_field.scss'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/index.scss b/src/plugins/wizard/public/application/components/data_tab/index.scss similarity index 79% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/index.scss rename to src/plugins/wizard/public/application/components/data_tab/index.scss index 773a12538430..dab7ed4cdda3 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/index.scss +++ b/src/plugins/wizard/public/application/components/data_tab/index.scss @@ -1,7 +1,8 @@ -@import "../../../util"; +@import "../../util"; .wizDataTab { @include scrollNavParent; + display: grid; grid-template-columns: 50% 50%; } diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/index.tsx b/src/plugins/wizard/public/application/components/data_tab/index.tsx similarity index 94% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/index.tsx rename to src/plugins/wizard/public/application/components/data_tab/index.tsx index 9fefef7d8ce9..1f880f20698d 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/index.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/index.tsx @@ -19,5 +19,3 @@ export const DataTab = () => { </div> ); }; - -export * from './items'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/schema_to_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx similarity index 77% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/utils/schema_to_dropbox.tsx rename to src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx index b14a8bd13d85..37c55e25be99 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/schema_to_dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx @@ -4,8 +4,9 @@ */ import React from 'react'; -import { Schemas } from '../../../../../../../vis_default_editor/public'; -import { Title, Dropbox } from '../items'; +import { Schemas } from '../../../../../vis_default_editor/public'; +import { Dropbox } from './dropbox'; +import { Title } from './title'; export const mapSchemaToAggPanel = (schemas: Schemas) => { const panelComponents = schemas.all.map((schema) => { diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx similarity index 75% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx rename to src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx index d896116a445d..c897a9b018c5 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/secondary_panel.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx @@ -5,14 +5,14 @@ import React, { useCallback, useMemo, useState } from 'react'; import { cloneDeep } from 'lodash'; -import { useTypedDispatch, useTypedSelector } from '../../../utils/state_management'; -import { DefaultEditorAggParams } from '../../../../../../vis_default_editor/public'; -import { Title } from './items'; -import { useIndexPattern, useVisualizationType } from '../../../utils/use'; -import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; -import { WizardServices } from '../../../../types'; -import { IAggType } from '../../../../../../data/public'; -import { saveAgg, editAgg } from '../../../utils/state_management/visualization_slice'; +import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; +import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public'; +import { Title } from './title'; +import { useIndexPattern, useVisualizationType } from '../../utils/use'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../types'; +import { IAggType } from '../../../../../data/public'; +import { saveAgg, editAgg } from '../../utils/state_management/visualization_slice'; export function SecondaryPanel() { const draftAgg = useTypedSelector((state) => state.visualization.activeVisualization!.draftAgg); @@ -38,8 +38,8 @@ export function SecondaryPanel() { const aggConfig = aggConfigs?.aggs[0]; - const groupName = useMemo( - () => schemas.find((schema) => schema.name === aggConfig?.schema)?.group, + const selectedSchema = useMemo( + () => schemas.find((schema) => schema.name === aggConfig?.schema), [aggConfig?.schema, schemas] ); @@ -52,7 +52,7 @@ export function SecondaryPanel() { return ( <div className="wizConfig__section wizConfig--secondary"> - <Title title="Test" isSecondary closeMenu={closeMenu} /> + <Title title={selectedSchema?.title ?? 'Edit'} isSecondary closeMenu={closeMenu} /> {showAggParamEditor && ( <DefaultEditorAggParams className="wizConfig__aggEditor" @@ -62,7 +62,7 @@ export function SecondaryPanel() { setTouched={setTouched} schemas={schemas} formIsTouched={false} - groupName={groupName ?? 'none'} + groupName={selectedSchema?.group ?? 'none'} metricAggs={[]} state={{ data: {}, diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx b/src/plugins/wizard/public/application/components/data_tab/title.tsx similarity index 100% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/items/title.tsx rename to src/plugins/wizard/public/application/components/data_tab/title.tsx diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/index.ts b/src/plugins/wizard/public/application/components/data_tab/use/index.ts similarity index 72% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/index.ts rename to src/plugins/wizard/public/application/components/data_tab/use/index.ts index 87b259c5a86e..64265e655fa7 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/index.ts +++ b/src/plugins/wizard/public/application/components/data_tab/use/index.ts @@ -4,4 +4,3 @@ */ export { useDropbox } from './use_dropbox'; -export { useFormField } from './use_form_field'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx similarity index 86% rename from src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx rename to src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx index ebabb90eccbf..60290fc96e93 100644 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx @@ -5,29 +5,24 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { cloneDeep } from 'lodash'; -import { BucketAggType, IndexPatternField, propFilter } from '../../../../../../../../data/common'; -import { Schema } from '../../../../../../../../vis_default_editor/public'; -import { FieldDragDataType } from '../../../../../utils/drag_drop/types'; -import { useTypedDispatch, useTypedSelector } from '../../../../../utils/state_management'; -import { DropboxState, DropboxDisplay } from '../types'; -import { DropboxProps } from '../dropbox'; -import { useDrop } from '../../../../../utils/drag_drop'; +import { BucketAggType, IndexPatternField, propFilter } from '../../../../../../data/common'; +import { Schema } from '../../../../../../vis_default_editor/public'; +import { FieldDragDataType } from '../../../utils/drag_drop/types'; +import { useTypedDispatch, useTypedSelector } from '../../../utils/state_management'; +import { DropboxDisplay, DropboxProps } from '../dropbox'; +import { useDrop } from '../../../utils/drag_drop'; import { editAgg, reorderAgg, updateAggConfigParams, -} from '../../../../../utils/state_management/visualization_slice'; -import { useIndexPattern } from '../../../../../../application/utils/use/use_index_pattern'; -import { useOpenSearchDashboards } from '../../../../../../../../opensearch_dashboards_react/public'; -import { WizardServices } from '../../../../../../types'; +} from '../../../utils/state_management/visualization_slice'; +import { useIndexPattern } from '../../../utils/use/use_index_pattern'; +import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../../types'; const filterByName = propFilter('name'); const filterByType = propFilter('type'); -export const INITIAL_STATE: DropboxState = { - instances: [], -}; - export interface UseDropboxProps extends Pick<DropboxProps, 'id' | 'label'> { schema: Schema; } @@ -192,6 +187,8 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { }; }, [aggService.types, dragData, indexPattern?.fields, schema.aggFilter, schema.group]); + const canDrop = validAggTypes.length > 0 && schema.max > dropboxAggs.length; + return { id: dropboxId, label, @@ -203,7 +200,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { onReorderField, ...dropState, dragData, - isValidDropTarget: validAggTypes.length > 0, + isValidDropTarget: canDrop, dropProps, }; }; diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss index 6de99e91356d..bd997b5a075d 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.scss +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -10,14 +10,14 @@ } .searchableDropdown--fixedWidthChild { - width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2) ; + width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2); } .searchableDropdown--topDisplay { padding-right: $euiSizeL; + font-size: $euiFontSizeS; } - .searchableDropdown--selectableWrapper .euiSelectableList { // When clicking on the selectable content it will "highlight" itself with a box shadow // This turns that off diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx index da6795335088..0d489b818167 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx @@ -142,6 +142,7 @@ export const SearchableDropdown = ({ const selectedView = ( <EuiButtonEmpty color="text" + size="s" style={{ textAlign: 'left' }} className="searchableDropdown--topDisplay" onClick={onButtonClick} @@ -150,21 +151,25 @@ export const SearchableDropdown = ({ </EuiButtonEmpty> ); - const formControl = <EuiFormControlLayout - title={selected === undefined ? "Select an option" : selected.label} + const formControl = ( + <EuiFormControlLayout + title={selected === undefined ? 'Select an option' : selected.label} isLoading={loading} fullWidth={true} style={{ cursor: 'pointer' }} prepend={prepend} icon={{ type: 'arrowDown', side: 'right' }} readOnly={true} - >{selectedView}</EuiFormControlLayout> + > + {selectedView} + </EuiFormControlLayout> + ); return ( - <div className="searchableDropdown"> - <EuiPopover button={formControl} isOpen={isPopoverOpen} closePopover={closePopover}> - <div className="searchableDropdown--fixedWidthChild">{selectable}</div> - </EuiPopover> - </div> + <div className="searchableDropdown"> + <EuiPopover button={formControl} isOpen={isPopoverOpen} closePopover={closePopover}> + <div className="searchableDropdown--fixedWidthChild">{selectable}</div> + </EuiPopover> + </div> ); }; diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx index c79f63d7b5c0..4e4291f1c447 100644 --- a/src/plugins/wizard/public/application/components/side_nav.tsx +++ b/src/plugins/wizard/public/application/components/side_nav.tsx @@ -8,7 +8,7 @@ import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; import './side_nav.scss'; import { useVisualizationType } from '../utils/use'; import { DataSourceSelect } from './data_source_select'; -import { DataTab } from '../contributions'; +import { DataTab } from './data_tab'; import { StyleTabConfig } from '../../services/type_service'; export const SideNav = () => { diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 7747eef57cba..94e0d037b4f6 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -17,6 +17,7 @@ import { import React, { FC, useState, useMemo, useEffect } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../types'; +import { validateSchemaState } from '../utils/validate_schema_state'; import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; import { setActiveVisualization } from '../utils/state_management/visualization_slice'; import { useVisualizationType } from '../utils/use'; @@ -27,20 +28,30 @@ export const Workspace: FC = ({ children }) => { const { services: { expressions: { ReactExpressionRenderer }, + notifications: { toasts }, }, } = useOpenSearchDashboards<WizardServices>(); - const { toExpression } = useVisualizationType(); + const { toExpression, ui } = useVisualizationType(); const [expression, setExpression] = useState<string>(); const rootState = useTypedSelector((state) => state); useEffect(() => { async function loadExpression() { + const schemas = ui.containerConfig.data.schemas; + const [valid, errorMsg] = validateSchemaState(schemas, rootState); + + if (!valid) { + if (errorMsg) { + toasts.addWarning(errorMsg); + } + return; + } const exp = await toExpression(rootState); setExpression(exp); } loadExpression(); - }, [rootState, toExpression]); + }, [rootState, toExpression, toasts, ui.containerConfig.data.schemas]); return ( <section className="wizWorkspace"> @@ -94,7 +105,8 @@ const TypeSelectorPopover = () => { icon: <EuiIcon type={icon} />, onClick: () => { closePopover(); - dispatch(setActiveVisualization(name)); + // TODO: Fix changing viz type + // dispatch(setActiveVisualization(name)); }, toolTipContent: description, toolTipPosition: 'right', @@ -102,7 +114,7 @@ const TypeSelectorPopover = () => { ), }, ], - [dispatch, visualizationTypes] + [visualizationTypes] ); const button = ( diff --git a/src/plugins/wizard/public/application/contributions/constants.ts b/src/plugins/wizard/public/application/contributions/constants.ts deleted file mode 100644 index 0a581cabec81..000000000000 --- a/src/plugins/wizard/public/application/contributions/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { CommonItemTypes } from './containers/common/items'; -import { DataTabItemTypes } from './containers/data_tab/items'; - -export const ItemTypes = { - ...CommonItemTypes, - ...DataTabItemTypes, -}; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx b/src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx deleted file mode 100644 index eb081eb2932a..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/common/items/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './types'; - -export { Select } from './select'; -export { TextInput } from './text_input'; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx b/src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx deleted file mode 100644 index cc6c0060c5b0..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/common/items/select.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from 'react'; -import { EuiFormRow, EuiSuperSelect } from '@elastic/eui'; -import { WizardServices } from 'src/plugins/wizard/public'; -import { useOpenSearchDashboards } from '../../../../../../../opensearch_dashboards_react/public'; -import { useTypedSelector } from '../../../../utils/state_management'; -import { SelectContribution } from './types'; - -interface SelectProps extends Omit<SelectContribution<string>, 'type'> { - value: string; -} - -export const Select = ({ label, options, onChange, value, ...rest }: SelectProps) => { - const rootState = useTypedSelector((state) => state); - const { services } = useOpenSearchDashboards<WizardServices>(); - const selectOptions = useMemo( - () => (typeof options === 'function' ? options(rootState, services) : options), - [options, rootState, services] - ); - // const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - return ( - <EuiFormRow - label={label} - // error={errorMessage} - // isInvalid={isInvalid} - fullWidth - data-test-subj={rest['data-test-subj']} - describedByIds={rest.idAria ? [rest.idAria] : undefined} - > - <EuiSuperSelect - fullWidth - onChange={(newValue) => { - onChange?.(newValue); - }} - // isInvalid={isInvalid} - valueOfSelected={value || ''} - data-test-subj="select" - options={selectOptions} - /> - </EuiFormRow> - ); -}; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx b/src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx deleted file mode 100644 index 91d34a16f072..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/common/items/text_input.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { EuiFormRow, EuiFieldText } from '@elastic/eui'; -import { InputContribution } from './types'; - -interface InputProps extends Omit<InputContribution, 'type'> { - value: string; -} - -export const TextInput = ({ label, onChange, value, ...rest }: InputProps) => { - // const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - return ( - <EuiFormRow - label={label} - // error={errorMessage} - // isInvalid={isInvalid} - fullWidth - data-test-subj={rest['data-test-subj']} - describedByIds={rest.idAria ? [rest.idAria] : undefined} - > - <EuiFieldText - fullWidth - onChange={(event) => { - onChange?.(event.target.value); - }} - // isInvalid={isInvalid} - value={value || ''} - data-test-subj="text_input" - /> - </EuiFormRow> - ); -}; diff --git a/src/plugins/wizard/public/application/contributions/containers/common/items/types.ts b/src/plugins/wizard/public/application/contributions/containers/common/items/types.ts deleted file mode 100644 index 1761a4de2000..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/common/items/types.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { EuiSuperSelectProps } from '@elastic/eui'; -import { WizardServices } from 'src/plugins/wizard/public'; -import { RootState } from '../../../../utils/state_management'; - -/** - * Types for contributions shared across various panels - */ -export enum ITEM_TYPES { - SELECT = 'select', - INPUT = 'input', -} - -export const CommonItemTypes = { - ...ITEM_TYPES, -}; - -export interface SelectContribution<T extends string> { - type: ITEM_TYPES.SELECT; - id: string; - label: string; - options: - | EuiSuperSelectProps<T>['options'] - | ((state: RootState, services: WizardServices) => EuiSuperSelectProps<T>['options']); - onChange?: (option: string) => void; - 'data-test-subj'?: string; - idAria?: string; -} - -export interface InputContribution { - type: ITEM_TYPES.INPUT; - id: string; - label: string; - onChange?: (value: string) => void; - 'data-test-subj'?: string; - idAria?: string; -} - -export type CommonItemContribution = SelectContribution<string> | InputContribution; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx deleted file mode 100644 index d068474eb267..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/form_field.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import React, { ComponentType } from 'react'; -import { Select, TextInput, ITEM_TYPES } from '../../common/items'; -import { FieldContributions } from './types'; -import { useFormField } from './use'; - -const mapItemToFormFieldComponent: { [key in ITEM_TYPES]: ComponentType<any> } = { - [ITEM_TYPES.SELECT]: Select, - [ITEM_TYPES.INPUT]: TextInput, -}; - -export const FormField = ({ type, id, onChange, ...props }: FieldContributions) => { - const FieldComponent = mapItemToFormFieldComponent[type]; - const hookProps = useFormField(id, onChange); - - return <FieldComponent {...props} {...hookProps} />; -}; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx deleted file mode 100644 index 72a3ad62df58..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/index.tsx +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './types'; -export * from './dropbox'; -export { Title } from './title'; -export { FormField } from './form_field'; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts deleted file mode 100644 index 5a5c14e4e2d1..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { IndexPatternField } from 'src/plugins/data/common'; -import { SelectContribution, InputContribution } from '../../common/items'; - -/** - * Types for contributions that relate to the config panel - */ - -export enum ITEM_TYPES { - DROPBOX = 'dropbox', - TITLE = 'title', -} - -export const DataTabItemTypes = { - ...ITEM_TYPES, -}; - -export type FieldContributions = SelectContribution<string> | InputContribution; -export type MainItemContribution = TitleItemContribution | DropboxContribution | FieldContributions; -export type SecondaryItemContribution = TitleItemContribution | FieldContributions; - -export interface TitleItemContribution { - type: ITEM_TYPES.TITLE; - title: string; -} - -export interface DropboxDisplay { - label: string; - id: string; -} -export interface DropboxFieldProps { - fieldName?: string; - [itemId: string]: any; -} - -export interface DropboxContribution { - type: ITEM_TYPES.DROPBOX; - id: string; - label: string; - limit?: number; - items: SecondaryItemContribution[]; - // Define how the IndexPatternField should be displayed on the dropbox - display?: ( - indexField: IndexPatternField, - state: DropboxFieldProps - ) => Pick<DropboxDisplay, 'label'>; - // Defines how the initial state of a field should be set when a field is dropped onto it - onDrop?: (field: IndexPatternField) => DropboxFieldProps; - isDroppable?: (field: IndexPatternField) => boolean; -} - -export interface InstanceState<T> { - instances: Array<{ - id: string; - properties: T; - }>; -} - -export type DropboxState = InstanceState<DropboxFieldProps>; -export type InstanceItemStates = DropboxState; -export type ConfigItemState = InstanceItemStates | string | undefined; diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx deleted file mode 100644 index 03d22dd0e7ff..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/items/use/use_form_field.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import produce from 'immer'; -import { useCallback, useMemo } from 'react'; -// import { -// ConfigState, -// updateConfigItemState, -// updateInstance, -// } from '../../../../../utils/state_management/config_slice'; -import { useTypedSelector, useTypedDispatch } from '../../../../../utils/state_management'; -import { FieldContributions } from '../types'; - -export const INDEX_FIELD_KEY = 'fieldName'; - -interface FieldProps { - onChange: Function; - value: string; -} - -export const useFormField = (id: string, onChange: FieldContributions['onChange']): FieldProps => { - const configState = useTypedSelector((state) => state.config); - const { activeItem, items } = configState; - const dispatch = useTypedDispatch(); - - const instanceState = useMemo(() => getInstanceState(configState) ?? {}, [configState]); - - const handleChange = useCallback( - (newValue: string) => { - onChange?.(newValue); - - // TODO: Will cleanup once add and edit field support is reintroduced - - // is a MainPanel field value - if (!activeItem) { - // dispatch( - // // updateConfigItemState({ - // // id, - // // itemState: newValue, - // // }) - // ); - return; - } - - const newInstanceState = produce(instanceState, (draftState) => { - draftState[id] = newValue; - }); - - // dispatch( - // updateInstance({ - // id: activeItem.id, - // instanceId: activeItem.instanceId, - // instanceState: newInstanceState, - // }) - // ); - }, - [activeItem, id, instanceState, onChange] - ); - - return { - value: activeItem ? instanceState[id] : items[id], - onChange: handleChange, - }; -}; - -function getInstanceState({ items, activeItem }: ConfigState) { - const { id: parentItemId, instanceId } = activeItem ?? {}; - const configItem = items[parentItemId ?? '']; - - if (!configItem || typeof configItem === 'string') return; - - const instanceItem = configItem.instances.find(({ id }) => id === instanceId); - - if (!instanceItem) return; - - return instanceItem.properties; -} diff --git a/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx b/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx deleted file mode 100644 index db9dfdaf7dee..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/data_tab/utils/item_to_panel.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { ItemTypes } from '../../../constants'; -import { - Title, - Dropbox, - FormField, - MainItemContribution, - SecondaryItemContribution, -} from '../items'; - -export const mapItemToPanelComponents = ( - items: Array<MainItemContribution | SecondaryItemContribution>, - isSecondary = false -) => { - const uniqueDict: { [key: string]: boolean } = {}; - - const [title, ...panelComponents] = items - .filter((item) => { - // Ensure unique item ID - const id = item.type !== ItemTypes.TITLE ? item.id : 'title'; - if (uniqueDict[id]) return false; - - uniqueDict[id] = true; - return true; - }) - .sort((itemA, itemB) => - // Ensure that the title is on top - itemA.type === ItemTypes.TITLE ? -1 : itemB.type === ItemTypes.TITLE ? 1 : 0 - ) - .map((item, index) => { - const { type } = item; - - switch (type) { - case ItemTypes.TITLE: - return <Title key={index} {...item} isSecondary={isSecondary} />; - - case ItemTypes.DROPBOX: - return <Dropbox key={item.id} {...item} />; - - case ItemTypes.SELECT: - case ItemTypes.INPUT: - return <FormField key={item.id} {...item} />; - - default: - break; - } - }); - - return ( - <> - {title} - <div className="wizConfig__content">{panelComponents}</div> - </> - ); -}; diff --git a/src/plugins/wizard/public/application/contributions/containers/index.ts b/src/plugins/wizard/public/application/contributions/containers/index.ts deleted file mode 100644 index 74d0a6b8f76e..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './data_tab'; -export * from './style_tab'; diff --git a/src/plugins/wizard/public/application/contributions/containers/style_tab/index.tsx b/src/plugins/wizard/public/application/contributions/containers/style_tab/index.tsx deleted file mode 100644 index 6da05f6715e7..000000000000 --- a/src/plugins/wizard/public/application/contributions/containers/style_tab/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; - -export const STYLE_TAB_ID = 'style_tab'; - -export const StyleTab = () => { - return <div>TODO: Layout styles come here.</div>; -}; diff --git a/src/plugins/wizard/public/application/contributions/index.ts b/src/plugins/wizard/public/application/contributions/index.ts deleted file mode 100644 index 6ea0ec832393..000000000000 --- a/src/plugins/wizard/public/application/contributions/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -export * from './containers'; -export * from './constants'; diff --git a/src/plugins/wizard/public/application/utils/async_search/index.ts b/src/plugins/wizard/public/application/utils/async_search/index.ts deleted file mode 100644 index 9746cde24e4c..000000000000 --- a/src/plugins/wizard/public/application/utils/async_search/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { CreateAggConfigParams } from 'src/plugins/data/common'; -import { DataPublicPluginStart, IndexPattern } from 'src/plugins/data/public'; - -interface IDoAsyncSearch { - data: DataPublicPluginStart; - indexPattern: IndexPattern | null; - aggs?: CreateAggConfigParams[]; -} - -export const doAsyncSearch = async ({ data, indexPattern, aggs }: IDoAsyncSearch) => { - if (!indexPattern || !aggs || !aggs.length) return; - - // Constuct the query portion of the search request - const query = data.query.getOpenSearchQuery(indexPattern); - - // Constuct the aggregations portion of the search request by using the `data.search.aggs` service. - // const aggs = [{ type: 'avg', params: { field: field.name } }]; - // const aggs = [ - // { type: 'terms', params: { field: 'day_of_week' } }, - // { type: 'avg', params: { field: field.name } }, - // { type: 'terms', params: { field: 'customer_gender' } }, - // ]; - const aggConfigs = data.search.aggs.createAggConfigs(indexPattern, aggs); - const aggsDsl = aggConfigs.toDsl(); - - const request = { - params: { - index: indexPattern.title, - body: { - aggs: aggsDsl, - query, - }, - }, - }; - - // Submit the search request using the `data.search` service. - const { rawResponse } = await data.search.search(request).toPromise(); - - return { - rawResponse, - aggConfigs, - }; -}; diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts index d18396197a2f..f7d4d3ff6d49 100644 --- a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -4,7 +4,7 @@ */ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { CreateAggConfigParams } from 'src/plugins/data/common'; +import { CreateAggConfigParams } from '../../../../../data/common'; import { WizardServices } from '../../../types'; interface VisualizationState { diff --git a/src/plugins/wizard/public/application/utils/validate_schema_state.ts b/src/plugins/wizard/public/application/utils/validate_schema_state.ts new file mode 100644 index 000000000000..2db3653e2ce3 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/validate_schema_state.ts @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { countBy } from 'lodash'; +import { Schemas } from '../../../../vis_default_editor/public'; +import { RootState } from './state_management'; + +export const validateSchemaState = (schemas: Schemas, state: RootState): [boolean, string?] => { + const activeViz = state.visualization.activeVisualization; + const vizName = activeViz?.name; + const aggs = activeViz?.aggConfigParams; + + // Check if any aggreagations exist + if (aggs?.length === 0) { + return [false]; + } + + // Check if each schema's min agg requirement is met + const aggSchemaCount = countBy(aggs, (agg) => agg.schema); + const invalidsSchemas = schemas.all.filter((schema) => { + if (!schema.min) return false; + if (!aggSchemaCount[schema.name] || aggSchemaCount[schema.name] < schema.min) return true; + + return false; + }); + + if (invalidsSchemas.length > 0) { + return [ + false, + `The ${vizName} visualization needs at least ${invalidsSchemas[0].min} field(s) in the agg type "${invalidsSchemas[0].name}"`, + ]; + } + + return [true, '']; +}; diff --git a/src/plugins/wizard/public/services/type_service/type_service.test.ts b/src/plugins/wizard/public/services/type_service/type_service.test.ts index 7edf38e7e5e0..89e1ecb59154 100644 --- a/src/plugins/wizard/public/services/type_service/type_service.test.ts +++ b/src/plugins/wizard/public/services/type_service/type_service.test.ts @@ -6,11 +6,14 @@ import { VisualizationTypeOptions } from './types'; import { TypeService } from './type_service'; -const DEFAULT_VIZ_PROPS = { +const DEFAULT_VIZ_PROPS: VisualizationTypeOptions = { name: 'some-name', icon: 'some-icon', title: 'Some Title', - contributions: {}, + ui: {} as any, // Not required for this test + toExpression: async (state) => { + return 'test'; + }, }; describe('TypeService', () => { @@ -28,7 +31,7 @@ describe('TypeService', () => { }); describe('#setup', () => { - test('should throw an error if two visualizzations of the same id are registered', () => { + test('should throw an error if two visualizations of the same id are registered', () => { const { createVisualizationType } = service.setup(); createVisualizationType(createVizType({ name: 'viz-type-1' })); diff --git a/src/plugins/wizard/public/services/type_service/types.ts b/src/plugins/wizard/public/services/type_service/types.ts index fae6cdf1c093..b999d66ddf30 100644 --- a/src/plugins/wizard/public/services/type_service/types.ts +++ b/src/plugins/wizard/public/services/type_service/types.ts @@ -7,26 +7,6 @@ import { IconType } from '@elastic/eui'; import { RootState } from '../../application/utils/state_management'; import { Schemas } from '../../../../vis_default_editor/public'; -export enum ContributionTypes { - CONTAINER = 'CONTAINER', - ITEM = 'ITEM', -} - -export enum ContainerLocations { - SIDE_PANEL = 'sidePanel', - TOOLBAR = 'toolbar', -} - -export interface ContainerContribution { - id: string; - name: string; - Component: JSX.Element; -} - -type ContainerSchema = any; - -export type ContainerLocationContribution = { [K in ContainerLocations]: ContainerContribution[] }; - export interface DataTabConfig { schemas: Schemas; } diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx deleted file mode 100644 index 6d362a229d23..000000000000 --- a/src/plugins/wizard/public/services/type_service/visualization_type.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { VisualizationTypeOptions } from './types'; -import { VisualizationType } from './visualization_type'; - -// TODO: Update service tests -describe('VisualizationType', () => { - const DEFAULT_VIZ_PROPS = { - name: 'some-name', - icon: 'some-icon', - title: 'Some Title', - contributions: {}, - }; - - // const createVizType = (props?: Partial<VisualizationTypeOptions>): VisualizationTypeOptions => { - // return { - // ...DEFAULT_VIZ_PROPS, - // ...props, - // }; - // }; - - // test('should have default container contributions if none are provided', () => { - // const viz = new VisualizationType(createVizType()); - - // expect(viz.contributions.containers).toEqual(DEFAULT_CONTAINERS); - // }); - - // test('should have replace default container contributions when provided', () => { - // const defaultContainer = DEFAULT_CONTAINERS.sidePanel[0]; - // const viz = new VisualizationType( - // createVizType({ - // contributions: { - // containers: { - // sidePanel: [ - // { - // id: defaultContainer.id, - // name: 'Test', - // Component: <div>Test</div>, - // }, - // ], - // }, - // }, - // }) - // ); - - // const container = viz.contributions.containers.sidePanel.find( - // ({ id }) => id === defaultContainer.id - // ); - // expect(container).toMatchInlineSnapshot(` - // Object { - // "Component": <div> - // Test - // </div>, - // "id": "data_tab", - // "name": "Test", - // } - // `); - // }); - - // test('should register new container if provided', () => { - // const viz = new VisualizationType( - // createVizType({ - // contributions: { - // containers: { - // sidePanel: [ - // { - // id: 'test_id', - // name: 'Test', - // Component: <div>Test</div>, - // }, - // ], - // }, - // }, - // }) - // ); - - // const container = viz.contributions.containers.sidePanel.find(({ id }) => id === 'test_id'); - // const containerNames = viz.contributions.containers.sidePanel.map(({ name }) => name); - // const defaultContainerNames = DEFAULT_CONTAINERS.sidePanel.map(({ name }) => name); - - // expect(containerNames).toEqual([...defaultContainerNames, 'Test']); - // expect(container).toMatchInlineSnapshot(` - // Object { - // "Component": <div> - // Test - // </div>, - // "id": "test_id", - // "name": "Test", - // } - // `); - // }); -}); diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.tsx index 2ae1d25a4d96..90f30d8f8a95 100644 --- a/src/plugins/wizard/public/services/type_service/visualization_type.tsx +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -2,18 +2,20 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +import { IconType } from '@elastic/eui'; +import { RootState } from '../../application/utils/state_management'; import { VisualizationTypeOptions } from './types'; type IVisualizationType = VisualizationTypeOptions; export class VisualizationType implements IVisualizationType { - public readonly name; - public readonly title; - public readonly description; - public readonly icon; - public readonly stage; - public readonly ui; - public readonly toExpression; + public readonly name: string; + public readonly title: string; + public readonly description: string; + public readonly icon: IconType; + public readonly stage: 'beta' | 'production'; + public readonly ui: IVisualizationType['ui']; + public readonly toExpression: (state: RootState) => Promise<string | undefined>; constructor(options: VisualizationTypeOptions) { this.name = options.name; diff --git a/src/plugins/wizard/public/visualizations/bar_chart/index.ts b/src/plugins/wizard/public/visualizations/bar_chart/index.ts deleted file mode 100644 index b135edf48ef6..000000000000 --- a/src/plugins/wizard/public/visualizations/bar_chart/index.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ItemTypes } from '../../application/contributions/constants'; -import { - DATA_TAB_ID, - DropboxContribution, - MainItemContribution as ConfigPanelItem, -} from '../../application/contributions'; -import { VisualizationTypeOptions } from '../../services/type_service'; - -export const createBarChartConfig = (): VisualizationTypeOptions => { - const configPanelItems: ConfigPanelItem[] = [ - { - type: ItemTypes.TITLE, - title: 'Bar Chart Configuration', - }, - createDropboxContribution('x_axis', 'X Axis', {}), - createDropboxContribution('y_axis', 'Y Axis', { limit: 5 }), - // { - // type: ItemTypes.INPUT, - // id: 'testLabel', - // label: 'Test Label', - // }, - ]; - - return { - name: 'bar_chart', - title: 'Bar Chart', - icon: 'visBarVertical', - description: 'This is a bar chart', - contributions: { - items: { - [DATA_TAB_ID]: configPanelItems, - }, - }, - }; -}; - -const createDropboxContribution = ( - id: string, - label: string, - props?: Pick<DropboxContribution, 'limit'> -): DropboxContribution => ({ - type: ItemTypes.DROPBOX, - id, - label, - items: [ - { - type: ItemTypes.SELECT, - id: 'aggregation', - label: 'Select a Function', - options: (state, services) => { - const { config } = state; - // config.items[] - const { buckets } = services.data.search.aggs.types.getAll(); - return buckets.map(({ name, title, type }) => ({ - value: name, - inputDisplay: title, - })); - }, - }, - { - type: ItemTypes.INPUT, - id: 'label', - label: 'Name', - }, - ], - display: (indexField, dropboxState) => { - const dropboxField = { - icon: indexField.type, - label: indexField.displayName, - }; - - if (dropboxState?.label) { - dropboxField.label = dropboxState.label; - } - - return dropboxField; - }, - onDrop: (indexField) => { - return { - // label: indexField.displayName, - }; - }, - // isDroppable: (indexField) => { - // return indexField.displayName === 'geo.srcdest'; - // }, - ...props, -}); diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts index b9574fd9a771..44ad05739a74 100644 --- a/src/plugins/wizard/public/visualizations/index.ts +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -5,8 +5,6 @@ import type { TypeServiceSetup } from '../services/type_service'; import { createMetricConfig } from './metric'; -import { createBarChartConfig } from './bar_chart'; -import { createPieChartConfig } from './pie_chart'; import { WizardPluginStartDependencies } from '../types'; export function registerDefaultTypes( diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts index f0614801493e..5b787ef5d264 100644 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.ts +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -95,8 +95,6 @@ export const toExpression = async ({ style: styleState, visualization }: MetricR const { activeVisualization, indexPattern: indexId = '' } = visualization; const { aggConfigParams } = activeVisualization || {}; - if (!aggConfigParams || !aggConfigParams.length) return; - const indexPatternsService = getIndexPatterns(); const indexPattern = await indexPatternsService.get(indexId); const aggConfigs = getAggService().createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); diff --git a/src/plugins/wizard/public/visualizations/pie_chart/index.ts b/src/plugins/wizard/public/visualizations/pie_chart/index.ts deleted file mode 100644 index e71540852a32..000000000000 --- a/src/plugins/wizard/public/visualizations/pie_chart/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { VisualizationTypeOptions } from '../../services/type_service'; - -export const createPieChartConfig = (): VisualizationTypeOptions => { - return { - name: 'pie_chart', - title: 'Pie Chart', - icon: 'visPie', - contributions: {}, - }; -}; From 5ccd5bfd0e092197acab2a98559b96100e1a3fe0 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Tue, 5 Jul 2022 16:25:03 -0700 Subject: [PATCH 10/47] chore: Updates Field selector (#1845) Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../data_tab/field_selector_field.scss | 19 +++++++++++++++---- .../data_tab/field_selector_field.tsx | 3 +-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss index e2f9919af3a8..ef163b2f084f 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss @@ -1,13 +1,24 @@ .wizFieldSelectorField { @include euiBottomShadowSmall; - padding: $euiSizeXS; background-color: $euiColorEmptyShade; border: $euiBorderThin; - margin-top: $euiSizeS; + margin-top: $euiSizeXS; + + & > .osdFieldButton__button { + padding: 0; + } + + & .osdFieldButton__name { + padding: $euiSizeS $euiSizeS $euiSizeS 0; + } & > button { - align-items: center; - gap: 4px; + align-items: stretch; + } + + & .osdFieldIcon { + box-shadow: none; + height: 100%; } } diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx index cfa2220eed6e..963660d8eb81 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx @@ -79,12 +79,11 @@ export const FieldSelectorField = ({ field }: FieldSelectorFieldProps) => { return ( <FieldButton - size="s" className="wizFieldSelectorField" isActive={infoIsOpen} onClick={togglePopover} dataTestSubj={`field-${field.name}-showDetails`} - fieldIcon={<FieldIcon type={field.type} scripted={field.scripted} />} + fieldIcon={<FieldIcon type={field.type} scripted={field.scripted} size="l" />} // fieldAction={actionButton} fieldName={fieldName} {...dragProps} From e9447bb7588aba8d61fd2a5f8a718e4c6769f9f5 Mon Sep 17 00:00:00 2001 From: Brooke <97559014+CPTNB@users.noreply.github.com> Date: Wed, 6 Jul 2022 13:28:45 -0700 Subject: [PATCH 11/47] Adding breadcrumbs for drag and drop creation (#1797) Signed-off-by: Brooke Green <cptn@amazon.com> --- src/plugins/wizard/common/index.ts | 1 + .../public/application/components/top_nav.tsx | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/plugins/wizard/common/index.ts b/src/plugins/wizard/common/index.ts index 4b3522fec709..f2ceda0f2af5 100644 --- a/src/plugins/wizard/common/index.ts +++ b/src/plugins/wizard/common/index.ts @@ -5,5 +5,6 @@ export const PLUGIN_ID = 'wizard'; export const PLUGIN_NAME = 'Wizard'; +export const VISUALIZE_ID = 'visualize'; export { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from './wizard_saved_object_attributes'; diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx index d63ceedb302f..e2bdc2b33a24 100644 --- a/src/plugins/wizard/public/application/components/top_nav.tsx +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo } from 'react'; -import { PLUGIN_ID } from '../../../common'; +import { i18n } from '@osd/i18n'; +import React, { useMemo, useEffect } from 'react'; +import { PLUGIN_ID, VISUALIZE_ID } from '../../../common'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { getTopNavconfig } from '../utils/get_top_nav_config'; import { WizardServices } from '../../types'; @@ -16,6 +17,7 @@ export const TopNav = () => { const { services } = useOpenSearchDashboards<WizardServices>(); const { setHeaderActionMenu, + chrome, navigation: { ui: { TopNavMenu }, }, @@ -24,6 +26,25 @@ export const TopNav = () => { const config = useMemo(() => getTopNavconfig(services), [services]); const indexPattern = useIndexPattern(); + useEffect(() => { + const visualizeHref = window.location.href.split(`${PLUGIN_ID}#/`)[0] + `${VISUALIZE_ID}#/`; + chrome.setBreadcrumbs([ + { + text: i18n.translate('visualize.listing.breadcrumb', { + defaultMessage: 'Visualize', + }), + href: visualizeHref, + }, + { + text: i18n.translate('wizard.nav.breadcrumb.create', { + defaultMessage: 'Create', + }), + }, + ]); + // we want to run this hook exactly once, which you do by an empty dep array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( <div className="wizTopNav"> <TopNavMenu From 3a6e3064666f3fbbe27ef9c24811fda1cbbdb5fd Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Thu, 7 Jul 2022 11:14:39 -0700 Subject: [PATCH 12/47] fix(D&D): fixes reloading app (#1855) Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- src/plugins/wizard/public/plugin.ts | 5 +++-- src/plugins/wizard/public/visualizations/index.ts | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index db8b95244093..6bd8a3d970d0 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -25,6 +25,7 @@ import { TypeService } from './services/type_service'; import { getPreloadedStore } from './application/utils/state_management'; import { setAggService, setIndexPatterns } from './plugin_services'; import { createSavedWizardLoader } from './saved_visualizations'; +import { registerDefaultTypes } from './visualizations'; export class WizardPlugin implements @@ -38,6 +39,8 @@ export class WizardPlugin { visualizations }: WizardPluginSetupDependencies ) { const typeService = this.typeService; + registerDefaultTypes(typeService.setup()); + // Register the plugin to core core.application.register({ id: 'wizard', @@ -63,8 +66,6 @@ export class WizardPlugin setIndexPatterns(data.indexPatterns); // Register Default Visualizations - const { registerDefaultTypes } = await import('./visualizations'); - registerDefaultTypes(typeService.setup(), pluginsStart); const services: WizardServices = { ...coreStart, diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts index 44ad05739a74..52d3a7234f2a 100644 --- a/src/plugins/wizard/public/visualizations/index.ts +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -5,12 +5,8 @@ import type { TypeServiceSetup } from '../services/type_service'; import { createMetricConfig } from './metric'; -import { WizardPluginStartDependencies } from '../types'; -export function registerDefaultTypes( - typeServiceSetup: TypeServiceSetup, - pluginsStart: WizardPluginStartDependencies -) { +export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { const visualizationTypes = [createMetricConfig]; visualizationTypes.forEach((createTypeConfig) => { From 77f17d98b5a6808db4bff0ef40e4dc0e67ed4b2a Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Fri, 8 Jul 2022 15:33:57 -0700 Subject: [PATCH 13/47] [D&D] Dropbox style and animations (#1863) * fix dropbox styles and added animations Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * simpler usePrefersReducedMotion Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix drop target animation Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../components/data_tab/dropbox.scss | 58 +++++++++++++++++-- .../components/data_tab/dropbox.tsx | 28 ++++++++- .../use/use_prefers_reduced_motion.ts | 31 ++++++++++ .../application/components/workspace.tsx | 1 + 4 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 src/plugins/wizard/public/application/components/data_tab/use/use_prefers_reduced_motion.ts diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.scss b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss index dd2ab9c9d980..118c475ec590 100644 --- a/src/plugins/wizard/public/application/components/data_tab/dropbox.scss +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss @@ -13,24 +13,28 @@ &__container { display: grid; - grid-gap: $euiSizeXS; - padding: $euiSizeS; - background-color: #e9edf3; + grid-gap: $euiSizeXS / 2; + padding: $euiSizeS - ($euiSizeXS / 2) $euiSizeS $euiSizeS $euiSizeS; + background-color: $euiColorLightShade; border-radius: $euiBorderRadius; } &__field { display: grid; grid-template-columns: 1fr auto; - - // grid-template-columns: auto 1fr auto; grid-gap: $euiSizeS; padding: $euiSizeS $euiSizeM; align-items: center; } &__draggable { - padding: 2px 0; + padding: $euiSizeXS / 2 0; + animation: pop-in $euiAnimSpeedSlow $euiAnimSlightResistance forwards; + transform-origin: bottom; + + &.closing { + animation: pop-out $euiAnimSpeedSlow $euiAnimSlightResistance forwards; // Also update speed in dropbox.tsx + } } &__field_text { @@ -41,6 +45,8 @@ &__dropTarget { color: $euiColorDarkShade; grid-template-columns: 1fr auto; + transform-origin: top; + animation: pop-in $euiAnimSpeedFast $euiAnimSlightResistance forwards; &.validField { background-color: #a8d9e7; @@ -54,3 +60,43 @@ } } } + +@keyframes pop-in { + from { + max-height: 0; + opacity: 0; + } + + to { + max-height: 1000px; + opacity: 1; + } +} + +@keyframes pop-out { + from { + max-height: 1000px; + opacity: 1; + } + + to { + max-height: 0; + opacity: 0; + } +} + +@media (prefers-reduced-motion) { + .dropBox { + &__draggable { + animation: none; + + &.closing { + animation: none; + } + } + + &__dropTarget { + animation: none; + } + } +} diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx index 451bbe4e52eb..9c6c9e290f8c 100644 --- a/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx @@ -13,11 +13,12 @@ import { EuiText, DropResult, } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { IDropAttributes, IDropState } from '../../utils/drag_drop'; import './dropbox.scss'; import { useDropbox } from './use'; import { UseDropboxProps } from './use/use_dropbox'; +import { usePrefersReducedMotion } from './use/use_prefers_reduced_motion'; export interface DropboxDisplay { label: string; @@ -55,6 +56,8 @@ const DropboxComponent = ({ canDrop, dropProps, }: DropboxProps) => { + const prefersReducedMotion = usePrefersReducedMotion(); + const [closing, setClosing] = useState<boolean | string>(false); const handleDragEnd = useCallback( ({ source, destination }: DropResult) => { if (!destination) return; @@ -67,13 +70,32 @@ const DropboxComponent = ({ [fields, onReorderField] ); + const animateDelete = useCallback( + (id: string) => { + setClosing(id); + setTimeout( + () => { + onDeleteField(id); + setClosing(false); + }, + prefersReducedMotion ? 0 : 350 // Also update speed in dropbox.scss + ); + }, + [onDeleteField, prefersReducedMotion] + ); + return ( <EuiDragDropContext onDragEnd={handleDragEnd}> <EuiFormRow label={boxLabel} className="dropBox" fullWidth> <div className="dropBox__container"> <EuiDroppable droppableId={dropboxId}> {fields.map(({ id, label }, index) => ( - <EuiDraggable className="dropBox__draggable" key={id} draggableId={id} index={index}> + <EuiDraggable + className={`dropBox__draggable ${id === closing && 'closing'}`} + key={id} + draggableId={id} + index={index} + > <EuiPanel key={index} paddingSize="s" className="dropBox__field"> <EuiText size="s" className="dropBox__field_text" onClick={() => onEditField(id)}> <a role="button" tabIndex={0}> @@ -85,7 +107,7 @@ const DropboxComponent = ({ iconType="cross" aria-label="clear-field" iconSize="s" - onClick={() => onDeleteField(id)} + onClick={() => animateDelete(id)} /> </EuiPanel> </EuiDraggable> diff --git a/src/plugins/wizard/public/application/components/data_tab/use/use_prefers_reduced_motion.ts b/src/plugins/wizard/public/application/components/data_tab/use/use_prefers_reduced_motion.ts new file mode 100644 index 000000000000..b88c3bc444d0 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_prefers_reduced_motion.ts @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useState } from 'react'; + +const QUERY = '(prefers-reduced-motion: no-preference)'; + +export function usePrefersReducedMotion() { + const [prefersReducedMotion, setPrefersReducedMotion] = useState( + !window.matchMedia(QUERY).matches + ); + + useEffect(() => { + const mediaQueryList = window.matchMedia(QUERY); + const listener = (event) => { + setPrefersReducedMotion(!event.matches); + }; + + if (mediaQueryList.addEventListener) { + mediaQueryList.addEventListener('change', listener); + } + + return () => { + mediaQueryList.removeEventListener('change', listener); + }; + }, []); + + return prefersReducedMotion; +} diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 94e0d037b4f6..0ce78eba9074 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -44,6 +44,7 @@ export const Workspace: FC = ({ children }) => { if (errorMsg) { toasts.addWarning(errorMsg); } + setExpression(undefined); return; } const exp = await toExpression(rootState); From 01b7da4fb2679909fe8d147f21835467a321b7d3 Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Mon, 11 Jul 2022 18:11:43 -0700 Subject: [PATCH 14/47] [D&D] Basic saving, loading, and updating (#1870) * [D&D] Enable basic saving and loading - Add `/edit` route - Sync state for saving and loading - Add setter for vizualization slice - Switch from BrowserRouter to Router - Add version to saved objects - Add savedWizardLoader to services - store visualization and style states separately - add version - update breadcrumb handling - move useSavedWizardVis to top_nav - handle savedObjectNotFound - use savedObjectLoader correctly - allow copy on save - update url and chrome on save - add type for WizardVisSavedObject fixes #1867 Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../saved_objects_management/README.md | 2 +- src/plugins/wizard/common/index.ts | 1 + .../common/wizard_saved_object_attributes.ts | 7 +- .../public/application/components/top_nav.tsx | 53 +++++----- .../wizard/public/application/index.tsx | 13 ++- .../public/application/utils/breadcrumbs.ts | 42 ++++++++ .../application/utils/get_saved_wizard_vis.ts | 16 ++++ .../application/utils/get_top_nav_config.tsx | 96 +++++++++++++------ .../utils/state_management/store.ts | 3 + .../utils/state_management/style_slice.ts | 2 +- .../state_management/visualization_slice.ts | 6 +- .../utils/use/use_saved_wizard_vis.ts | 90 +++++++++++++++++ src/plugins/wizard/public/plugin.test.ts | 7 +- src/plugins/wizard/public/plugin.ts | 18 ++-- .../public/saved_visualizations/_saved_vis.ts | 17 ++-- src/plugins/wizard/public/types.ts | 16 +++- .../wizard/server/saved_objects/wizard_app.ts | 17 +++- 17 files changed, 322 insertions(+), 84 deletions(-) create mode 100644 src/plugins/wizard/public/application/utils/breadcrumbs.ts create mode 100644 src/plugins/wizard/public/application/utils/get_saved_wizard_vis.ts create mode 100644 src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md index 9fc21786c66b..748ce5b89922 100644 --- a/src/plugins/saved_objects_management/README.md +++ b/src/plugins/saved_objects_management/README.md @@ -7,7 +7,7 @@ From the primary UI page, this plugin allows you to: 2. Import/export saved objects 3. Inspect/edit raw saved object values without validation -For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are alos free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly). +For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are also free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly). ## Making a new saved object type manageable 1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See `SavedObjectsTypeManagementDefinition` for explanation of its properties: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285) diff --git a/src/plugins/wizard/common/index.ts b/src/plugins/wizard/common/index.ts index f2ceda0f2af5..b55653d41549 100644 --- a/src/plugins/wizard/common/index.ts +++ b/src/plugins/wizard/common/index.ts @@ -6,5 +6,6 @@ export const PLUGIN_ID = 'wizard'; export const PLUGIN_NAME = 'Wizard'; export const VISUALIZE_ID = 'visualize'; +export const EDIT_PATH = '/edit'; export { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from './wizard_saved_object_attributes'; diff --git a/src/plugins/wizard/common/wizard_saved_object_attributes.ts b/src/plugins/wizard/common/wizard_saved_object_attributes.ts index ff6c12417d24..9fd67783347b 100644 --- a/src/plugins/wizard/common/wizard_saved_object_attributes.ts +++ b/src/plugins/wizard/common/wizard_saved_object_attributes.ts @@ -3,12 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectAttributes } from 'opensearch-dashboards/public'; +import { integer } from '@opensearch-project/opensearch/api/types'; +import { SavedObjectAttributes } from '../../../core/types'; export const WIZARD_SAVED_OBJECT = 'wizard'; export interface WizardSavedObjectAttributes extends SavedObjectAttributes { title: string; description?: string; - state: string; + visualizationState?: string; + styleState?: string; + version: integer; } diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx index e2bdc2b33a24..299425e5b1d8 100644 --- a/src/plugins/wizard/public/application/components/top_nav.tsx +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -3,47 +3,54 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { i18n } from '@osd/i18n'; -import React, { useMemo, useEffect } from 'react'; -import { PLUGIN_ID, VISUALIZE_ID } from '../../../common'; +import React, { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { PLUGIN_ID } from '../../../common'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { getTopNavconfig } from '../utils/get_top_nav_config'; +import { getTopNavConfig } from '../utils/get_top_nav_config'; import { WizardServices } from '../../types'; import './top_nav.scss'; import { useIndexPattern } from '../utils/use'; +import { useTypedSelector } from '../utils/state_management'; +import { useSavedWizardVis } from '../utils/use/use_saved_wizard_vis'; export const TopNav = () => { + // id will only be set for the edit route + const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); const { services } = useOpenSearchDashboards<WizardServices>(); const { setHeaderActionMenu, - chrome, navigation: { ui: { TopNavMenu }, }, } = services; + const rootState = useTypedSelector((state) => state); + const hasUnappliedChanges = useTypedSelector( + (state) => !!state.visualization.activeVisualization?.draftAgg + ); - const config = useMemo(() => getTopNavconfig(services), [services]); - const indexPattern = useIndexPattern(); + const savedWizardVis = useSavedWizardVis(services, visualizationIdFromUrl); - useEffect(() => { - const visualizeHref = window.location.href.split(`${PLUGIN_ID}#/`)[0] + `${VISUALIZE_ID}#/`; - chrome.setBreadcrumbs([ - { - text: i18n.translate('visualize.listing.breadcrumb', { - defaultMessage: 'Visualize', - }), - href: visualizeHref, - }, + const config = useMemo(() => { + if (savedWizardVis === undefined) { + return; + } + const { visualization: visualizationState, style: styleState } = rootState; + + return getTopNavConfig( { - text: i18n.translate('wizard.nav.breadcrumb.create', { - defaultMessage: 'Create', - }), + visualizationIdFromUrl, + savedWizardVis, + visualizationState, + styleState, + hasUnappliedChanges, }, - ]); - // we want to run this hook exactly once, which you do by an empty dep array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + services + ); + }, [hasUnappliedChanges, rootState, savedWizardVis, services, visualizationIdFromUrl]); + + const indexPattern = useIndexPattern(); return ( <div className="wizTopNav"> diff --git a/src/plugins/wizard/public/application/index.tsx b/src/plugins/wizard/public/application/index.tsx index c451d082b153..28ee13a80bf6 100644 --- a/src/plugins/wizard/public/application/index.tsx +++ b/src/plugins/wizard/public/application/index.tsx @@ -5,25 +5,30 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { Router, Route, Switch } from 'react-router-dom'; import { Provider as ReduxProvider } from 'react-redux'; import { Store } from 'redux'; import { AppMountParameters } from '../../../../core/public'; import { WizardServices } from '../types'; import { WizardApp } from './app'; import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; +import { EDIT_PATH } from '../../common'; export const renderApp = ( - { appBasePath, element }: AppMountParameters, + { element, history }: AppMountParameters, services: WizardServices, store: Store ) => { ReactDOM.render( - <Router basename={appBasePath}> + <Router history={history}> <OpenSearchDashboardsContextProvider services={services}> <ReduxProvider store={store}> <services.i18n.Context> - <WizardApp /> + <Switch> + <Route path={[`${EDIT_PATH}/:id`, '/']} exact={false}> + <WizardApp /> + </Route> + </Switch> </services.i18n.Context> </ReduxProvider> </OpenSearchDashboardsContextProvider> diff --git a/src/plugins/wizard/public/application/utils/breadcrumbs.ts b/src/plugins/wizard/public/application/utils/breadcrumbs.ts new file mode 100644 index 000000000000..7bdb41075f35 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/breadcrumbs.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { VISUALIZE_ID } from '../../../common'; + +const defaultEditText = i18n.translate('wizard.editor.defaultEditBreadcrumbText', { + defaultMessage: 'Edit', +}); + +export function getVisualizeLandingBreadcrumbs(navigateToApp) { + return [ + { + text: i18n.translate('wizard.listing.breadcrumb', { + defaultMessage: 'Visualize', + }), + onClick: () => navigateToApp(VISUALIZE_ID), + }, + ]; +} + +export function getCreateBreadcrumbs(navigateToApp) { + return [ + ...getVisualizeLandingBreadcrumbs(navigateToApp), + { + text: i18n.translate('wizard.editor.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]; +} + +export function getEditBreadcrumbs(text: string = defaultEditText, navigateToApp) { + return [ + ...getVisualizeLandingBreadcrumbs(navigateToApp), + { + text, + }, + ]; +} diff --git a/src/plugins/wizard/public/application/utils/get_saved_wizard_vis.ts b/src/plugins/wizard/public/application/utils/get_saved_wizard_vis.ts new file mode 100644 index 000000000000..f56169114f60 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/get_saved_wizard_vis.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { WizardServices } from '../..'; + +export const getSavedWizardVis = async (services: WizardServices, wizardVisId?: string) => { + const { savedWizardLoader } = services; + if (!savedWizardLoader) { + return {}; + } + const savedWizardVis = await savedWizardLoader.get(wizardVisId); + + return savedWizardVis; +}; diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx index 725f7f2baa92..670513187843 100644 --- a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx @@ -37,22 +37,50 @@ import { showSaveModal, } from '../../../../saved_objects/public'; import { WizardServices } from '../..'; +import { WizardVisSavedObject } from '../../types'; +import { StyleState, VisualizationState } from './state_management'; +import { EDIT_PATH } from '../../../common'; +interface TopNavConfigParams { + visualizationIdFromUrl: string; + savedWizardVis: WizardVisSavedObject; + visualizationState: VisualizationState; + styleState: StyleState; + hasUnappliedChanges: boolean; +} -export const getTopNavconfig = ({ - savedObjects: { client: savedObjectsClient }, - toastNotifications, - i18n: { Context: I18nContext }, -}: WizardServices) => { +export const getTopNavConfig = ( + { + visualizationIdFromUrl, + savedWizardVis, + visualizationState, + styleState, + hasUnappliedChanges, + }: TopNavConfigParams, + { history, toastNotifications, i18n: { Context: I18nContext } }: WizardServices +) => { const topNavConfig: TopNavMenuData[] = [ { id: 'save', iconType: 'save', - emphasize: true, - label: 'Save', + emphasize: savedWizardVis && !savedWizardVis.id, + description: i18n.translate('wizard.topNavMenu.saveVisualizationButtonAriaLabel', { + defaultMessage: 'Save Visualization', + }), + className: 'saveButton', + label: i18n.translate('wizard.topNavMenu.saveVisualizationButtonLabel', { + defaultMessage: 'save', + }), testId: 'wizardSaveButton', - run: (anchorElement) => { + disableButton: hasUnappliedChanges, + tooltip() { + if (hasUnappliedChanges) { + return i18n.translate('wizard.topNavMenu.saveVisualizationDisabledButtonTooltip', { + defaultMessage: 'Apply aggregation configuration changes before saving', // TODO: Update text to match agg save flow + }); + } + }, + run: (_anchorElement) => { const onSave = async ({ - // TODO: Figure out what the other props here do newTitle, newCopyOnSave, isTitleDuplicateConfirmed, @@ -60,15 +88,22 @@ export const getTopNavconfig = ({ newDescription, returnToOrigin, }: OnSaveProps & { returnToOrigin: boolean }) => { - // TODO: Save the actual state of the wizard - const wizardSavedObject = await savedObjectsClient.create('wizard', { - title: newTitle, - description: newDescription, - state: JSON.stringify({}), - }); + if (!savedWizardVis) { + return; + } + savedWizardVis.visualizationState = JSON.stringify(visualizationState); + savedWizardVis.styleState = JSON.stringify(styleState); + savedWizardVis.title = newTitle; + savedWizardVis.description = newDescription; + savedWizardVis.copyOnSave = newCopyOnSave; try { - const id = await wizardSavedObject.save(); + const id = await savedWizardVis.save({ + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + returnToOrigin, + }); if (id) { toastNotifications.addSuccess({ @@ -77,13 +112,21 @@ export const getTopNavconfig = ({ { defaultMessage: `Saved '{visTitle}'`, values: { - visTitle: newTitle, + visTitle: savedWizardVis.title, }, } ), 'data-test-subj': 'saveVisualizationSuccess', }); + // Update URL + if (id !== visualizationIdFromUrl) { + history.push({ + ...history.location, + pathname: `${EDIT_PATH}/${id}`, + }); + } + return { id }; } @@ -93,15 +136,12 @@ export const getTopNavconfig = ({ console.error(error); toastNotifications.addDanger({ - title: i18n.translate( - 'visualize.topNavMenu.saveVisualization.failureNotificationText', - { - defaultMessage: `Error on saving '{visTitle}'`, - values: { - visTitle: newTitle, - }, - } - ), + title: i18n.translate('wizard.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving '{visTitle}'`, + values: { + visTitle: newTitle, + }, + }), text: error.message, 'data-test-subj': 'saveVisualizationError', }); @@ -111,9 +151,9 @@ export const getTopNavconfig = ({ const saveModal = ( <SavedObjectSaveModalOrigin - documentInfo={{ title: '' }} + documentInfo={savedWizardVis} onSave={onSave} - objectType={'visualization'} + objectType={'wizard'} onClose={() => {}} /> ); diff --git a/src/plugins/wizard/public/application/utils/state_management/store.ts b/src/plugins/wizard/public/application/utils/state_management/store.ts index 29af0e9d73b5..d8fff094cd1e 100644 --- a/src/plugins/wizard/public/application/utils/state_management/store.ts +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -30,3 +30,6 @@ export const getPreloadedStore = async (services: WizardServices) => { export type RootState = ReturnType<typeof rootReducer>; type Store = ReturnType<typeof configurePreloadedStore>; export type AppDispatch = Store['dispatch']; + +export { setState as setStyleState, StyleState } from './style_slice'; +export { setState as setVisualizationState, VisualizationState } from './visualization_slice'; diff --git a/src/plugins/wizard/public/application/utils/state_management/style_slice.ts b/src/plugins/wizard/public/application/utils/state_management/style_slice.ts index 98a425184d45..55579c759ccf 100644 --- a/src/plugins/wizard/public/application/utils/state_management/style_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/style_slice.ts @@ -6,7 +6,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { WizardServices } from '../../../types'; -type StyleState<T = any> = T; +export type StyleState<T = any> = T; const initialState = {} as StyleState; diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts index f7d4d3ff6d49..06c865e14395 100644 --- a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -7,7 +7,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { CreateAggConfigParams } from '../../../../../data/common'; import { WizardServices } from '../../../types'; -interface VisualizationState { +export interface VisualizationState { indexPattern?: string; searchField: string; activeVisualization?: { @@ -105,6 +105,9 @@ export const slice = createSlice({ updateAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams[]>) => { state.activeVisualization!.aggConfigParams = action.payload; }, + setState: (_state, action: PayloadAction<VisualizationState>) => { + return action.payload; + }, }, }); @@ -117,4 +120,5 @@ export const { updateAggConfigParams, saveAgg, reorderAgg, + setState, } = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts new file mode 100644 index 000000000000..30b72199f10d --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts @@ -0,0 +1,90 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { useEffect, useState } from 'react'; +import { SavedObject } from '../../../../../saved_objects/public'; +import { + redirectWhenMissing, + SavedObjectNotFound, +} from '../../../../../opensearch_dashboards_utils/public'; +import { EDIT_PATH, PLUGIN_ID } from '../../../../common'; +import { WizardServices } from '../../../types'; +import { MetricOptionsDefaults } from '../../../visualizations/metric/metric_viz_type'; +import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; +import { getSavedWizardVis } from '../get_saved_wizard_vis'; +import { useTypedDispatch, setStyleState, setVisualizationState } from '../state_management'; + +export const useSavedWizardVis = ( + services: WizardServices, + visualizationIdFromUrl: string | undefined +) => { + const [savedVisState, setSavedVisState] = useState<SavedObject | undefined>(undefined); + const dispatch = useTypedDispatch(); + + useEffect(() => { + const { + application: { navigateToApp }, + chrome, + history, + http: { basePath }, + toastNotifications, + } = services; + const loadSavedWizardVis = async () => { + try { + const savedWizardVis = await getSavedWizardVis(services, visualizationIdFromUrl); + + if (savedWizardVis.id) { + chrome.setBreadcrumbs(getEditBreadcrumbs(savedWizardVis.title, navigateToApp)); + chrome.docTitle.change(savedWizardVis.title); + } else { + chrome.setBreadcrumbs(getCreateBreadcrumbs(navigateToApp)); + } + + if (savedWizardVis.styleState !== '{}' && savedWizardVis.visualizationState !== '{}') { + const styleState = JSON.parse(savedWizardVis.styleState); + const visualizationState = JSON.parse(savedWizardVis.visualizationState); + // TODO: Add validation and transformation, throw/handle errors + dispatch(setStyleState<MetricOptionsDefaults>(styleState)); + dispatch(setVisualizationState(visualizationState)); + } + + setSavedVisState(savedWizardVis); + } catch (error) { + const managementRedirectTarget = { + [PLUGIN_ID]: { + app: 'management', + path: `opensearch-dashboards/objects/savedWizard/${visualizationIdFromUrl}`, + }, + }; + + try { + if (error instanceof SavedObjectNotFound) { + redirectWhenMissing({ + history, + navigateToApp, + toastNotifications, + basePath, + mapping: managementRedirectTarget, + })(error); + } + } catch (e) { + const message = e instanceof Error ? e.message : ''; + toastNotifications.addWarning({ + title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { + defaultMessage: 'Failed to load the visualization', + }), + text: message, + }); + history.replace(EDIT_PATH); + } + } + }; + + loadSavedWizardVis(); + }, [dispatch, services, visualizationIdFromUrl]); + + return savedVisState; +}; diff --git a/src/plugins/wizard/public/plugin.test.ts b/src/plugins/wizard/public/plugin.test.ts index c83f4c6a8458..74936288a76f 100644 --- a/src/plugins/wizard/public/plugin.test.ts +++ b/src/plugins/wizard/public/plugin.test.ts @@ -9,11 +9,12 @@ import { dataPluginMock } from '../../../plugins/data/public/mocks'; import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks'; import { navigationPluginMock } from '../../../plugins/navigation/public/mocks'; import { visualizationsPluginMock } from '../../../plugins/visualizations/public/mocks'; +import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { WizardPlugin } from './plugin'; describe('WizardPlugin', () => { describe('setup', () => { - it('initializes the plugin correctly and registers it as an alias vizualization', () => { + it('initializes the plugin correctly and registers it as an alias visualization', () => { const plugin = new WizardPlugin(coreMock.createPluginInitializerContext()); const pluginStartContract = { data: dataPluginMock.createStartContract(), @@ -34,8 +35,8 @@ describe('WizardPlugin', () => { expect(setupDeps.visualizations.registerAlias).toHaveBeenCalledWith( // TODO: Update this once the properties are final expect.objectContaining({ - name: 'wizard', - title: 'Wizard', + name: PLUGIN_ID, + title: PLUGIN_NAME, aliasPath: '#/', }) ); diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 6bd8a3d970d0..7af37ccf61b6 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -20,7 +20,7 @@ import { WizardStart, } from './types'; import wizardIcon from './assets/wizard_icon.svg'; -import { PLUGIN_NAME } from '../common'; +import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { TypeService } from './services/type_service'; import { getPreloadedStore } from './application/utils/state_management'; import { setAggService, setIndexPatterns } from './plugin_services'; @@ -35,7 +35,7 @@ export class WizardPlugin constructor(public initializerContext: PluginInitializerContext) {} public setup( - core: CoreSetup<WizardPluginStartDependencies>, + core: CoreSetup<WizardPluginStartDependencies, WizardStart>, { visualizations }: WizardPluginSetupDependencies ) { const typeService = this.typeService; @@ -43,7 +43,7 @@ export class WizardPlugin // Register the plugin to core core.application.register({ - id: 'wizard', + id: PLUGIN_ID, title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { @@ -51,7 +51,7 @@ export class WizardPlugin const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json - const [coreStart, pluginsStart] = await core.getStartServices(); + const [coreStart, pluginsStart, selfStart] = await core.getStartServices(); const { data, savedObjects, navigation, expressions } = pluginsStart; // make sure the index pattern list is up to date @@ -74,8 +74,10 @@ export class WizardPlugin savedObjectsPublic: savedObjects, navigation, expressions, + history: params.history, setHeaderActionMenu: params.setHeaderActionMenu, types: typeService.start(), + savedWizardLoader: selfStart.savedWizardLoader, }; // Instantiate the store @@ -88,15 +90,15 @@ export class WizardPlugin // Register the plugin as an alias to create visualization visualizations.registerAlias({ - name: 'wizard', - title: 'Wizard', - description: i18n.translate('wizard.vizPicker.description', { + name: PLUGIN_ID, + title: PLUGIN_NAME, + description: i18n.translate('wizard.visPicker.description', { defaultMessage: 'TODO...', }), // TODO: Replace with actual icon once available icon: wizardIcon, stage: 'beta', - aliasApp: 'wizard', + aliasApp: PLUGIN_ID, aliasPath: '#/', }); diff --git a/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts index 8ff823812f73..66a02a974dd6 100644 --- a/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts @@ -7,20 +7,21 @@ import { createSavedObjectClass, SavedObjectOpenSearchDashboardsServices, } from '../../../saved_objects/public'; +import { EDIT_PATH, PLUGIN_ID, WIZARD_SAVED_OBJECT } from '../../common'; export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboardsServices) { const SavedObjectClass = createSavedObjectClass(services); class SavedWizardVis extends SavedObjectClass { - public static type = 'wizard'; + public static type = WIZARD_SAVED_OBJECT; // if type:wizard has no mapping, we push this mapping into OpenSearch public static mapping = { title: 'text', description: 'text', - state: 'text', - // savedSearchId: 'keyword', - // version: 'integer', + visualizationState: 'text', + styleState: 'text', + version: 'integer', }; // Order these fields to the top, the rest are alphabetical @@ -39,13 +40,13 @@ export function createSavedWizardVisClass(services: SavedObjectOpenSearchDashboa defaults: { title: '', description: '', - state: '{}', - // savedSearchId, - // version: 1, + visualizationState: '{}', + styleState: '{}', + version: 1, }, }); this.showInRecentlyAccessed = true; - this.getFullPath = () => `/app/wizard#/edit/${this.id}`; + this.getFullPath = () => `/app/${PLUGIN_ID}${EDIT_PATH}/${this.id}`; } } diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts index 8ebcea1e19b8..55b3f1b41343 100644 --- a/src/plugins/wizard/public/types.ts +++ b/src/plugins/wizard/public/types.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SavedObjectsStart } from '../../saved_objects/public'; +import { History } from 'history'; +import { SavedObject, SavedObjectsStart } from '../../saved_objects/public'; import { EmbeddableSetup } from '../../embeddable/public'; import { DashboardStart } from '../../dashboard/public'; import { VisualizationsSetup } from '../../visualizations/public'; @@ -33,10 +34,23 @@ export interface WizardPluginStartDependencies { export interface WizardServices extends CoreStart { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + savedWizardLoader: WizardStart['savedWizardLoader']; toastNotifications: ToastsStart; savedObjectsPublic: SavedObjectsStart; navigation: NavigationPublicPluginStart; data: DataPublicPluginStart; types: TypeServiceStart; expressions: ExpressionsStart; + history: History; } + +export interface ISavedVis { + id?: string; + title: string; + description?: string; + visualizationState?: string; + styleState?: string; + version?: number; +} + +export interface WizardVisSavedObject extends SavedObject, ISavedVis {} diff --git a/src/plugins/wizard/server/saved_objects/wizard_app.ts b/src/plugins/wizard/server/saved_objects/wizard_app.ts index 7060dfafe18c..9f17dd502a84 100644 --- a/src/plugins/wizard/server/saved_objects/wizard_app.ts +++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts @@ -4,7 +4,12 @@ */ import { SavedObject, SavedObjectsType } from '../../../../core/server'; -import { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from '../../common'; +import { + EDIT_PATH, + PLUGIN_ID, + WizardSavedObjectAttributes, + WIZARD_SAVED_OBJECT, +} from '../../common'; export const wizardSavedObjectType: SavedObjectsType = { name: WIZARD_SAVED_OBJECT, @@ -19,7 +24,7 @@ export const wizardSavedObjectType: SavedObjectsType = { `/management/opensearch-dashboards/objects/savedWizard/${encodeURIComponent(id)}`, getInAppUrl({ id }: SavedObject) { return { - path: `/app/wizard#/edit/${encodeURIComponent(id)}`, + path: `/app/${PLUGIN_ID}${EDIT_PATH}/${encodeURIComponent(id)}`, uiCapabilitiesPath: 'wizard.show', }; }, @@ -33,11 +38,15 @@ export const wizardSavedObjectType: SavedObjectsType = { description: { type: 'text', }, - // TODO: Determine what needs to be pulled out of state and added directly into the mapping - state: { + visualizationState: { type: 'text', index: false, }, + styleState: { + type: 'text', + index: false, + }, + version: { type: 'integer' }, }, }, }; From cd73e5ece50d4108b1c208f3b3132240bb896e21 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Fri, 15 Jul 2022 03:37:40 -0700 Subject: [PATCH 15/47] chore: Adds a few readme's (#1894) Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- src/plugins/navigation/README.md | 8 +++++ src/plugins/wizard/README.md | 56 +++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/plugins/navigation/README.md b/src/plugins/navigation/README.md index 2b32cb50f0b1..db65dd82ccfe 100644 --- a/src/plugins/navigation/README.md +++ b/src/plugins/navigation/README.md @@ -3,3 +3,11 @@ The navigation plugins exports the `TopNavMenu` component. It also provides a stateful version of it on the `start` contract. +## navigation.ui.TopNavMenu + +The `naivgation.ui` module exposes the `TopNavMenu` component that features The search bar, time filter and menu options to be used across the app in multiple locations. It primarity consists of 2 components: + +- Menu options: Options to show on the menu bar alongside the breadcrumbs +- Search bar: The [`data.ui.SearchBar`](../data/public/ui/search_bar/) component responsible for the query bar, time filter and field filters. + +Most of the logic for the component resides in the `SearchBar` component. This simply adds a way to add menu options on top of the search bar. diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index bcb362b374cb..9cd95d4810db 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -1,10 +1,58 @@ -# wizard +# Wizard -A OpenSearch Dashboards plugin +A OpenSearch Dashboards plugin for the visualization experience that makes exploring data and creating visualizations much easier. It will act as an additional way to create visualizations alongside the exiting tools within the current visualizations plugin. The tool will be incremental to the visualization tools available to the user in OpenSearch Dashboards today. ---- +## Usage -## Development +To use this plugin, navigate to: + +Visualize -> Create Visualization -> Wizard + +## Add a visualization (TODO: Cleanup before merging into mainline) + +All new visualizations currently reside in [public/visualizations](./public/visualizations). To add a new one, create a new visualization folder and add the required code to setup and register a new vis type. + +### Anatomy of a visualization + +``` +metric/ +├─ metric_viz_type.ts +├─ index.ts +├─ to_expression.ts +├─ components/ + ├─ metric_viz_options.tsx +``` + +Outline: +- index.ts: Exposes the create<Viz>Config function that is used to register the viz type +- <vizName>_viz_type.ts: Contains the config that the type service needs to register the new vis type. +- to_expression.ts: The expression function that the plugin will use to render the visualization given the state of the plugin +- <vizName>_viz_options.tsx: The component that will render the other properties that user can set in the `Style` tab. + +**Notes:** + +- Currently only the metric viz is setup so schema properties that other vis types need may not be setup fully and need to be set correctly. +- `to_expression` is quite custom and can be abstracted into a common utility for different visualizations. Adding more vis types should make it clear as to how this can be done + + +## Development (TODO: Delete before merging into mainline) + +All work for this feature currently happens on the `feature/d-and-d` branch + +### Git workflow + +Set main repo as the `upstream` remote +```sh +git remote add upstream https://github.com/opensearch-project/OpenSearch-Dashboards.git +``` + +Keeping the `feature/d-and-d` branch up to date locally + +```sh +git fetch upstream +git checkout feature/d-and-d +git merge upstream/feature/d-and-d +``` See the [OpenSearch Dashboards contributing guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/CONTRIBUTING.md) for instructions From 58c136563d7a0c1c463f3b1ed7d4e7f2ed8ecc76 Mon Sep 17 00:00:00 2001 From: Kartik Desai <xdeskart@amazon.com> Date: Mon, 18 Jul 2022 14:17:38 -0500 Subject: [PATCH 16/47] [D&D] Empty-workspace polish (#1900) * empty workspace polish Signed-off-by: kaddy645 <xdeskart@amazon.com> * pr update Signed-off-by: kaddy645 <xdeskart@amazon.com> * pr update 2 Signed-off-by: kaddy645 <xdeskart@amazon.com> --- .../application/components/workspace.scss | 24 ++++++++++ .../application/components/workspace.tsx | 22 ++++++++-- .../wizard/public/assets/fields_bg.svg | 36 +++++++++++++++ .../wizard/public/assets/hand_field.svg | 44 +++++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 src/plugins/wizard/public/assets/fields_bg.svg create mode 100644 src/plugins/wizard/public/assets/hand_field.svg diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss index 58bcd6f1e7cc..0a4465c0e692 100644 --- a/src/plugins/wizard/public/application/components/workspace.scss +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -1,5 +1,6 @@ .wizWorkspace { display: grid; + -ms-grid-rows: auto $euiSizeM 1fr; grid-template-rows: auto 1fr; grid-area: workspace; grid-gap: $euiSizeM; @@ -9,4 +10,27 @@ &__empty { height: 100%; } + + &__container { + position: relative; + } + + &__handFieldSvg { + animation-name: dragAndDropAnimation; + animation-direction: alternate-reverse; + animation-duration: 1.5s; + animation-iteration-count: infinite; + position: absolute; + top: 43%; + } +} + +@keyframes dragAndDropAnimation { + 0% { + transform: translate(65%, 0); + } + + 100% { + transform: none; + } } diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 0ce78eba9074..4d30dbb76463 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -14,7 +14,7 @@ import { EuiPanel, EuiPopover, } from '@elastic/eui'; -import React, { FC, useState, useMemo, useEffect } from 'react'; +import React, { FC, useState, useMemo, useEffect, Fragment } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../types'; import { validateSchemaState } from '../utils/validate_schema_state'; @@ -22,6 +22,9 @@ import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; import { setActiveVisualization } from '../utils/state_management/visualization_slice'; import { useVisualizationType } from '../utils/use'; +import hand_field from '../../assets/hand_field.svg'; +import fields_bg from '../../assets/fields_bg.svg'; + import './workspace.scss'; export const Workspace: FC = ({ children }) => { @@ -67,9 +70,20 @@ export const Workspace: FC = ({ children }) => { ) : ( <EuiFlexItem className="wizWorkspace__empty"> <EuiEmptyPrompt - iconType="visBarVertical" - title={<h2>Welcome to the wizard!</h2>} - body={<p>Drag some fields onto the panel to visualize some data.</p>} + title={<h2>Drop some fields here to start</h2>} + body={ + <Fragment> + <p>Drag a field directly to the canvas or axis to generate a visualization.</p> + <span className="wizWorkspace__container"> + <EuiIcon className="wizWorkspace__fieldSvg" type={fields_bg} size="original" /> + <EuiIcon + className="wizWorkspace__handFieldSvg" + type={hand_field} + size="original" + /> + </span> + </Fragment> + } /> </EuiFlexItem> )} diff --git a/src/plugins/wizard/public/assets/fields_bg.svg b/src/plugins/wizard/public/assets/fields_bg.svg new file mode 100644 index 000000000000..b319b6937406 --- /dev/null +++ b/src/plugins/wizard/public/assets/fields_bg.svg @@ -0,0 +1,36 @@ +<svg width="336" height="220" viewBox="0 0 336 220" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="118" y="2" width="216" height="216" rx="2" fill="url(#paint0_diamond_169_44976)" stroke="#006BB4" stroke-width="4" stroke-linecap="round" stroke-dasharray="80 80"/> +<g opacity="0.5"> +<path d="M0 99.6C0 98.1641 1.14799 97 2.5641 97H97.4359C98.852 97 100 98.1641 100 99.6V120.4C100 121.836 98.852 123 97.4359 123H2.5641C1.14799 123 0 121.836 0 120.4V99.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 99.6H2.5641L2.5641 120.4H97.4359V99.6ZM2.5641 97C1.14799 97 0 98.1641 0 99.6V120.4C0 121.836 1.14799 123 2.5641 123H97.4359C98.852 123 100 121.836 100 120.4V99.6C100 98.1641 98.852 97 97.4359 97H2.5641Z" fill="#343741"/> +<path d="M15.155 104.091L14.6642 106.413H16.4707L16.0426 108.433H14.2361L13.359 112.723C13.3102 113.016 13.3329 113.238 13.4268 113.387C13.5208 113.537 13.7558 113.618 14.1317 113.629C14.2779 113.635 14.5772 113.62 15.0297 113.586L14.7791 115.692C14.2013 115.847 13.5852 115.919 12.9308 115.908C11.8657 115.896 11.0687 115.637 10.5396 115.131C10.0105 114.625 9.79474 113.937 9.8922 113.068L10.8111 108.433H9.41187L9.82955 106.413H11.2288L11.7196 104.091H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 110C22.353 109.347 22.9324 108.818 23.6471 108.818H34.0001C34.7148 108.818 35.2942 109.347 35.2942 110C35.2942 110.653 34.7148 111.182 34.0001 111.182H23.6471C22.9324 111.182 22.353 110.653 22.353 110Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 110C43.5293 109.347 44.0999 108.818 44.8038 108.818L88.1371 108.818C88.841 108.818 89.4117 109.347 89.4117 110C89.4117 110.653 88.841 111.182 88.1371 111.182L44.8038 111.182C44.0999 111.182 43.5293 110.653 43.5293 110Z" fill="#343741"/> +<path d="M0 67.6C0 66.1641 1.14799 65 2.5641 65H97.4359C98.852 65 100 66.1641 100 67.6V88.4C100 89.8359 98.852 91 97.4359 91H2.5641C1.14799 91 0 89.8359 0 88.4V67.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 67.6H2.5641L2.5641 88.4H97.4359V67.6ZM2.5641 65C1.14799 65 0 66.1641 0 67.6V88.4C0 89.8359 1.14799 91 2.5641 91H97.4359C98.852 91 100 89.8359 100 88.4V67.6C100 66.1641 98.852 65 97.4359 65H2.5641Z" fill="#343741"/> +<path d="M15.155 70.9091L14.6642 73.4633H16.4707L16.0426 75.6851H14.2361L13.359 80.4041C13.3102 80.727 13.3329 80.9707 13.4268 81.1353C13.5208 81.2998 13.7558 81.3885 14.1317 81.4011C14.2779 81.4075 14.5772 81.3916 15.0297 81.3536L14.7791 83.6704C14.2013 83.8413 13.5852 83.9205 12.9308 83.9078C11.8657 83.8951 11.0687 83.6103 10.5396 83.0533C10.0105 82.4962 9.79474 81.7398 9.8922 80.7839L10.8111 75.6851H9.41187L9.82955 73.4633H11.2288L11.7196 70.9091H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 78C22.353 77.3473 22.9324 76.8182 23.6471 76.8182H34.0001C34.7148 76.8182 35.2942 77.3473 35.2942 78C35.2942 78.6527 34.7148 79.1818 34.0001 79.1818H23.6471C22.9324 79.1818 22.353 78.6527 22.353 78Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 78C43.5293 77.3473 44.0999 76.8182 44.8038 76.8182L88.1371 76.8182C88.841 76.8182 89.4117 77.3473 89.4117 78C89.4117 78.6527 88.841 79.1818 88.1371 79.1818L44.8038 79.1818C44.0999 79.1818 43.5293 78.6527 43.5293 78Z" fill="#343741"/> +<path d="M0 35.6C0 34.1641 1.14799 33 2.5641 33H97.4359C98.852 33 100 34.1641 100 35.6V56.4C100 57.8359 98.852 59 97.4359 59H2.5641C1.14799 59 0 57.8359 0 56.4V35.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 35.6H2.5641L2.5641 56.4H97.4359V35.6ZM2.5641 33C1.14799 33 0 34.1641 0 35.6V56.4C0 57.8359 1.14799 59 2.5641 59H97.4359C98.852 59 100 57.8359 100 56.4V35.6C100 34.1641 98.852 33 97.4359 33H2.5641Z" fill="#343741"/> +<path d="M15.155 38.9091L14.6642 41.4633H16.4707L16.0426 43.6851H14.2361L13.359 48.4041C13.3102 48.727 13.3329 48.9707 13.4268 49.1353C13.5208 49.2998 13.7558 49.3885 14.1317 49.4011C14.2779 49.4075 14.5772 49.3916 15.0297 49.3536L14.7791 51.6704C14.2013 51.8413 13.5852 51.9205 12.9308 51.9078C11.8657 51.8951 11.0687 51.6103 10.5396 51.0533C10.0105 50.4962 9.79474 49.7398 9.8922 48.7839L10.8111 43.6851H9.41187L9.82955 41.4633H11.2288L11.7196 38.9091H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 46C22.353 45.3473 22.9324 44.8182 23.6471 44.8182H34.0001C34.7148 44.8182 35.2942 45.3473 35.2942 46C35.2942 46.6527 34.7148 47.1818 34.0001 47.1818H23.6471C22.9324 47.1818 22.353 46.6527 22.353 46Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 46C43.5293 45.3473 44.0999 44.8182 44.8038 44.8182L88.1371 44.8182C88.841 44.8182 89.4117 45.3473 89.4117 46C89.4117 46.6527 88.841 47.1818 88.1371 47.1818L44.8038 47.1818C44.0999 47.1818 43.5293 46.6527 43.5293 46Z" fill="#343741"/> +<path d="M0 131.6C0 130.164 1.14799 129 2.5641 129H97.4359C98.852 129 100 130.164 100 131.6V152.4C100 153.836 98.852 155 97.4359 155H2.5641C1.14799 155 0 153.836 0 152.4V131.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 131.6H2.5641L2.5641 152.4H97.4359V131.6ZM2.5641 129C1.14799 129 0 130.164 0 131.6V152.4C0 153.836 1.14799 155 2.5641 155H97.4359C98.852 155 100 153.836 100 152.4V131.6C100 130.164 98.852 129 97.4359 129H2.5641Z" fill="#343741"/> +<path d="M12.6653 144.581H11.4987L10.4188 147.909H8.83443L9.91433 144.581H8.23535L8.48759 143.096H10.3873L11.0888 140.953H9.41773L9.67786 139.468H11.5697L12.6653 136.091H14.2419L13.1462 139.468H14.3207L15.4164 136.091H17.0007L15.9051 139.468H17.6471L17.3949 140.953H15.4321L14.7306 143.096H16.4569L16.2046 144.581H14.2497L13.1777 147.909H11.5933L12.6653 144.581ZM11.9717 143.096H13.1383L13.8477 140.953H12.6732L11.9717 143.096Z" fill="#387765"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 142C22.353 141.347 22.9324 140.818 23.6471 140.818H34.0001C34.7148 140.818 35.2942 141.347 35.2942 142C35.2942 142.653 34.7148 143.182 34.0001 143.182H23.6471C22.9324 143.182 22.353 142.653 22.353 142Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 142C43.5293 141.347 44.0999 140.818 44.8038 140.818L88.1371 140.818C88.841 140.818 89.4117 141.347 89.4117 142C89.4117 142.653 88.841 143.182 88.1371 143.182L44.8038 143.182C44.0999 143.182 43.5293 142.653 43.5293 142Z" fill="#343741"/> +<path d="M0 163.6C0 162.164 1.14799 161 2.5641 161H97.4359C98.852 161 100 162.164 100 163.6V184.4C100 185.836 98.852 187 97.4359 187H2.5641C1.14799 187 0 185.836 0 184.4V163.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 163.6H2.5641L2.5641 184.4H97.4359V163.6ZM2.5641 161C1.14799 161 0 162.164 0 163.6V184.4C0 185.836 1.14799 187 2.5641 187H97.4359C98.852 187 100 185.836 100 184.4V163.6C100 162.164 98.852 161 97.4359 161H2.5641Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8236 179.398C18.8236 180.333 18.1451 181.091 17.3066 181.091H9.75239C8.91455 181.091 8.23535 180.334 8.23535 179.398V170.966C8.23535 170.031 8.9138 169.273 9.75239 169.273H11.4118V168.687C11.4118 168.395 11.6059 168.151 11.8483 168.1L11.9412 168.091C12.2336 168.091 12.4706 168.344 12.4706 168.687V169.273H14.5883V168.687C14.5883 168.395 14.7823 168.151 15.0248 168.1L15.1177 168.091C15.4101 168.091 15.6471 168.344 15.6471 168.687V169.273H17.3066C18.1444 169.273 18.8236 170.03 18.8236 170.966V179.398ZM9.29418 172.818V179.117C9.29418 179.555 9.61176 179.909 10.0042 179.909H17.0547C17.4473 179.909 17.7648 179.555 17.7648 179.117V172.818H9.29418ZM10.8824 177.545C11.1423 177.545 11.3585 177.762 11.4033 178.033L11.4118 178.136C11.4118 178.426 11.2178 178.668 10.9753 178.718L10.8824 178.727C10.59 178.727 10.353 178.453 10.353 178.136C10.353 177.846 10.547 177.605 10.7895 177.555L10.8824 177.545ZM13.5295 177.545C13.7894 177.545 14.0055 177.762 14.0504 178.033L14.0589 178.136C14.0589 178.426 13.8648 178.668 13.6224 178.718L13.5295 178.727C13.2371 178.727 13.0001 178.453 13.0001 178.136C13.0001 177.846 13.1941 177.605 13.4366 177.555L13.5295 177.545ZM16.1765 177.545C16.4364 177.545 16.6526 177.762 16.6974 178.033L16.7059 178.136C16.7059 178.426 16.5119 178.668 16.2694 178.718L16.1765 178.727C15.8841 178.727 15.6471 178.453 15.6471 178.136C15.6471 177.846 15.8412 177.605 16.0836 177.555L16.1765 177.545ZM10.8824 175.773C11.1423 175.773 11.3585 175.989 11.4033 176.26L11.4118 176.364C11.4118 176.654 11.2178 176.895 10.9753 176.945L10.8824 176.955C10.59 176.955 10.353 176.68 10.353 176.364C10.353 176.074 10.547 175.832 10.7895 175.782L10.8824 175.773ZM13.5295 175.773C13.7894 175.773 14.0055 175.989 14.0504 176.26L14.0589 176.364C14.0589 176.654 13.8648 176.895 13.6224 176.945L13.5295 176.955C13.2371 176.955 13.0001 176.68 13.0001 176.364C13.0001 176.074 13.1941 175.832 13.4366 175.782L13.5295 175.773ZM16.1765 175.773C16.4364 175.773 16.6526 175.989 16.6974 176.26L16.7059 176.364C16.7059 176.654 16.5119 176.895 16.2694 176.945L16.1765 176.955C15.8841 176.955 15.6471 176.68 15.6471 176.364C15.6471 176.074 15.8412 175.832 16.0836 175.782L16.1765 175.773ZM10.8824 174C11.1423 174 11.3585 174.217 11.4033 174.487L11.4118 174.591C11.4118 174.881 11.2178 175.122 10.9753 175.172L10.8824 175.182C10.59 175.182 10.353 174.908 10.353 174.591C10.353 174.301 10.547 174.06 10.7895 174.009L10.8824 174ZM13.5295 174C13.7894 174 14.0055 174.217 14.0504 174.487L14.0589 174.591C14.0589 174.881 13.8648 175.122 13.6224 175.172L13.5295 175.182C13.2371 175.182 13.0001 174.908 13.0001 174.591C13.0001 174.301 13.1941 174.06 13.4366 174.009L13.5295 174ZM16.1765 174C16.4364 174 16.6526 174.217 16.6974 174.487L16.7059 174.591C16.7059 174.881 16.5119 175.122 16.2694 175.172L16.1765 175.182C15.8841 175.182 15.6471 174.908 15.6471 174.591C15.6471 174.301 15.8412 174.06 16.0836 174.009L16.1765 174ZM9.29418 171.636H17.7648V171.247C17.7648 170.809 17.4472 170.455 17.0547 170.455H10.0042C9.61166 170.455 9.29418 170.809 9.29418 171.247V171.636Z" fill="#7B705A"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 174C22.353 173.347 22.9324 172.818 23.6471 172.818H34.0001C34.7148 172.818 35.2942 173.347 35.2942 174C35.2942 174.653 34.7148 175.182 34.0001 175.182H23.6471C22.9324 175.182 22.353 174.653 22.353 174Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 174C43.5293 173.347 44.0999 172.818 44.8038 172.818L88.1371 172.818C88.841 172.818 89.4117 173.347 89.4117 174C89.4117 174.653 88.841 175.182 88.1371 175.182L44.8038 175.182C44.0999 175.182 43.5293 174.653 43.5293 174Z" fill="#343741"/> +</g> +<defs> +<radialGradient id="paint0_diamond_169_44976" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(226 110) rotate(90) scale(264.688)"> +<stop stop-color="#EFF7FF"/> +<stop offset="1" stop-color="#DBEDFF"/> +</radialGradient> +</defs> +</svg> diff --git a/src/plugins/wizard/public/assets/hand_field.svg b/src/plugins/wizard/public/assets/hand_field.svg new file mode 100644 index 000000000000..753a3af354c0 --- /dev/null +++ b/src/plugins/wizard/public/assets/hand_field.svg @@ -0,0 +1,44 @@ +<svg width="206" height="68" viewBox="0 0 206 68" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g filter="url(#filter0_d_169_44976)"> +<rect x="2" y="2" width="176" height="40" rx="2" fill="#EFF4F9" stroke="#343741" stroke-width="4"/> +<path d="M23.6411 10L22.8172 14.7154H25.8498L25.1311 18.8172H22.0985L20.6261 27.5293C20.5443 28.1253 20.5822 28.5752 20.74 28.879C20.8978 29.1829 21.2922 29.3465 21.9232 29.3699C22.1686 29.3815 22.6711 29.3523 23.4308 29.2822L23.01 33.5593C22.0401 33.8749 21.0059 34.021 19.9074 33.9976C18.1194 33.9742 16.7813 33.4483 15.8932 32.4199C15.005 31.3916 14.6427 29.9951 14.8063 28.2304L16.3489 18.8172H14L14.7012 14.7154H17.0501L17.874 10H23.6411Z" fill="#4A7194"/> +<line x1="37" y1="22" x2="53" y2="22" stroke="#343741" stroke-width="4" stroke-linecap="round"/> +<line x1="69" y1="22" x2="162" y2="22" stroke="#343741" stroke-width="4" stroke-linecap="round"/> +</g> +<g filter="url(#filter1_d_169_44976)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M192.347 61.1429L189.898 56.2449L187.449 61.1429H177.653V58.6045C177.653 56.5994 169.816 51.0126 169.816 51.0126C168.734 50.2086 167.857 48.4761 167.857 47.1112V38.2857C167.857 35.4807 170.05 33.2064 172.755 33.2064V40.8254H175.204V28.974C175.204 27.8045 176.118 26.8572 177.245 26.8572C179.5 26.8572 181.327 28.7518 181.327 31.0896V30.2438C181.327 29.0743 182.24 28.127 183.368 28.127C185.622 28.127 187.449 30.0216 187.449 32.3594V31.5137C187.449 30.3442 188.362 29.3969 189.49 29.3062C191.745 29.3969 193.571 31.2915 193.571 33.6292V32.7835C193.571 31.614 194.485 30.6667 195.613 30.6667C197.867 30.6667 199.694 32.5613 199.694 34.8991V46.4596C199.694 47.8063 199.113 49.8316 198.407 50.9975C198.407 50.9975 194.796 56.5994 194.796 58.6045V61.1429H192.347ZM182.551 41.551H185V51.3469H182.551V41.551ZM187.449 41.551H189.898V51.3469H187.449V41.551ZM192.347 41.551H194.796V51.3469H192.347V41.551Z" fill="white"/> +</g> +<g filter="url(#filter2_d_169_44976)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M189.898 56.2449L192.347 61.1429H194.796V58.6044C194.796 57.6331 195.643 55.8178 196.517 54.2041C196.916 53.4667 197.321 52.7714 197.653 52.218C198.094 51.4824 198.407 50.9974 198.407 50.9974C199.113 49.8315 199.694 47.8062 199.694 46.4595V34.899C199.694 32.6255 197.966 30.7711 195.797 30.6709C195.736 30.6681 195.675 30.6667 195.613 30.6667C194.485 30.6667 193.571 31.614 193.571 32.7835V33.6292C193.571 33.3396 193.543 33.0569 193.49 32.7835C193.258 31.5969 192.548 30.587 191.578 29.9655C191.005 29.5985 190.342 29.3669 189.632 29.3142C189.585 29.3107 189.538 29.308 189.49 29.3061C188.362 29.3968 187.449 30.3441 187.449 31.5137V32.3594C187.449 32.0697 187.421 31.7869 187.368 31.5137C187.134 30.3206 186.417 29.3099 185.438 28.7111C184.88 28.3697 184.237 28.1622 183.549 28.1311C183.489 28.1284 183.429 28.127 183.368 28.127C182.24 28.127 181.327 29.0743 181.327 30.2438V31.0895C181.327 30.7999 181.298 30.5171 181.245 30.2438C181.012 29.0529 180.297 28.0436 179.321 27.4444C178.713 27.0713 178.003 26.8571 177.245 26.8571C176.118 26.8571 175.204 27.8044 175.204 28.974V40.8254H172.755V33.2063C172.618 33.2063 172.482 33.2122 172.347 33.2237C171.438 33.3016 170.598 33.6371 169.898 34.1597C168.662 35.0818 167.857 36.5864 167.857 38.2857V47.1112C167.857 48.4761 168.734 50.2086 169.816 51.0125C169.816 51.0125 177.653 56.5994 177.653 58.6044V61.1429H187.449L189.898 56.2449ZM187.449 41.551V51.3469H189.898V41.551H187.449ZM185 41.551H182.551V51.3469H185V41.551ZM192.347 41.551V51.3469H194.796V41.551H192.347ZM168.157 53.3389L168.134 53.3226L168.112 53.3057C167.123 52.5713 166.376 51.5446 165.879 50.5568C165.381 49.5683 165 48.3521 165 47.1112V38.2857C165 34.1356 168.167 30.5784 172.347 30.3598V28.974C172.347 26.3253 174.443 24 177.245 24C178.93 24 180.445 24.6015 181.627 25.5894C182.163 25.3836 182.748 25.2698 183.368 25.2698C185.037 25.2698 186.541 25.8607 187.718 26.833C188.201 26.6309 188.721 26.5017 189.261 26.4582L189.433 26.4444L189.605 26.4513C191.236 26.5169 192.72 27.1363 193.884 28.1243C194.417 27.9215 194.998 27.8095 195.613 27.8095C199.542 27.8095 202.551 31.0821 202.551 34.899V46.4595C202.551 47.484 202.341 48.6031 202.063 49.5784C201.784 50.5573 201.376 51.6108 200.851 52.477L200.83 52.5115M200.809 52.5451L200.802 52.5553L200.776 52.5959L200.671 52.7625C200.578 52.9094 200.445 53.1241 200.284 53.389C199.96 53.921 199.532 54.6442 199.108 55.4197C198.679 56.2043 198.28 56.9962 197.995 57.6751C197.777 58.1941 197.698 58.4841 197.669 58.5894C197.654 58.6466 197.653 58.6493 197.653 58.6044V64H190.581L189.898 62.6337L189.215 64H174.796V59.0728C174.695 58.933 174.54 58.7411 174.317 58.4975C173.708 57.8293 172.84 57.037 171.896 56.2435C170.967 55.4622 170.029 54.7334 169.318 54.1965C168.964 53.9292 168.67 53.7121 168.466 53.5629C168.364 53.4884 168.285 53.431 168.232 53.3929L168.174 53.3506L168.157 53.3389M174.933 59.2893C174.932 59.289 174.924 59.2751 174.912 59.2486C174.928 59.2764 174.934 59.2896 174.933 59.2893Z" fill="#343741"/> +</g> +<defs> +<filter id="filter0_d_169_44976" x="0" y="0" width="188" height="52" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dx="4" dy="4"/> +<feGaussianBlur stdDeviation="2"/> +<feComposite in2="hardAlpha" operator="out"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_169_44976"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_169_44976" result="shape"/> +</filter> +<filter id="filter1_d_169_44976" x="164.857" y="24.8572" width="37.8367" height="40.2857" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_169_44976"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_169_44976" result="shape"/> +</filter> +<filter id="filter2_d_169_44976" x="162" y="22" width="43.551" height="46" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_169_44976"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_169_44976" result="shape"/> +</filter> +</defs> +</svg> From 51411cb1cbea3a283085bfda084c1e1c1a7eb790 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Tue, 19 Jul 2022 17:24:44 -0700 Subject: [PATCH 17/47] [D&D] Fix: Topnav updates aggregation parameters (#1905) * fix(D&D): Fixes top nav query and timerange Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix(D&D): Handles topnav state update Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../public/application/components/top_nav.tsx | 6 ++-- .../application/components/workspace.tsx | 29 ++++++++++++++++--- .../visualizations/metric/to_expression.ts | 2 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx index 299425e5b1d8..b1427e01523c 100644 --- a/src/plugins/wizard/public/application/components/top_nav.tsx +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -58,10 +58,10 @@ export const TopNav = () => { appName={PLUGIN_ID} config={config} setMenuMountPoint={setHeaderActionMenu} - showSearchBar={true} - useDefaultBehaviors={true} - screenTitle="Test" indexPatterns={indexPattern ? [indexPattern] : []} + showSearchBar + showSaveQuery + useDefaultBehaviors /> </div> ); diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 4d30dbb76463..17565a1d0003 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -14,8 +14,9 @@ import { EuiPanel, EuiPopover, } from '@elastic/eui'; -import React, { FC, useState, useMemo, useEffect, Fragment } from 'react'; +import React, { FC, useState, useMemo, useEffect, useLayoutEffect } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { IExpressionLoaderParams } from '../../../../expressions/public'; import { WizardServices } from '../../types'; import { validateSchemaState } from '../utils/validate_schema_state'; import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; @@ -32,10 +33,16 @@ export const Workspace: FC = ({ children }) => { services: { expressions: { ReactExpressionRenderer }, notifications: { toasts }, + data, }, } = useOpenSearchDashboards<WizardServices>(); const { toExpression, ui } = useVisualizationType(); const [expression, setExpression] = useState<string>(); + const [searchContext, setSearchContext] = useState<IExpressionLoaderParams['searchContext']>({ + query: data.query.queryString.getQuery(), + filters: data.query.filterManager.getFilters(), + timeRange: data.query.timefilter.timefilter.getTime(), + }); const rootState = useTypedSelector((state) => state); useEffect(() => { @@ -57,6 +64,20 @@ export const Workspace: FC = ({ children }) => { loadExpression(); }, [rootState, toExpression, toasts, ui.containerConfig.data.schemas]); + useLayoutEffect(() => { + const subscription = data.query.state$.subscribe(({ state }) => { + setSearchContext({ + query: state.query, + timeRange: state.time, + filters: state.filters, + }); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [data.query.state$]); + return ( <section className="wizWorkspace"> <EuiFlexGroup className="wizCanvasControls"> @@ -66,13 +87,13 @@ export const Workspace: FC = ({ children }) => { </EuiFlexGroup> <EuiPanel className="wizCanvas"> {expression ? ( - <ReactExpressionRenderer expression={expression} /> + <ReactExpressionRenderer expression={expression} searchContext={searchContext} /> ) : ( <EuiFlexItem className="wizWorkspace__empty"> <EuiEmptyPrompt title={<h2>Drop some fields here to start</h2>} body={ - <Fragment> + <> <p>Drag a field directly to the canvas or axis to generate a visualization.</p> <span className="wizWorkspace__container"> <EuiIcon className="wizWorkspace__fieldSvg" type={fields_bg} size="original" /> @@ -82,7 +103,7 @@ export const Workspace: FC = ({ children }) => { size="original" /> </span> - </Fragment> + </> } /> </EuiFlexItem> diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts index 5b787ef5d264..7b8d7b802e50 100644 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.ts +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -171,5 +171,5 @@ export const toExpression = async ({ style: styleState, visualization }: MetricR const ast = buildExpression([opensearchaggs, metricVis]); - return ast.toString(); + return `opensearchDashboards | opensearch_dashboards_context | ${ast.toString()}`; }; From 10c4934128d5e4d621a34e47f1d090a9b685643a Mon Sep 17 00:00:00 2001 From: Manasvini B Suryanarayana <manasvis@amazon.com> Date: Tue, 19 Jul 2022 17:25:37 -0700 Subject: [PATCH 18/47] [D&D] Add a flag in the YAML config to enable and disable the D&D plugin (#1889) Resolves Issue - https://github.com/opensearch-project/OpenSearch-Dashboards/issues/1877 Signed-off-by: Manasvini B Suryanarayana <manasvis@amazon.com> --- config/opensearch_dashboards.yml | 4 ++++ src/plugins/wizard/config.ts | 12 ++++++++++++ src/plugins/wizard/public/plugin.ts | 3 ++- src/plugins/wizard/server/index.ts | 10 +++++++++- 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/plugins/wizard/config.ts diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 3ece0fd3b612..7757ba6761ad 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -184,3 +184,7 @@ # Set the value of this setting to false to suppress search usage telemetry # for reducing the load of OpenSearch cluster. # data.search.usageTelemetry.enabled: false + +# Set the value of this setting to true to start exploring wizard +# functionality in Visualization. +# wizard.enabled: true diff --git a/src/plugins/wizard/config.ts b/src/plugins/wizard/config.ts new file mode 100644 index 000000000000..b6be3f718eea --- /dev/null +++ b/src/plugins/wizard/config.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema, TypeOf } from '@osd/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf<typeof configSchema>; diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 7af37ccf61b6..8d9ebb406a0f 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -26,13 +26,14 @@ import { getPreloadedStore } from './application/utils/state_management'; import { setAggService, setIndexPatterns } from './plugin_services'; import { createSavedWizardLoader } from './saved_visualizations'; import { registerDefaultTypes } from './visualizations'; +import { ConfigSchema } from '../config'; export class WizardPlugin implements Plugin<WizardSetup, WizardStart, WizardPluginSetupDependencies, WizardPluginStartDependencies> { private typeService = new TypeService(); - constructor(public initializerContext: PluginInitializerContext) {} + constructor(public initializerContext: PluginInitializerContext<ConfigSchema>) {} public setup( core: CoreSetup<WizardPluginStartDependencies, WizardStart>, diff --git a/src/plugins/wizard/server/index.ts b/src/plugins/wizard/server/index.ts index e995ea17b4a7..811998b8a954 100644 --- a/src/plugins/wizard/server/index.ts +++ b/src/plugins/wizard/server/index.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { PluginInitializerContext } from '../../../core/server'; +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; +import { ConfigSchema, configSchema } from '../config'; import { WizardPlugin } from './plugin'; // This exports static code and TypeScript types, @@ -14,3 +15,10 @@ export function plugin(initializerContext: PluginInitializerContext) { } export { WizardPluginSetup, WizardPluginStart } from './types'; + +export const config: PluginConfigDescriptor<ConfigSchema> = { + exposeToBrowser: { + enabled: true, + }, + schema: configSchema, +}; From e93e6a5730310b0a98aa32d5030fe3d18c241b0f Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 20 Jul 2022 13:22:40 -0700 Subject: [PATCH 19/47] fix(Workspace): Fixes illustration and copy (#1916) Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../public/application/components/workspace.scss | 11 ++++------- .../public/application/components/workspace.tsx | 4 ++-- src/plugins/wizard/public/assets/fields_bg.svg | 2 +- src/plugins/wizard/public/assets/index_pattern.svg | 4 ---- 4 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 src/plugins/wizard/public/assets/index_pattern.svg diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss index 0a4465c0e692..f83a148ec44d 100644 --- a/src/plugins/wizard/public/application/components/workspace.scss +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -16,21 +16,18 @@ } &__handFieldSvg { - animation-name: dragAndDropAnimation; - animation-direction: alternate-reverse; - animation-duration: 1.5s; - animation-iteration-count: infinite; + animation: wizDragAnimation 2s ease-in-out infinite alternate; position: absolute; top: 43%; } } -@keyframes dragAndDropAnimation { +@keyframes wizDragAnimation { 0% { - transform: translate(65%, 0); + transform: none; } 100% { - transform: none; + transform: translate(65%, -30%); } } diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 17565a1d0003..9a910b51c320 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -91,10 +91,10 @@ export const Workspace: FC = ({ children }) => { ) : ( <EuiFlexItem className="wizWorkspace__empty"> <EuiEmptyPrompt - title={<h2>Drop some fields here to start</h2>} + title={<h2>Drop some fields to start</h2>} body={ <> - <p>Drag a field directly to the canvas or axis to generate a visualization.</p> + <p>Drag a field to the configuration panel to generate a visualization.</p> <span className="wizWorkspace__container"> <EuiIcon className="wizWorkspace__fieldSvg" type={fields_bg} size="original" /> <EuiIcon diff --git a/src/plugins/wizard/public/assets/fields_bg.svg b/src/plugins/wizard/public/assets/fields_bg.svg index b319b6937406..3c22912ec34b 100644 --- a/src/plugins/wizard/public/assets/fields_bg.svg +++ b/src/plugins/wizard/public/assets/fields_bg.svg @@ -1,5 +1,5 @@ <svg width="336" height="220" viewBox="0 0 336 220" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="118" y="2" width="216" height="216" rx="2" fill="url(#paint0_diamond_169_44976)" stroke="#006BB4" stroke-width="4" stroke-linecap="round" stroke-dasharray="80 80"/> +<rect x="118" y="2" width="216" height="216" rx="2" fill="url(#paint0_diamond_169_44976)" stroke="#006BB4" stroke-width="4" stroke-linecap="round" stroke-dasharray="108" stroke-dashoffset="54"/> <g opacity="0.5"> <path d="M0 99.6C0 98.1641 1.14799 97 2.5641 97H97.4359C98.852 97 100 98.1641 100 99.6V120.4C100 121.836 98.852 123 97.4359 123H2.5641C1.14799 123 0 121.836 0 120.4V99.6Z" fill="#EFF4F9"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 99.6H2.5641L2.5641 120.4H97.4359V99.6ZM2.5641 97C1.14799 97 0 98.1641 0 99.6V120.4C0 121.836 1.14799 123 2.5641 123H97.4359C98.852 123 100 121.836 100 120.4V99.6C100 98.1641 98.852 97 97.4359 97H2.5641Z" fill="#343741"/> diff --git a/src/plugins/wizard/public/assets/index_pattern.svg b/src/plugins/wizard/public/assets/index_pattern.svg deleted file mode 100644 index b1f140a38632..000000000000 --- a/src/plugins/wizard/public/assets/index_pattern.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 6.125H8.75C9.47487 6.125 10.0625 5.53737 10.0625 4.8125V1.3125C10.0625 0.587626 9.47487 0 8.75 0H5.25C4.52513 0 3.9375 0.587626 3.9375 1.3125V4.8125C3.9375 5.53737 4.52513 6.125 5.25 6.125ZM4.8125 1.3125C4.8125 1.07088 5.00838 0.875 5.25 0.875H8.75C8.99162 0.875 9.1875 1.07088 9.1875 1.3125V4.8125C9.1875 5.05412 8.99162 5.25 8.75 5.25H5.25C5.00838 5.25 4.8125 5.05412 4.8125 4.8125V1.3125ZM7.4375 7V7.875H12.6875V10.5H11.8125V8.75H7.4375V10.5H6.5625V8.75H2.1875V10.5H1.3125V7.875H6.5625V7H7.4375Z" fill="#343741"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.3125 1.75H5.6875V2.625H8.3125V1.75ZM1.75 14C1.02513 14 0.4375 13.4124 0.4375 12.6875C0.4375 11.9626 1.02513 11.375 1.75 11.375C2.47487 11.375 3.0625 11.9626 3.0625 12.6875C3.0625 13.4124 2.47487 14 1.75 14ZM1.75 12.25C1.50838 12.25 1.3125 12.4459 1.3125 12.6875C1.3125 12.9291 1.50838 13.125 1.75 13.125C1.99162 13.125 2.1875 12.9291 2.1875 12.6875C2.1875 12.4459 1.99162 12.25 1.75 12.25ZM7 14C6.27513 14 5.6875 13.4124 5.6875 12.6875C5.6875 11.9626 6.27513 11.375 7 11.375C7.72487 11.375 8.3125 11.9626 8.3125 12.6875C8.3125 13.4124 7.72487 14 7 14ZM7 12.25C6.75838 12.25 6.5625 12.4459 6.5625 12.6875C6.5625 12.9291 6.75838 13.125 7 13.125C7.24162 13.125 7.4375 12.9291 7.4375 12.6875C7.4375 12.4459 7.24162 12.25 7 12.25ZM12.25 14C11.5251 14 10.9375 13.4124 10.9375 12.6875C10.9375 11.9626 11.5251 11.375 12.25 11.375C12.9749 11.375 13.5625 11.9626 13.5625 12.6875C13.5625 13.4124 12.9749 14 12.25 14ZM12.25 12.25C12.0084 12.25 11.8125 12.4459 11.8125 12.6875C11.8125 12.9291 12.0084 13.125 12.25 13.125C12.4916 13.125 12.6875 12.9291 12.6875 12.6875C12.6875 12.4459 12.4916 12.25 12.25 12.25ZM5.6875 3.5H8.3125V4.375H5.6875V3.5Z" fill="#017D73"/> -</svg> From 83fe818949fc2f396b56a0bc3bcab12dbad124e6 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 20 Jul 2022 17:27:27 -0700 Subject: [PATCH 20/47] [D&D] Misc fixes (#1924) * fix: minor fixes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: nit syntax fixes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: simplify useSavedWizardVis Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../wizard/type_selection/type_selection.tsx | 2 +- .../public/application/components/top_nav.tsx | 10 ++--- .../public/application/utils/use/index.ts | 1 + .../utils/use/use_index_pattern.tsx | 25 ++++++------ .../utils/use/use_saved_wizard_vis.ts | 7 ++-- src/plugins/wizard/public/plugin.test.ts | 3 +- src/plugins/wizard/public/plugin.ts | 5 +-- .../services/type_service/utils/index.ts | 6 --- .../type_service/utils/merge_array.test.ts | 24 ------------ .../type_service/utils/merge_arrays.ts | 39 ------------------- .../metric/to_expression.test.ts | 6 +++ .../visualizations/metric/to_expression.ts | 2 +- src/plugins/wizard/server/routes/index.ts | 27 ++++++------- 13 files changed, 46 insertions(+), 111 deletions(-) delete mode 100644 src/plugins/wizard/public/services/type_service/utils/index.ts delete mode 100644 src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts delete mode 100644 src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts create mode 100644 src/plugins/wizard/public/visualizations/metric/to_expression.test.ts diff --git a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx index 797bc2e8cf93..27ea53f0f16f 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx @@ -222,7 +222,7 @@ class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionSta private renderVisType = (visType: VisTypeListEntry) => { let stage = {}; let highlightMsg; - if (!isVisTypeAlias(visType.type) && visType.type.stage === 'experimental') { + if (visType.type.stage === 'experimental') { stage = { betaBadgeLabel: i18n.translate('visualizations.newVisWizard.experimentalTitle', { defaultMessage: 'Experimental', diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx index b1427e01523c..7a4bfa1010f4 100644 --- a/src/plugins/wizard/public/application/components/top_nav.tsx +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -11,9 +11,8 @@ import { getTopNavConfig } from '../utils/get_top_nav_config'; import { WizardServices } from '../../types'; import './top_nav.scss'; -import { useIndexPattern } from '../utils/use'; +import { useIndexPattern, useSavedWizardVis } from '../utils/use'; import { useTypedSelector } from '../utils/state_management'; -import { useSavedWizardVis } from '../utils/use/use_saved_wizard_vis'; export const TopNav = () => { // id will only be set for the edit route @@ -30,12 +29,11 @@ export const TopNav = () => { (state) => !!state.visualization.activeVisualization?.draftAgg ); - const savedWizardVis = useSavedWizardVis(services, visualizationIdFromUrl); + const savedWizardVis = useSavedWizardVis(visualizationIdFromUrl); const config = useMemo(() => { - if (savedWizardVis === undefined) { - return; - } + if (savedWizardVis === undefined) return; + const { visualization: visualizationState, style: styleState } = rootState; return getTopNavConfig( diff --git a/src/plugins/wizard/public/application/utils/use/index.ts b/src/plugins/wizard/public/application/utils/use/index.ts index 542ae073cde5..c9203242e63c 100644 --- a/src/plugins/wizard/public/application/utils/use/index.ts +++ b/src/plugins/wizard/public/application/utils/use/index.ts @@ -5,3 +5,4 @@ export { useVisualizationType } from './use_visualization_type'; export { useIndexPattern, useIndexPatterns } from './use_index_pattern'; +export { useSavedWizardVis } from './use_saved_wizard_vis'; diff --git a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx index d5f255fcb10c..6ce448b9f055 100644 --- a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx +++ b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx @@ -2,7 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { IndexPattern } from '../../../../../data/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../../types'; @@ -17,14 +17,14 @@ export const useIndexPattern = (): IndexPattern | undefined => { }, } = useOpenSearchDashboards<WizardServices>(); - const handleIndexUpdate = useCallback(async () => { - const currentIndex = await indexPatterns.get(indexId); - setIndexPattern(currentIndex); - }, [indexId, indexPatterns]); - useEffect(() => { + const handleIndexUpdate = async () => { + const currentIndex = await indexPatterns.get(indexId); + setIndexPattern(currentIndex); + }; + handleIndexUpdate(); - }, [handleIndexUpdate]); + }, [indexId, indexPatterns]); return indexPattern; }; @@ -38,7 +38,7 @@ export const useIndexPatterns = () => { services: { data }, } = useOpenSearchDashboards<WizardServices>(); - let foundSelected; + let foundSelected: IndexPattern; if (!loading && !error) { foundSelected = indexPatterns.filter((p) => p.id === indexId)[0]; if (foundSelected === undefined) { @@ -51,19 +51,18 @@ export const useIndexPatterns = () => { useEffect(() => { const handleUpdate = async () => { try { - const ids = await data.indexPatterns.getIds(indexId); + const ids = await data.indexPatterns.getIds(true); const patterns = await Promise.all(ids.map((id) => data.indexPatterns.get(id))); setIndexPatterns(patterns); } catch (e) { - setError(e); + setError(e as Error); } finally { setLoading(false); } }; + handleUpdate(); - // we want to run this hook exactly once, which you do by an empty dep array - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [data.indexPatterns]); return { indexPatterns, diff --git a/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts index 30b72199f10d..108beaefdb14 100644 --- a/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts +++ b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts @@ -16,11 +16,10 @@ import { MetricOptionsDefaults } from '../../../visualizations/metric/metric_viz import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; import { getSavedWizardVis } from '../get_saved_wizard_vis'; import { useTypedDispatch, setStyleState, setVisualizationState } from '../state_management'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -export const useSavedWizardVis = ( - services: WizardServices, - visualizationIdFromUrl: string | undefined -) => { +export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => { + const { services } = useOpenSearchDashboards<WizardServices>(); const [savedVisState, setSavedVisState] = useState<SavedObject | undefined>(undefined); const dispatch = useTypedDispatch(); diff --git a/src/plugins/wizard/public/plugin.test.ts b/src/plugins/wizard/public/plugin.test.ts index 74936288a76f..6dafa46c86ff 100644 --- a/src/plugins/wizard/public/plugin.test.ts +++ b/src/plugins/wizard/public/plugin.test.ts @@ -33,11 +33,12 @@ describe('WizardPlugin', () => { const setup = plugin.setup(coreSetup, setupDeps); expect(setup).toHaveProperty('createVisualizationType'); expect(setupDeps.visualizations.registerAlias).toHaveBeenCalledWith( - // TODO: Update this once the properties are final expect.objectContaining({ name: PLUGIN_ID, title: PLUGIN_NAME, aliasPath: '#/', + aliasApp: PLUGIN_ID, + stage: 'experimental', }) ); }); diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 8d9ebb406a0f..7a3fe5aa204e 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -94,11 +94,10 @@ export class WizardPlugin name: PLUGIN_ID, title: PLUGIN_NAME, description: i18n.translate('wizard.visPicker.description', { - defaultMessage: 'TODO...', + defaultMessage: 'Create visualizations using the new Drag & Drop experience', }), - // TODO: Replace with actual icon once available icon: wizardIcon, - stage: 'beta', + stage: 'experimental', aliasApp: PLUGIN_ID, aliasPath: '#/', }); diff --git a/src/plugins/wizard/public/services/type_service/utils/index.ts b/src/plugins/wizard/public/services/type_service/utils/index.ts deleted file mode 100644 index 4d0644b2d93e..000000000000 --- a/src/plugins/wizard/public/services/type_service/utils/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export * from './merge_arrays'; diff --git a/src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts b/src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts deleted file mode 100644 index 01a95f240d5f..000000000000 --- a/src/plugins/wizard/public/services/type_service/utils/merge_array.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { mergeArrays } from './merge_arrays'; - -describe('mergeArrays', () => { - const arrayA = ['a', 'b', 'c'].map((x) => ({ id: x, value: `${x} in arrayA` })); - const arrayB = ['a', 'c', 'd'].map((x) => ({ id: x, value: `${x} in arrayB` })); - test('should merge two object arrays based on id in order without duplicates', () => { - const mergedArrays = mergeArrays(arrayA, arrayB, 'id'); - expect(mergedArrays.map((x) => x.id)).toEqual(['a', 'b', 'c', 'd']); - expect(mergedArrays[0].value).toEqual('a in arrayB'); - }); - - test('should throw an error if key is not a string or number', () => { - const arr = [{ id: {} }]; - - expect(() => mergeArrays(arrayA, arr, 'id')).toThrowErrorMatchingInlineSnapshot( - `"Can only merge arrays with keys of type number or string"` - ); - }); -}); diff --git a/src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts b/src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts deleted file mode 100644 index 3b5f9b0adfb8..000000000000 --- a/src/plugins/wizard/public/services/type_service/utils/merge_arrays.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { uniq } from 'lodash'; - -export function mergeArrays<T extends object, K extends keyof T>(a: T[], b: T[], key: K): T[] { - const dict: Record<PropertyKey, T> = {}; - - const rawCombinedArrays = [...a, ...b]; - - // Create an ordered list of unique the array keys. Removing duplicates while keeping the order - const combinedOrder = uniq(rawCombinedArrays.map((entry) => entry[key])); - - // Create a map of all unique ID's and their values. - // If there is more than one entry with the same ID, the last entry is kept - rawCombinedArrays.forEach((entry) => { - if (!entry.hasOwnProperty(key)) { - throw new Error('Key not present in an object in one the arrays to merge'); - } - - const id = entry[key]; - - if (typeof id !== 'string' && typeof id !== 'number') { - throw new Error('Can only merge arrays with keys of type number or string'); - } - - dict[id] = entry; - }); - - // Return the combined array in order with unique keys keeping only the last entry for each unique id - return combinedOrder.map((entryId) => { - if (typeof entryId !== 'string' && typeof entryId !== 'number') { - throw new Error('Can only merge arrays with keys of type number or string'); - } - return dict[entryId]; - }); -} diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts new file mode 100644 index 000000000000..9fd364ad256c --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// TODO: Cleanup the TODOs in './to_expression.ts' before writing tests for this function diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts index 7b8d7b802e50..ce930d9b8e40 100644 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.ts +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -87,7 +87,7 @@ const getVisSchemas = (aggConfigs: AggConfigs): any => { return schemas; }; -interface MetricRootState extends RootState { +export interface MetricRootState extends RootState { style: MetricOptionsDefaults; } diff --git a/src/plugins/wizard/server/routes/index.ts b/src/plugins/wizard/server/routes/index.ts index f6268695e838..90b698ccb992 100644 --- a/src/plugins/wizard/server/routes/index.ts +++ b/src/plugins/wizard/server/routes/index.ts @@ -6,17 +6,18 @@ import { IRouter } from '../../../../core/server'; export function defineRoutes(router: IRouter) { - router.get( - { - path: '/api/wizard/example', - validate: false, - }, - async (context, request, response) => { - return response.ok({ - body: { - time: new Date().toISOString(), - }, - }); - } - ); + // Add server siude routes if needed like the example below + // router.get( + // { + // path: '/api/wizard/example', + // validate: false, + // }, + // async (context, request, response) => { + // return response.ok({ + // body: { + // time: new Date().toISOString(), + // }, + // }); + // } + // ); } From 9b3c7efdd9426a5f68706c6ef44e3f7ef1570ad3 Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Thu, 21 Jul 2022 12:12:22 -0700 Subject: [PATCH 21/47] [D&D] Enable basic embeddable panels (#1911) - add embeddable, embeddable component, embeddable factory - update `toExpression` to allow passing services - register embeddable factory in plugin setup fixes #1908 Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../saved_objects_management/README.md | 2 +- src/plugins/wizard/public/embeddable/index.ts | 7 + .../public/embeddable/wizard_component.tsx | 133 +++++++++++++++ .../public/embeddable/wizard_embeddable.tsx | 160 ++++++++++++++++++ .../embeddable/wizard_embeddable_factory.tsx | 100 +++++++++++ src/plugins/wizard/public/plugin.ts | 21 ++- .../type_service/visualization_type.tsx | 6 +- src/plugins/wizard/public/types.ts | 3 +- .../visualizations/metric/to_expression.ts | 11 +- 9 files changed, 436 insertions(+), 7 deletions(-) create mode 100644 src/plugins/wizard/public/embeddable/index.ts create mode 100644 src/plugins/wizard/public/embeddable/wizard_component.tsx create mode 100644 src/plugins/wizard/public/embeddable/wizard_embeddable.tsx create mode 100644 src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md index 748ce5b89922..3e2a67dd0b42 100644 --- a/src/plugins/saved_objects_management/README.md +++ b/src/plugins/saved_objects_management/README.md @@ -34,7 +34,7 @@ You'll notice that when clicking on the "Inspect" button from the saved objects ### Registering -Ideally, we'd allow plugins to self-register their `savedObjectLoader` and (declare a dependency on this plugin). However, as currently implemented, any plugins that want this plugin to handle their inpect routes need to be added as optional dependencies and registered here. +Ideally, we'd allow plugins to self-register their `savedObjectLoader` and (declare a dependency on this plugin). However, as currently implemented, any plugins that want this plugin to handle their inspect routes need to be added as optional dependencies and registered here. 1. Add your plugin to the `optionalPlugins` array in `./opensearch_dashboards.json` 2. Update the `StartDependencies` interface of this plugin to include the public plugin start type diff --git a/src/plugins/wizard/public/embeddable/index.ts b/src/plugins/wizard/public/embeddable/index.ts new file mode 100644 index 000000000000..d0137757e0ab --- /dev/null +++ b/src/plugins/wizard/public/embeddable/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './wizard_embeddable'; +export * from './wizard_embeddable_factory'; diff --git a/src/plugins/wizard/public/embeddable/wizard_component.tsx b/src/plugins/wizard/public/embeddable/wizard_component.tsx new file mode 100644 index 000000000000..79a768c754c3 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_component.tsx @@ -0,0 +1,133 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiText, + EuiAvatar, + EuiFlexGrid, + EuiCodeBlock, +} from '@elastic/eui'; + +import { withEmbeddableSubscription } from '../../../embeddable/public'; +import { WizardEmbeddable, WizardInput, WizardOutput } from './wizard_embeddable'; +import { validateSchemaState } from '../application/utils/validate_schema_state'; + +interface Props { + embeddable: WizardEmbeddable; + input: WizardInput; + output: WizardOutput; +} + +function wrapSearchTerms(task?: string, search?: string) { + if (!search) return task; + if (!task) return task; + const parts = task.split(new RegExp(`(${search})`, 'g')); + return parts.map((part, i) => + part === search ? ( + <span key={i} style={{ backgroundColor: 'yellow' }}> + {part} + </span> + ) : ( + part + ) + ); +} + +function WizardEmbeddableComponentInner({ + embeddable, + input: { search }, + output: { savedAttributes }, +}: Props) { + const { ReactExpressionRenderer, toasts, types, indexPatterns, aggs } = embeddable; + const [expression, setExpression] = useState<string>(); + const { title, description, visualizationState, styleState } = savedAttributes || {}; + + useEffect(() => { + const { visualizationState: visualization, styleState: style } = savedAttributes || {}; + if (savedAttributes === undefined || visualization === undefined || style === undefined) { + return; + } + + const rootState = { + visualization: JSON.parse(visualization), + style: JSON.parse(style), + }; + + const visualizationType = types.get(rootState.visualization?.activeVisualization?.name ?? ''); + if (!visualizationType) { + throw new Error(`Invalid visualization type ${visualizationType}`); + } + const { toExpression, ui } = visualizationType; + + async function loadExpression() { + const schemas = ui.containerConfig.data.schemas; + const [valid, errorMsg] = validateSchemaState(schemas, rootState); + + if (!valid) { + if (errorMsg) { + toasts.addWarning(errorMsg); + } + setExpression(undefined); + return; + } + const exp = await toExpression(rootState, indexPatterns, aggs); + setExpression(exp); + } + + if (savedAttributes !== undefined) { + loadExpression(); + } + }, [aggs, indexPatterns, savedAttributes, toasts, types]); + + // TODO: add correct loading and error states, remove debugging mode + return ( + <> + {expression ? ( + <EuiFlexItem> + <ReactExpressionRenderer expression={expression} /> + </EuiFlexItem> + ) : ( + <EuiFlexGroup> + <EuiFlexItem grow={false}> + <EuiAvatar name={title || description || ''} size="l" /> + </EuiFlexItem> + <EuiFlexItem> + <EuiFlexGrid columns={1}> + <EuiFlexItem> + <EuiText data-test-subj="wizardEmbeddableTitle"> + <h3>{wrapSearchTerms(title || '', search)}</h3> + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <EuiText data-test-subj="wizardEmbeddableDescription"> + {wrapSearchTerms(description, search)} + </EuiText> + </EuiFlexItem> + <EuiFlexItem> + <EuiCodeBlock data-test-subj="wizardEmbeddableDescription"> + {wrapSearchTerms(visualizationState, search)} + </EuiCodeBlock> + </EuiFlexItem> + <EuiFlexItem> + <EuiCodeBlock data-test-subj="wizardEmbeddableDescription"> + {wrapSearchTerms(styleState, search)} + </EuiCodeBlock> + </EuiFlexItem> + </EuiFlexGrid> + </EuiFlexItem> + </EuiFlexGroup> + )} + </> + ); +} + +export const WizardEmbeddableComponent = withEmbeddableSubscription< + WizardInput, + WizardOutput, + WizardEmbeddable +>(WizardEmbeddableComponentInner); diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx new file mode 100644 index 000000000000..2e4a137d368c --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx @@ -0,0 +1,160 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Subscription } from 'rxjs'; + +import { WizardSavedObjectAttributes } from '../../common'; +import { + Embeddable, + EmbeddableOutput, + IContainer, + SavedObjectEmbeddableInput, +} from '../../../embeddable/public'; +import { IToasts, SavedObjectsClientContract } from '../../../../core/public'; +import { WizardEmbeddableComponent } from './wizard_component'; +import { ReactExpressionRendererType } from '../../../expressions/public'; +import { TypeServiceStart } from '../services/type_service'; +import { DataPublicPluginStart } from '../../../data/public'; + +export const WIZARD_EMBEDDABLE = 'WIZARD_EMBEDDABLE'; + +// TODO: remove search, hasMatch or update as appropriate +export interface WizardInput extends SavedObjectEmbeddableInput { + /** + * Optional search string which will be used to highlight search terms as + * well as calculate `output.hasMatch`. + */ + search?: string; +} + +export interface WizardOutput extends EmbeddableOutput { + /** + * Should be true if input.search is defined and the task or title contain + * search as a substring. + */ + hasMatch: boolean; + /** + * Will contain the saved object attributes of the Wizard Saved Object that matches + * `input.savedObjectId`. If the id is invalid, this may be undefined. + */ + savedAttributes?: WizardSavedObjectAttributes; +} + +/** + * Returns whether any attributes contain the search string. If search is empty, true is returned. If + * there are no savedAttributes, false is returned. + * @param search - the search string + * @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId` + */ +function getHasMatch(search?: string, savedAttributes?: WizardSavedObjectAttributes): boolean { + if (!search) return true; + if (!savedAttributes) return false; + return Boolean( + (savedAttributes.description && savedAttributes.description.match(search)) || + (savedAttributes.title && savedAttributes.title.match(search)) + ); +} + +export class WizardEmbeddable extends Embeddable<WizardInput, WizardOutput> { + public readonly type = WIZARD_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + private savedObjectsClient: SavedObjectsClientContract; + public ReactExpressionRenderer: ReactExpressionRendererType; + public toasts: IToasts; + public types: TypeServiceStart; + public indexPatterns: DataPublicPluginStart['indexPatterns']; + public aggs: DataPublicPluginStart['search']['aggs']; + private savedObjectId?: string; + + constructor( + initialInput: WizardInput, + { + parent, + savedObjectsClient, + data, + ReactExpressionRenderer, + toasts, + types, + }: { + parent?: IContainer; + data: DataPublicPluginStart; + savedObjectsClient: SavedObjectsClientContract; + ReactExpressionRenderer: ReactExpressionRendererType; + toasts: IToasts; + types: TypeServiceStart; + } + ) { + // TODO: can default title come from saved object? + super(initialInput, { defaultTitle: 'wizard', hasMatch: false }, parent); + this.savedObjectsClient = savedObjectsClient; + this.ReactExpressionRenderer = ReactExpressionRenderer; + this.toasts = toasts; + this.types = types; + this.indexPatterns = data.indexPatterns; + this.aggs = data.search.aggs; + + this.subscription = this.getInput$().subscribe(async () => { + // There is a little more work today for this embeddable because it has + // more output it needs to update in response to input state changes. + let savedAttributes: WizardSavedObjectAttributes | undefined; + + // Since this is an expensive task, we save a local copy of the previous + // savedObjectId locally and only retrieve the new saved object if the id + // actually changed. + if (this.savedObjectId !== this.input.savedObjectId) { + this.savedObjectId = this.input.savedObjectId; + const wizardSavedObject = await this.savedObjectsClient.get<WizardSavedObjectAttributes>( + 'wizard', + this.input.savedObjectId + ); + savedAttributes = wizardSavedObject?.attributes; + } + + // The search string might have changed as well so we need to make sure we recalculate + // hasMatch. + this.updateOutput({ + hasMatch: getHasMatch(this.input.search, savedAttributes), + savedAttributes, + title: savedAttributes?.title, + }); + }); + } + + public render(node: HTMLElement) { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(<WizardEmbeddableComponent embeddable={this} />, node); + } + + /** + * Lets re-sync our saved object to make sure it's up to date! + */ + public async reload() { + this.savedObjectId = this.input.savedObjectId; + const wizardSavedObject = await this.savedObjectsClient.get<WizardSavedObjectAttributes>( + 'wizard', + this.input.savedObjectId + ); + const savedAttributes = wizardSavedObject?.attributes; + this.updateOutput({ + hasMatch: getHasMatch(this.input.search, savedAttributes), + savedAttributes, + title: wizardSavedObject?.attributes?.title, + }); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx new file mode 100644 index 000000000000..ce1780d0066e --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx @@ -0,0 +1,100 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { NotificationsStart, SavedObjectsClientContract } from '../../../../core/public'; +import { DataPublicPluginStart } from '../../../data/public'; +import { + EmbeddableFactory, + EmbeddableFactoryDefinition, + EmbeddableStart, + ErrorEmbeddable, + IContainer, +} from '../../../embeddable/public'; +import { ExpressionsStart } from '../../../expressions/public'; +import { WizardSavedObjectAttributes } from '../../common'; +import { TypeServiceStart } from '../services/type_service'; +import { + WizardEmbeddable, + WizardInput, + WizardOutput, + WIZARD_EMBEDDABLE, +} from './wizard_embeddable'; + +interface StartServices { + data: DataPublicPluginStart; + expressions: ExpressionsStart; + getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + savedObjectsClient: SavedObjectsClientContract; + notifications: NotificationsStart; + types: TypeServiceStart; +} + +// TODO: use or remove? +export type WizardEmbeddableFactory = EmbeddableFactory< + WizardInput, + WizardOutput, + WizardEmbeddable, + WizardSavedObjectAttributes +>; + +export class WizardEmbeddableFactoryDefinition + implements + EmbeddableFactoryDefinition< + WizardInput, + WizardOutput, + WizardEmbeddable, + WizardSavedObjectAttributes + > { + public readonly type = WIZARD_EMBEDDABLE; + public readonly savedObjectMetaData = { + // TODO: Update to include most vis functionality + name: 'Wizard', + includeFields: ['visualizationState'], + type: 'wizard', + getIconForSavedObject: () => 'pencil', + }; + + constructor(private getStartServices: () => Promise<StartServices>) {} + + public async isEditable() { + // TODO: Add proper access controls + // return getCapabilities().visualize.save as boolean; + return true; + } + + public createFromSavedObject = ( + savedObjectId: string, + input: Partial<WizardInput> & { id: string }, + parent?: IContainer + ): Promise<WizardEmbeddable | ErrorEmbeddable> => { + return this.create({ ...input, savedObjectId }, parent); + }; + + public async create(input: WizardInput, parent?: IContainer) { + // TODO: Use savedWizardLoader here instead + const { + data, + expressions: { ReactExpressionRenderer }, + notifications: { toasts }, + savedObjectsClient, + types, + } = await this.getStartServices(); + return new WizardEmbeddable(input, { + parent, + data, + savedObjectsClient, + ReactExpressionRenderer, + toasts, + types, + }); + } + + public getDisplayName() { + return i18n.translate('wizard.displayName', { + defaultMessage: 'Wizard', + }); + } +} diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 7a3fe5aa204e..4f4e7016044d 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -19,6 +19,7 @@ import { WizardSetup, WizardStart, } from './types'; +import { WizardEmbeddableFactoryDefinition, WIZARD_EMBEDDABLE } from './embeddable'; import wizardIcon from './assets/wizard_icon.svg'; import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { TypeService } from './services/type_service'; @@ -37,7 +38,7 @@ export class WizardPlugin public setup( core: CoreSetup<WizardPluginStartDependencies, WizardStart>, - { visualizations }: WizardPluginSetupDependencies + { embeddable, visualizations }: WizardPluginSetupDependencies ) { const typeService = this.typeService; registerDefaultTypes(typeService.setup()); @@ -89,6 +90,24 @@ export class WizardPlugin }, }); + // Register embeddable + // TODO: investigate simplification via getter a la visualizations: + // const start = createStartServicesGetter(core.getStartServices)); + // const embeddableFactory = new WizardEmbeddableFactoryDefinition({ start }); + const embeddableFactory = new WizardEmbeddableFactoryDefinition(async () => { + const [coreStart, pluginsStart, _wizardStart] = await core.getStartServices(); + // TODO: refactor to pass minimal service methods? + return { + savedObjectsClient: coreStart.savedObjects.client, + data: pluginsStart.data, + getEmbeddableFactory: pluginsStart.embeddable.getEmbeddableFactory, + expressions: pluginsStart.expressions, + notifications: coreStart.notifications, + types: this.typeService.start(), + }; + }); + embeddable.registerEmbeddableFactory(WIZARD_EMBEDDABLE, embeddableFactory); + // Register the plugin as an alias to create visualization visualizations.registerAlias({ name: PLUGIN_ID, diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.tsx index 90f30d8f8a95..62fddfff85bb 100644 --- a/src/plugins/wizard/public/services/type_service/visualization_type.tsx +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -15,7 +15,11 @@ export class VisualizationType implements IVisualizationType { public readonly icon: IconType; public readonly stage: 'beta' | 'production'; public readonly ui: IVisualizationType['ui']; - public readonly toExpression: (state: RootState) => Promise<string | undefined>; + public readonly toExpression: ( + state: RootState, + indexPatterns?, + aggs? + ) => Promise<string | undefined>; constructor(options: VisualizationTypeOptions) { this.name = options.name; diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts index 55b3f1b41343..f8371b832bdc 100644 --- a/src/plugins/wizard/public/types.ts +++ b/src/plugins/wizard/public/types.ts @@ -5,7 +5,7 @@ import { History } from 'history'; import { SavedObject, SavedObjectsStart } from '../../saved_objects/public'; -import { EmbeddableSetup } from '../../embeddable/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; import { DashboardStart } from '../../dashboard/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { ExpressionsStart } from '../../expressions/public'; @@ -25,6 +25,7 @@ export interface WizardPluginSetupDependencies { visualizations: VisualizationsSetup; } export interface WizardPluginStartDependencies { + embeddable: EmbeddableStart; navigation: NavigationPublicPluginStart; data: DataPublicPluginStart; savedObjects: SavedObjectsStart; diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts index ce930d9b8e40..0a459c23a7a1 100644 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.ts +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -91,13 +91,18 @@ export interface MetricRootState extends RootState { style: MetricOptionsDefaults; } -export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { +export const toExpression = async ( + { style: styleState, visualization }: MetricRootState, + indexPatterns, + aggs +) => { const { activeVisualization, indexPattern: indexId = '' } = visualization; const { aggConfigParams } = activeVisualization || {}; - const indexPatternsService = getIndexPatterns(); + const indexPatternsService = indexPatterns ?? getIndexPatterns(); const indexPattern = await indexPatternsService.get(indexId); - const aggConfigs = getAggService().createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + const aggService = aggs ?? getAggService(); + const aggConfigs = aggService.createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); // soon this becomes: const opensearchaggs = vis.data.aggs!.toExpressionAst(); const opensearchaggs = buildExpressionFunction<OpenSearchaggsExpressionFunctionDefinition>( From ee0da75dca98a0fc64ed6b308b529d6c1d2a5796 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Thu, 21 Jul 2022 12:29:33 -0700 Subject: [PATCH 22/47] [D&D] Fix scss lint and available fields (#1927) * fix(Lint): fixes scss linting issues Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: Filter field types correctly Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: minor Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../wizard/public/application/_util.scss | 14 ++--- .../wizard/public/application/_variables.scss | 4 +- .../wizard/public/application/app.scss | 10 ++-- .../components/data_tab/field_selector.tsx | 53 ++++++++----------- .../data_tab/utils/get_available_fields.ts | 28 ++++++++++ .../components/data_tab/utils/index.ts | 6 +++ .../application/components/side_nav.scss | 4 +- .../application/components/top_nav.scss | 6 +-- 8 files changed, 75 insertions(+), 50 deletions(-) create mode 100644 src/plugins/wizard/public/application/components/data_tab/utils/get_available_fields.ts create mode 100644 src/plugins/wizard/public/application/components/data_tab/utils/index.ts diff --git a/src/plugins/wizard/public/application/_util.scss b/src/plugins/wizard/public/application/_util.scss index 9a444c1fe091..e1ef0bc048a7 100644 --- a/src/plugins/wizard/public/application/_util.scss +++ b/src/plugins/wizard/public/application/_util.scss @@ -1,8 +1,8 @@ -@mixin scrollNavParent ($template-row: none) { - display: grid; - min-height: 0; +@mixin scrollNavParent($template-row: none) { + display: grid; + min-height: 0; - @if $template-row != 'none' { - grid-template-rows: $template-row; - } -} \ No newline at end of file + @if $template-row != "none" { + grid-template-rows: $template-row; + } +} diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss index 6ffa04d0eb4a..789aa00373e1 100644 --- a/src/plugins/wizard/public/application/_variables.scss +++ b/src/plugins/wizard/public/application/_variables.scss @@ -1,5 +1,5 @@ -@import '@elastic/eui/src/global_styling/variables/header'; -@import '@elastic/eui/src/global_styling/variables/form'; +@import "@elastic/eui/src/global_styling/variables/header"; +@import "@elastic/eui/src/global_styling/variables/form"; $osdHeaderOffset: $euiHeaderHeightCompensation * 2; $wizSideNavWidth: 470px; diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss index 3e2a28aca700..b02bc79395d0 100644 --- a/src/plugins/wizard/public/application/app.scss +++ b/src/plugins/wizard/public/application/app.scss @@ -3,11 +3,9 @@ .wizLayout { padding: 0; display: grid; - grid-template-rows: min-content 1fr; - grid-template-columns: $wizSideNavWidth 1fr; - grid-template-areas: + grid-template: min-content 1fr / #{$wizSideNavWidth} 1fr; + grid-template-areas: "topNav topNav" - "sideNav workspace" - ; - height: calc(100vh - #{$osdHeaderOffset}); + "sideNav workspace"; + height: calc(100vh - #{$osdHeaderOffset}); } diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx index 2361e8d1d073..353f642afcf6 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { EuiFlexItem, EuiAccordion, EuiNotificationBadge, EuiTitle } from '@elastic/eui'; import { FieldSearch } from './field_search'; @@ -17,6 +17,7 @@ import { FieldSelectorField } from './field_selector_field'; import './field_selector.scss'; import { useTypedSelector } from '../../utils/state_management'; import { useIndexPattern } from '../../utils/use'; +import { getAvailableFields } from './utils'; interface IFieldCategories { categorical: IndexPatternField[]; @@ -31,45 +32,37 @@ const META_FIELDS: string[] = [ OPENSEARCH_FIELD_TYPES._TYPE, ]; -const ALLOWED_FIELDS: string[] = [OSD_FIELD_TYPES.STRING, OSD_FIELD_TYPES.NUMBER]; - export const FieldSelector = () => { const indexPattern = useIndexPattern(); const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); const [filteredFields, setFilteredFields] = useState<IndexPatternField[]>([]); - // TODO: Temporary validate function - // Need to identify how to get fieldCounts to use the standard filter and group functions - const isVisualizable = useCallback((field: IndexPatternField): boolean => { - const isAggregatable = field.aggregatable === true; - const isNotScripted = !field.scripted; - const isAllowed = ALLOWED_FIELDS.includes(field.type); - - return isAggregatable && isNotScripted && isAllowed; - }, []); - useEffect(() => { const indexFields = indexPattern?.fields ?? []; - const filteredSubset = indexFields - .filter(isVisualizable) - .filter((field) => field.displayName.includes(fieldSearchValue)); + const filteredSubset = getAvailableFields(indexFields).filter((field) => + field.displayName.includes(fieldSearchValue) + ); setFilteredFields(filteredSubset); return; - }, [fieldSearchValue, indexPattern?.fields, isVisualizable]); - - const fields = filteredFields?.reduce<IFieldCategories>( - (fieldGroups, currentField) => { - const category = getFieldCategory(currentField); - fieldGroups[category].push(currentField); - - return fieldGroups; - }, - { - categorical: [], - numerical: [], - meta: [], - } + }, [fieldSearchValue, indexPattern?.fields]); + + const fields = useMemo( + () => + filteredFields?.reduce<IFieldCategories>( + (fieldGroups, currentField) => { + const category = getFieldCategory(currentField); + fieldGroups[category].push(currentField); + + return fieldGroups; + }, + { + categorical: [], + numerical: [], + meta: [], + } + ), + [filteredFields] ); return ( diff --git a/src/plugins/wizard/public/application/components/data_tab/utils/get_available_fields.ts b/src/plugins/wizard/public/application/components/data_tab/utils/get_available_fields.ts new file mode 100644 index 000000000000..d938e92b6978 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/utils/get_available_fields.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + FieldTypes, + IndexPatternField, + isNestedField, + propFilter, +} from '../../../../../../data/common'; + +const filterByType = propFilter('type'); + +export const getAvailableFields = ( + fields: IndexPatternField[], + filterFieldTypes: FieldTypes = '*' +) => { + const filteredFields = fields.filter((field: IndexPatternField) => { + if (!field.aggregatable || isNestedField(field) || field.scripted) { + return false; + } + + return filterByType([field], filterFieldTypes).length !== 0; + }); + + return filteredFields; +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/utils/index.ts b/src/plugins/wizard/public/application/components/data_tab/utils/index.ts new file mode 100644 index 000000000000..dd0cdea3e23e --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/utils/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { getAvailableFields } from './get_available_fields'; diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss index 7291a79eb726..fe3318c32132 100644 --- a/src/plugins/wizard/public/application/components/side_nav.scss +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -21,6 +21,6 @@ } .wizDatasourceSelect { - max-width: $wizSideNavWidth; - padding: $euiSize $euiSize 0 $euiSize; + max-width: $wizSideNavWidth; + padding: $euiSize $euiSize 0 $euiSize; } diff --git a/src/plugins/wizard/public/application/components/top_nav.scss b/src/plugins/wizard/public/application/components/top_nav.scss index f8e1d1d6cfa4..0e9c5f46aa3f 100644 --- a/src/plugins/wizard/public/application/components/top_nav.scss +++ b/src/plugins/wizard/public/application/components/top_nav.scss @@ -1,4 +1,4 @@ .wizTopNav { - grid-area: topNav; - border-bottom: $euiBorderThin; -} \ No newline at end of file + grid-area: topNav; + border-bottom: $euiBorderThin; +} From 3c0c72ff0305f4cceb6239bca219a535f9590eff Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Thu, 21 Jul 2022 18:26:03 -0700 Subject: [PATCH 23/47] [D&D] Add wizard saved objects to vis list (#1933) via appExtensions of alias registration fixes #1887 Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../vis_types/vis_type_alias_registry.ts | 3 ++- src/plugins/wizard/public/plugin.ts | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts index 0d196482be37..7f5323cee06a 100644 --- a/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts +++ b/src/plugins/visualizations/public/vis_types/vis_type_alias_registry.ts @@ -28,6 +28,7 @@ * under the License. */ +import { SavedObjectAttributes } from 'opensearch-dashboards/public'; import { TriggerContextMapping } from '../../../ui_actions/public'; export interface VisualizationListItem { @@ -51,7 +52,7 @@ export interface VisualizationsAppExtension { toListItem: (savedObject: { id: string; type: string; - attributes: object; + attributes: SavedObjectAttributes; }) => VisualizationListItem; } diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 4f4e7016044d..422dc78c7d0b 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -21,7 +21,7 @@ import { } from './types'; import { WizardEmbeddableFactoryDefinition, WIZARD_EMBEDDABLE } from './embeddable'; import wizardIcon from './assets/wizard_icon.svg'; -import { PLUGIN_ID, PLUGIN_NAME } from '../common'; +import { EDIT_PATH, PLUGIN_ID, PLUGIN_NAME, WIZARD_SAVED_OBJECT } from '../common'; import { TypeService } from './services/type_service'; import { getPreloadedStore } from './application/utils/state_management'; import { setAggService, setIndexPatterns } from './plugin_services'; @@ -119,6 +119,22 @@ export class WizardPlugin stage: 'experimental', aliasApp: PLUGIN_ID, aliasPath: '#/', + appExtensions: { + visualizations: { + docTypes: [PLUGIN_ID], + toListItem: ({ id, attributes }) => ({ + description: attributes?.description, + editApp: PLUGIN_ID, + editUrl: `${EDIT_PATH}/${encodeURIComponent(id)}`, + icon: wizardIcon, + id, + savedObjectType: WIZARD_SAVED_OBJECT, + stage: 'experimental', + title: attributes?.title, + typeTitle: PLUGIN_NAME, + }), + }, + }, }); return { From 7e9a693e0f0e1701573099a33ffe23b96f9e2f7d Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Thu, 21 Jul 2022 18:48:58 -0700 Subject: [PATCH 24/47] [D&D] Feature/experimental (#1934) * feat: experimental banner Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * feat: experimental embeddable Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: experimental banner location Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../wizard/type_selection/type_selection.tsx | 18 ++++---- .../components/experimental_info.tsx | 43 +++++++++++++++++++ .../application/components/workspace.tsx | 4 ++ .../public/embeddable/disabled_embeddable.tsx | 34 +++++++++++++++ .../embeddable/disabled_visualization.scss | 8 ++++ .../embeddable/disabled_visualization.tsx | 31 +++++++++++++ .../embeddable/wizard_embeddable_factory.tsx | 28 +++++++++--- src/plugins/wizard/public/plugin.ts | 1 + 8 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 src/plugins/wizard/public/application/components/experimental_info.tsx create mode 100644 src/plugins/wizard/public/embeddable/disabled_embeddable.tsx create mode 100644 src/plugins/wizard/public/embeddable/disabled_visualization.scss create mode 100644 src/plugins/wizard/public/embeddable/disabled_visualization.tsx diff --git a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx index 27ea53f0f16f..c5c6546c01c0 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx @@ -182,21 +182,19 @@ class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionSta } private filteredVisTypes(visTypes: TypesStart, query: string): VisTypeListEntry[] { - const types = visTypes.all().filter((type) => { - // Filter out all lab visualizations if lab mode is not enabled + const filterExperimental = (type: VisType | VisTypeAlias): boolean => { if (!this.props.showExperimental && type.stage === 'experimental') { return false; } - - // Filter out hidden visualizations - if (type.hidden) { - return false; - } - return true; - }); + }; - const allTypes = [...types, ...visTypes.getAliases()]; + const types = visTypes + .all() + .filter(filterExperimental) + .filter((type) => !type.hidden); // Filter out hidden visualizations + const aliasedTypes = visTypes.getAliases().filter(filterExperimental); + const allTypes = [...types, ...aliasedTypes]; let entries: VisTypeListEntry[]; if (!query) { diff --git a/src/plugins/wizard/public/application/components/experimental_info.tsx b/src/plugins/wizard/public/application/components/experimental_info.tsx new file mode 100644 index 000000000000..cc4d0cab1340 --- /dev/null +++ b/src/plugins/wizard/public/application/components/experimental_info.tsx @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { memo } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; + +export const InfoComponent = () => { + const title = ( + <> + <FormattedMessage + id="wizard.experimentalInfoText" + defaultMessage="This editor is experimental, do not use in production. + For feedback, please create an issue in {githubLink}." + values={{ + githubLink: ( + <EuiLink + external + href="https://github.com/opensearch-project/OpenSearch-Dashboards/issues/new/choose" + target="_blank" + > + GitHub + </EuiLink> + ), + }} + /> + </> + ); + + return ( + <EuiCallOut + className="hide-for-sharing" + data-test-subj="experimentalVisInfo" + size="s" + title={title} + iconType="beaker" + /> + ); +}; + +export const ExperimentalInfo = memo(InfoComponent); diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 9a910b51c320..39d75a7c43d2 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -27,6 +27,7 @@ import hand_field from '../../assets/hand_field.svg'; import fields_bg from '../../assets/fields_bg.svg'; import './workspace.scss'; +import { ExperimentalInfo } from './experimental_info'; export const Workspace: FC = ({ children }) => { const { @@ -84,6 +85,9 @@ export const Workspace: FC = ({ children }) => { <EuiFlexItem grow={false}> <TypeSelectorPopover /> </EuiFlexItem> + <EuiFlexItem> + <ExperimentalInfo /> + </EuiFlexItem> </EuiFlexGroup> <EuiPanel className="wizCanvas"> {expression ? ( diff --git a/src/plugins/wizard/public/embeddable/disabled_embeddable.tsx b/src/plugins/wizard/public/embeddable/disabled_embeddable.tsx new file mode 100644 index 000000000000..5f55859156d0 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/disabled_embeddable.tsx @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Embeddable, EmbeddableOutput } from '../../../embeddable/public'; + +import { DisabledVisualization } from './disabled_visualization'; +import { WizardInput, WIZARD_EMBEDDABLE } from './wizard_embeddable'; + +export class DisabledEmbeddable extends Embeddable<WizardInput, EmbeddableOutput> { + private domNode?: HTMLElement; + public readonly type = WIZARD_EMBEDDABLE; + + constructor(private readonly title: string, initialInput: WizardInput) { + super(initialInput, { title }); + } + + public reload() {} + public render(domNode: HTMLElement) { + if (this.title) { + this.domNode = domNode; + ReactDOM.render(<DisabledVisualization title={this.title} />, domNode); + } + } + + public destroy() { + if (this.domNode) { + ReactDOM.unmountComponentAtNode(this.domNode); + } + } +} diff --git a/src/plugins/wizard/public/embeddable/disabled_visualization.scss b/src/plugins/wizard/public/embeddable/disabled_visualization.scss new file mode 100644 index 000000000000..73f22386e80e --- /dev/null +++ b/src/plugins/wizard/public/embeddable/disabled_visualization.scss @@ -0,0 +1,8 @@ +.wizDisabledVisualization { + width: 100%; + display: grid; + grid-gap: $euiSize; + place-content: center; + place-items: center; + text-align: center; +} diff --git a/src/plugins/wizard/public/embeddable/disabled_visualization.tsx b/src/plugins/wizard/public/embeddable/disabled_visualization.tsx new file mode 100644 index 000000000000..04b8bf234541 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/disabled_visualization.tsx @@ -0,0 +1,31 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiIcon } from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import React from 'react'; + +import './disabled_visualization.scss'; + +export function DisabledVisualization({ title }: { title: string }) { + return ( + <div className="wizDisabledVisualization"> + <EuiIcon type="beaker" size="xl" /> + <div> + <FormattedMessage + id="wizard.disabledVisualizationTitle" + defaultMessage="{title} is an experimental visualization." + values={{ title: <em className="visDisabledLabVisualization__title">{title}</em> }} + /> + </div> + <div> + <FormattedMessage + id="wizard.disabledVisualizationMessage" + defaultMessage="Please turn on lab-mode in the advanced settings to see these visualizations." + /> + </div> + </div> + ); +} diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx index ce1780d0066e..b870687c2d2e 100644 --- a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx @@ -4,18 +4,25 @@ */ import { i18n } from '@osd/i18n'; -import { NotificationsStart, SavedObjectsClientContract } from '../../../../core/public'; +import { + IUiSettingsClient, + NotificationsStart, + SavedObjectsClientContract, +} from '../../../../core/public'; import { DataPublicPluginStart } from '../../../data/public'; import { EmbeddableFactory, EmbeddableFactoryDefinition, + EmbeddableOutput, EmbeddableStart, ErrorEmbeddable, IContainer, } from '../../../embeddable/public'; import { ExpressionsStart } from '../../../expressions/public'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public'; import { WizardSavedObjectAttributes } from '../../common'; import { TypeServiceStart } from '../services/type_service'; +import { DisabledEmbeddable } from './disabled_embeddable'; import { WizardEmbeddable, WizardInput, @@ -30,13 +37,14 @@ interface StartServices { savedObjectsClient: SavedObjectsClientContract; notifications: NotificationsStart; types: TypeServiceStart; + uiSettings: IUiSettingsClient; } // TODO: use or remove? export type WizardEmbeddableFactory = EmbeddableFactory< WizardInput, - WizardOutput, - WizardEmbeddable, + WizardOutput | EmbeddableOutput, + WizardEmbeddable | DisabledEmbeddable, WizardSavedObjectAttributes >; @@ -44,8 +52,8 @@ export class WizardEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition< WizardInput, - WizardOutput, - WizardEmbeddable, + WizardOutput | EmbeddableOutput, + WizardEmbeddable | DisabledEmbeddable, WizardSavedObjectAttributes > { public readonly type = WIZARD_EMBEDDABLE; @@ -69,7 +77,7 @@ export class WizardEmbeddableFactoryDefinition savedObjectId: string, input: Partial<WizardInput> & { id: string }, parent?: IContainer - ): Promise<WizardEmbeddable | ErrorEmbeddable> => { + ): Promise<WizardEmbeddable | ErrorEmbeddable | DisabledEmbeddable> => { return this.create({ ...input, savedObjectId }, parent); }; @@ -81,7 +89,15 @@ export class WizardEmbeddableFactoryDefinition notifications: { toasts }, savedObjectsClient, types, + uiSettings, } = await this.getStartServices(); + + const isLabsEnabled = uiSettings.get<boolean>(VISUALIZE_ENABLE_LABS_SETTING); + + if (!isLabsEnabled) { + return new DisabledEmbeddable('Wizard', input); + } + return new WizardEmbeddable(input, { parent, data, diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 422dc78c7d0b..4946c93761e9 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -104,6 +104,7 @@ export class WizardPlugin expressions: pluginsStart.expressions, notifications: coreStart.notifications, types: this.typeService.start(), + uiSettings: coreStart.uiSettings, }; }); embeddable.registerEmbeddableFactory(WIZARD_EMBEDDABLE, embeddableFactory); From 818d230a4ec71d9a075c331007fc634b2a7872b2 Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Thu, 21 Jul 2022 22:50:03 -0700 Subject: [PATCH 25/47] [D&D] Remove search/hasMatch embeddable vestiges (#1935) - removedebugging rendering - also update embeddables icon fixes #1910, #1925 Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../public/embeddable/wizard_component.tsx | 77 ++----------------- .../public/embeddable/wizard_embeddable.tsx | 39 +--------- .../embeddable/wizard_embeddable_factory.tsx | 19 ++--- .../wizard/server/saved_objects/wizard_app.ts | 2 +- 4 files changed, 20 insertions(+), 117 deletions(-) diff --git a/src/plugins/wizard/public/embeddable/wizard_component.tsx b/src/plugins/wizard/public/embeddable/wizard_component.tsx index 79a768c754c3..58087c83ffda 100644 --- a/src/plugins/wizard/public/embeddable/wizard_component.tsx +++ b/src/plugins/wizard/public/embeddable/wizard_component.tsx @@ -4,48 +4,24 @@ */ import React, { useEffect, useState } from 'react'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiText, - EuiAvatar, - EuiFlexGrid, - EuiCodeBlock, -} from '@elastic/eui'; -import { withEmbeddableSubscription } from '../../../embeddable/public'; -import { WizardEmbeddable, WizardInput, WizardOutput } from './wizard_embeddable'; +import { SavedObjectEmbeddableInput, withEmbeddableSubscription } from '../../../embeddable/public'; +import { WizardEmbeddable, WizardOutput } from './wizard_embeddable'; import { validateSchemaState } from '../application/utils/validate_schema_state'; interface Props { embeddable: WizardEmbeddable; - input: WizardInput; + input: SavedObjectEmbeddableInput; output: WizardOutput; } -function wrapSearchTerms(task?: string, search?: string) { - if (!search) return task; - if (!task) return task; - const parts = task.split(new RegExp(`(${search})`, 'g')); - return parts.map((part, i) => - part === search ? ( - <span key={i} style={{ backgroundColor: 'yellow' }}> - {part} - </span> - ) : ( - part - ) - ); -} - function WizardEmbeddableComponentInner({ embeddable, - input: { search }, + input: {}, output: { savedAttributes }, }: Props) { const { ReactExpressionRenderer, toasts, types, indexPatterns, aggs } = embeddable; const [expression, setExpression] = useState<string>(); - const { title, description, visualizationState, styleState } = savedAttributes || {}; useEffect(() => { const { visualizationState: visualization, styleState: style } = savedAttributes || {}; @@ -84,50 +60,13 @@ function WizardEmbeddableComponentInner({ } }, [aggs, indexPatterns, savedAttributes, toasts, types]); - // TODO: add correct loading and error states, remove debugging mode - return ( - <> - {expression ? ( - <EuiFlexItem> - <ReactExpressionRenderer expression={expression} /> - </EuiFlexItem> - ) : ( - <EuiFlexGroup> - <EuiFlexItem grow={false}> - <EuiAvatar name={title || description || ''} size="l" /> - </EuiFlexItem> - <EuiFlexItem> - <EuiFlexGrid columns={1}> - <EuiFlexItem> - <EuiText data-test-subj="wizardEmbeddableTitle"> - <h3>{wrapSearchTerms(title || '', search)}</h3> - </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <EuiText data-test-subj="wizardEmbeddableDescription"> - {wrapSearchTerms(description, search)} - </EuiText> - </EuiFlexItem> - <EuiFlexItem> - <EuiCodeBlock data-test-subj="wizardEmbeddableDescription"> - {wrapSearchTerms(visualizationState, search)} - </EuiCodeBlock> - </EuiFlexItem> - <EuiFlexItem> - <EuiCodeBlock data-test-subj="wizardEmbeddableDescription"> - {wrapSearchTerms(styleState, search)} - </EuiCodeBlock> - </EuiFlexItem> - </EuiFlexGrid> - </EuiFlexItem> - </EuiFlexGroup> - )} - </> - ); + return <ReactExpressionRenderer expression={expression ?? ''} />; + + // TODO: add correct loading and error states } export const WizardEmbeddableComponent = withEmbeddableSubscription< - WizardInput, + SavedObjectEmbeddableInput, WizardOutput, WizardEmbeddable >(WizardEmbeddableComponentInner); diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx index 2e4a137d368c..12449674ce6c 100644 --- a/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx @@ -22,21 +22,7 @@ import { DataPublicPluginStart } from '../../../data/public'; export const WIZARD_EMBEDDABLE = 'WIZARD_EMBEDDABLE'; -// TODO: remove search, hasMatch or update as appropriate -export interface WizardInput extends SavedObjectEmbeddableInput { - /** - * Optional search string which will be used to highlight search terms as - * well as calculate `output.hasMatch`. - */ - search?: string; -} - export interface WizardOutput extends EmbeddableOutput { - /** - * Should be true if input.search is defined and the task or title contain - * search as a substring. - */ - hasMatch: boolean; /** * Will contain the saved object attributes of the Wizard Saved Object that matches * `input.savedObjectId`. If the id is invalid, this may be undefined. @@ -44,22 +30,7 @@ export interface WizardOutput extends EmbeddableOutput { savedAttributes?: WizardSavedObjectAttributes; } -/** - * Returns whether any attributes contain the search string. If search is empty, true is returned. If - * there are no savedAttributes, false is returned. - * @param search - the search string - * @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId` - */ -function getHasMatch(search?: string, savedAttributes?: WizardSavedObjectAttributes): boolean { - if (!search) return true; - if (!savedAttributes) return false; - return Boolean( - (savedAttributes.description && savedAttributes.description.match(search)) || - (savedAttributes.title && savedAttributes.title.match(search)) - ); -} - -export class WizardEmbeddable extends Embeddable<WizardInput, WizardOutput> { +export class WizardEmbeddable extends Embeddable<SavedObjectEmbeddableInput, WizardOutput> { public readonly type = WIZARD_EMBEDDABLE; private subscription: Subscription; private node?: HTMLElement; @@ -72,7 +43,7 @@ export class WizardEmbeddable extends Embeddable<WizardInput, WizardOutput> { private savedObjectId?: string; constructor( - initialInput: WizardInput, + initialInput: SavedObjectEmbeddableInput, { parent, savedObjectsClient, @@ -90,7 +61,7 @@ export class WizardEmbeddable extends Embeddable<WizardInput, WizardOutput> { } ) { // TODO: can default title come from saved object? - super(initialInput, { defaultTitle: 'wizard', hasMatch: false }, parent); + super(initialInput, { defaultTitle: 'wizard' }, parent); this.savedObjectsClient = savedObjectsClient; this.ReactExpressionRenderer = ReactExpressionRenderer; this.toasts = toasts; @@ -115,10 +86,7 @@ export class WizardEmbeddable extends Embeddable<WizardInput, WizardOutput> { savedAttributes = wizardSavedObject?.attributes; } - // The search string might have changed as well so we need to make sure we recalculate - // hasMatch. this.updateOutput({ - hasMatch: getHasMatch(this.input.search, savedAttributes), savedAttributes, title: savedAttributes?.title, }); @@ -144,7 +112,6 @@ export class WizardEmbeddable extends Embeddable<WizardInput, WizardOutput> { ); const savedAttributes = wizardSavedObject?.attributes; this.updateOutput({ - hasMatch: getHasMatch(this.input.search, savedAttributes), savedAttributes, title: wizardSavedObject?.attributes?.title, }); diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx index b870687c2d2e..6e8a49a05d3f 100644 --- a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx @@ -17,18 +17,15 @@ import { EmbeddableStart, ErrorEmbeddable, IContainer, + SavedObjectEmbeddableInput, } from '../../../embeddable/public'; import { ExpressionsStart } from '../../../expressions/public'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public'; import { WizardSavedObjectAttributes } from '../../common'; import { TypeServiceStart } from '../services/type_service'; import { DisabledEmbeddable } from './disabled_embeddable'; -import { - WizardEmbeddable, - WizardInput, - WizardOutput, - WIZARD_EMBEDDABLE, -} from './wizard_embeddable'; +import { WizardEmbeddable, WizardOutput, WIZARD_EMBEDDABLE } from './wizard_embeddable'; +import wizardIcon from '../assets/wizard_icon.svg'; interface StartServices { data: DataPublicPluginStart; @@ -42,7 +39,7 @@ interface StartServices { // TODO: use or remove? export type WizardEmbeddableFactory = EmbeddableFactory< - WizardInput, + SavedObjectEmbeddableInput, WizardOutput | EmbeddableOutput, WizardEmbeddable | DisabledEmbeddable, WizardSavedObjectAttributes @@ -51,7 +48,7 @@ export type WizardEmbeddableFactory = EmbeddableFactory< export class WizardEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition< - WizardInput, + SavedObjectEmbeddableInput, WizardOutput | EmbeddableOutput, WizardEmbeddable | DisabledEmbeddable, WizardSavedObjectAttributes @@ -62,7 +59,7 @@ export class WizardEmbeddableFactoryDefinition name: 'Wizard', includeFields: ['visualizationState'], type: 'wizard', - getIconForSavedObject: () => 'pencil', + getIconForSavedObject: () => wizardIcon, }; constructor(private getStartServices: () => Promise<StartServices>) {} @@ -75,13 +72,13 @@ export class WizardEmbeddableFactoryDefinition public createFromSavedObject = ( savedObjectId: string, - input: Partial<WizardInput> & { id: string }, + input: Partial<SavedObjectEmbeddableInput> & { id: string }, parent?: IContainer ): Promise<WizardEmbeddable | ErrorEmbeddable | DisabledEmbeddable> => { return this.create({ ...input, savedObjectId }, parent); }; - public async create(input: WizardInput, parent?: IContainer) { + public async create(input: SavedObjectEmbeddableInput, parent?: IContainer) { // TODO: Use savedWizardLoader here instead const { data, diff --git a/src/plugins/wizard/server/saved_objects/wizard_app.ts b/src/plugins/wizard/server/saved_objects/wizard_app.ts index 9f17dd502a84..f5820d0f3f29 100644 --- a/src/plugins/wizard/server/saved_objects/wizard_app.ts +++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts @@ -16,7 +16,7 @@ export const wizardSavedObjectType: SavedObjectsType = { hidden: false, namespaceType: 'single', management: { - icon: 'visVisualBuilder', // TODO: Need a custom icon here + // icon: '', // TODO: Need a custom icon here - unfortunately a custom SVG won't work without changes to the SavedObjectsManagement plugin defaultSearchField: 'title', importableAndExportable: true, getTitle: ({ attributes: { title } }: SavedObject<WizardSavedObjectAttributes>) => title, From 24b22d1dbb842583401c6e43a28aac8669b91742 Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Mon, 25 Jul 2022 17:43:37 -0700 Subject: [PATCH 26/47] [D&D] Refactor and cleanup embeddables (#1947) General plugin updates (#1939): - add start service getters/setters to plugin service - move setters from setup to start so they're available to embeddable Embeddable updates: - use getters instead of depending on start services in constructor - remove wizard from add panel "create" list - add correct edit paths/URLs for linking to wizard #1940 - add basic error embeddable rendering - render via ExpressionLoader instead of wizard_component #1920 - wizard_component no longer used, but updated for future use - add subscription handling for query, filter, timerange changes #1937 - fix clone/replace panel actions #1943, #1944 - fix title/description panel rendering #1921 - add inspection panel action #1936 Asset updates: - Update empty workspace illustration - Add secondary fill icon version to match new visualization icons fixes #1936, #1920, #1937, #1940, #1921, #1939, #1941, #1943, #1944 Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../wizard/public/assets/fields_bg.svg | 91 +++-- .../wizard/public/assets/hand_field.svg | 24 +- .../assets/wizard_icon_secondary_fill.svg | 25 ++ .../public/embeddable/wizard_component.tsx | 66 +--- .../public/embeddable/wizard_embeddable.tsx | 314 +++++++++++++----- .../embeddable/wizard_embeddable_factory.tsx | 101 +++--- src/plugins/wizard/public/plugin.ts | 66 ++-- src/plugins/wizard/public/plugin_services.ts | 26 +- .../type_service/visualization_type.tsx | 6 +- .../visualizations/metric/to_expression.ts | 11 +- 10 files changed, 468 insertions(+), 262 deletions(-) create mode 100644 src/plugins/wizard/public/assets/wizard_icon_secondary_fill.svg diff --git a/src/plugins/wizard/public/assets/fields_bg.svg b/src/plugins/wizard/public/assets/fields_bg.svg index 3c22912ec34b..d7ac9e455f0c 100644 --- a/src/plugins/wizard/public/assets/fields_bg.svg +++ b/src/plugins/wizard/public/assets/fields_bg.svg @@ -1,36 +1,61 @@ -<svg width="336" height="220" viewBox="0 0 336 220" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="118" y="2" width="216" height="216" rx="2" fill="url(#paint0_diamond_169_44976)" stroke="#006BB4" stroke-width="4" stroke-linecap="round" stroke-dasharray="108" stroke-dashoffset="54"/> +<svg width="243" height="186" viewBox="0 0 243 186" fill="none" xmlns="http://www.w3.org/2000/svg"> <g opacity="0.5"> -<path d="M0 99.6C0 98.1641 1.14799 97 2.5641 97H97.4359C98.852 97 100 98.1641 100 99.6V120.4C100 121.836 98.852 123 97.4359 123H2.5641C1.14799 123 0 121.836 0 120.4V99.6Z" fill="#EFF4F9"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 99.6H2.5641L2.5641 120.4H97.4359V99.6ZM2.5641 97C1.14799 97 0 98.1641 0 99.6V120.4C0 121.836 1.14799 123 2.5641 123H97.4359C98.852 123 100 121.836 100 120.4V99.6C100 98.1641 98.852 97 97.4359 97H2.5641Z" fill="#343741"/> -<path d="M15.155 104.091L14.6642 106.413H16.4707L16.0426 108.433H14.2361L13.359 112.723C13.3102 113.016 13.3329 113.238 13.4268 113.387C13.5208 113.537 13.7558 113.618 14.1317 113.629C14.2779 113.635 14.5772 113.62 15.0297 113.586L14.7791 115.692C14.2013 115.847 13.5852 115.919 12.9308 115.908C11.8657 115.896 11.0687 115.637 10.5396 115.131C10.0105 114.625 9.79474 113.937 9.8922 113.068L10.8111 108.433H9.41187L9.82955 106.413H11.2288L11.7196 104.091H15.155Z" fill="#4A7194"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 110C22.353 109.347 22.9324 108.818 23.6471 108.818H34.0001C34.7148 108.818 35.2942 109.347 35.2942 110C35.2942 110.653 34.7148 111.182 34.0001 111.182H23.6471C22.9324 111.182 22.353 110.653 22.353 110Z" fill="#343741"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 110C43.5293 109.347 44.0999 108.818 44.8038 108.818L88.1371 108.818C88.841 108.818 89.4117 109.347 89.4117 110C89.4117 110.653 88.841 111.182 88.1371 111.182L44.8038 111.182C44.0999 111.182 43.5293 110.653 43.5293 110Z" fill="#343741"/> -<path d="M0 67.6C0 66.1641 1.14799 65 2.5641 65H97.4359C98.852 65 100 66.1641 100 67.6V88.4C100 89.8359 98.852 91 97.4359 91H2.5641C1.14799 91 0 89.8359 0 88.4V67.6Z" fill="#EFF4F9"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 67.6H2.5641L2.5641 88.4H97.4359V67.6ZM2.5641 65C1.14799 65 0 66.1641 0 67.6V88.4C0 89.8359 1.14799 91 2.5641 91H97.4359C98.852 91 100 89.8359 100 88.4V67.6C100 66.1641 98.852 65 97.4359 65H2.5641Z" fill="#343741"/> -<path d="M15.155 70.9091L14.6642 73.4633H16.4707L16.0426 75.6851H14.2361L13.359 80.4041C13.3102 80.727 13.3329 80.9707 13.4268 81.1353C13.5208 81.2998 13.7558 81.3885 14.1317 81.4011C14.2779 81.4075 14.5772 81.3916 15.0297 81.3536L14.7791 83.6704C14.2013 83.8413 13.5852 83.9205 12.9308 83.9078C11.8657 83.8951 11.0687 83.6103 10.5396 83.0533C10.0105 82.4962 9.79474 81.7398 9.8922 80.7839L10.8111 75.6851H9.41187L9.82955 73.4633H11.2288L11.7196 70.9091H15.155Z" fill="#4A7194"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 78C22.353 77.3473 22.9324 76.8182 23.6471 76.8182H34.0001C34.7148 76.8182 35.2942 77.3473 35.2942 78C35.2942 78.6527 34.7148 79.1818 34.0001 79.1818H23.6471C22.9324 79.1818 22.353 78.6527 22.353 78Z" fill="#343741"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 78C43.5293 77.3473 44.0999 76.8182 44.8038 76.8182L88.1371 76.8182C88.841 76.8182 89.4117 77.3473 89.4117 78C89.4117 78.6527 88.841 79.1818 88.1371 79.1818L44.8038 79.1818C44.0999 79.1818 43.5293 78.6527 43.5293 78Z" fill="#343741"/> -<path d="M0 35.6C0 34.1641 1.14799 33 2.5641 33H97.4359C98.852 33 100 34.1641 100 35.6V56.4C100 57.8359 98.852 59 97.4359 59H2.5641C1.14799 59 0 57.8359 0 56.4V35.6Z" fill="#EFF4F9"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 35.6H2.5641L2.5641 56.4H97.4359V35.6ZM2.5641 33C1.14799 33 0 34.1641 0 35.6V56.4C0 57.8359 1.14799 59 2.5641 59H97.4359C98.852 59 100 57.8359 100 56.4V35.6C100 34.1641 98.852 33 97.4359 33H2.5641Z" fill="#343741"/> -<path d="M15.155 38.9091L14.6642 41.4633H16.4707L16.0426 43.6851H14.2361L13.359 48.4041C13.3102 48.727 13.3329 48.9707 13.4268 49.1353C13.5208 49.2998 13.7558 49.3885 14.1317 49.4011C14.2779 49.4075 14.5772 49.3916 15.0297 49.3536L14.7791 51.6704C14.2013 51.8413 13.5852 51.9205 12.9308 51.9078C11.8657 51.8951 11.0687 51.6103 10.5396 51.0533C10.0105 50.4962 9.79474 49.7398 9.8922 48.7839L10.8111 43.6851H9.41187L9.82955 41.4633H11.2288L11.7196 38.9091H15.155Z" fill="#4A7194"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 46C22.353 45.3473 22.9324 44.8182 23.6471 44.8182H34.0001C34.7148 44.8182 35.2942 45.3473 35.2942 46C35.2942 46.6527 34.7148 47.1818 34.0001 47.1818H23.6471C22.9324 47.1818 22.353 46.6527 22.353 46Z" fill="#343741"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 46C43.5293 45.3473 44.0999 44.8182 44.8038 44.8182L88.1371 44.8182C88.841 44.8182 89.4117 45.3473 89.4117 46C89.4117 46.6527 88.841 47.1818 88.1371 47.1818L44.8038 47.1818C44.0999 47.1818 43.5293 46.6527 43.5293 46Z" fill="#343741"/> -<path d="M0 131.6C0 130.164 1.14799 129 2.5641 129H97.4359C98.852 129 100 130.164 100 131.6V152.4C100 153.836 98.852 155 97.4359 155H2.5641C1.14799 155 0 153.836 0 152.4V131.6Z" fill="#EFF4F9"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 131.6H2.5641L2.5641 152.4H97.4359V131.6ZM2.5641 129C1.14799 129 0 130.164 0 131.6V152.4C0 153.836 1.14799 155 2.5641 155H97.4359C98.852 155 100 153.836 100 152.4V131.6C100 130.164 98.852 129 97.4359 129H2.5641Z" fill="#343741"/> -<path d="M12.6653 144.581H11.4987L10.4188 147.909H8.83443L9.91433 144.581H8.23535L8.48759 143.096H10.3873L11.0888 140.953H9.41773L9.67786 139.468H11.5697L12.6653 136.091H14.2419L13.1462 139.468H14.3207L15.4164 136.091H17.0007L15.9051 139.468H17.6471L17.3949 140.953H15.4321L14.7306 143.096H16.4569L16.2046 144.581H14.2497L13.1777 147.909H11.5933L12.6653 144.581ZM11.9717 143.096H13.1383L13.8477 140.953H12.6732L11.9717 143.096Z" fill="#387765"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 142C22.353 141.347 22.9324 140.818 23.6471 140.818H34.0001C34.7148 140.818 35.2942 141.347 35.2942 142C35.2942 142.653 34.7148 143.182 34.0001 143.182H23.6471C22.9324 143.182 22.353 142.653 22.353 142Z" fill="#343741"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 142C43.5293 141.347 44.0999 140.818 44.8038 140.818L88.1371 140.818C88.841 140.818 89.4117 141.347 89.4117 142C89.4117 142.653 88.841 143.182 88.1371 143.182L44.8038 143.182C44.0999 143.182 43.5293 142.653 43.5293 142Z" fill="#343741"/> -<path d="M0 163.6C0 162.164 1.14799 161 2.5641 161H97.4359C98.852 161 100 162.164 100 163.6V184.4C100 185.836 98.852 187 97.4359 187H2.5641C1.14799 187 0 185.836 0 184.4V163.6Z" fill="#EFF4F9"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 163.6H2.5641L2.5641 184.4H97.4359V163.6ZM2.5641 161C1.14799 161 0 162.164 0 163.6V184.4C0 185.836 1.14799 187 2.5641 187H97.4359C98.852 187 100 185.836 100 184.4V163.6C100 162.164 98.852 161 97.4359 161H2.5641Z" fill="#343741"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8236 179.398C18.8236 180.333 18.1451 181.091 17.3066 181.091H9.75239C8.91455 181.091 8.23535 180.334 8.23535 179.398V170.966C8.23535 170.031 8.9138 169.273 9.75239 169.273H11.4118V168.687C11.4118 168.395 11.6059 168.151 11.8483 168.1L11.9412 168.091C12.2336 168.091 12.4706 168.344 12.4706 168.687V169.273H14.5883V168.687C14.5883 168.395 14.7823 168.151 15.0248 168.1L15.1177 168.091C15.4101 168.091 15.6471 168.344 15.6471 168.687V169.273H17.3066C18.1444 169.273 18.8236 170.03 18.8236 170.966V179.398ZM9.29418 172.818V179.117C9.29418 179.555 9.61176 179.909 10.0042 179.909H17.0547C17.4473 179.909 17.7648 179.555 17.7648 179.117V172.818H9.29418ZM10.8824 177.545C11.1423 177.545 11.3585 177.762 11.4033 178.033L11.4118 178.136C11.4118 178.426 11.2178 178.668 10.9753 178.718L10.8824 178.727C10.59 178.727 10.353 178.453 10.353 178.136C10.353 177.846 10.547 177.605 10.7895 177.555L10.8824 177.545ZM13.5295 177.545C13.7894 177.545 14.0055 177.762 14.0504 178.033L14.0589 178.136C14.0589 178.426 13.8648 178.668 13.6224 178.718L13.5295 178.727C13.2371 178.727 13.0001 178.453 13.0001 178.136C13.0001 177.846 13.1941 177.605 13.4366 177.555L13.5295 177.545ZM16.1765 177.545C16.4364 177.545 16.6526 177.762 16.6974 178.033L16.7059 178.136C16.7059 178.426 16.5119 178.668 16.2694 178.718L16.1765 178.727C15.8841 178.727 15.6471 178.453 15.6471 178.136C15.6471 177.846 15.8412 177.605 16.0836 177.555L16.1765 177.545ZM10.8824 175.773C11.1423 175.773 11.3585 175.989 11.4033 176.26L11.4118 176.364C11.4118 176.654 11.2178 176.895 10.9753 176.945L10.8824 176.955C10.59 176.955 10.353 176.68 10.353 176.364C10.353 176.074 10.547 175.832 10.7895 175.782L10.8824 175.773ZM13.5295 175.773C13.7894 175.773 14.0055 175.989 14.0504 176.26L14.0589 176.364C14.0589 176.654 13.8648 176.895 13.6224 176.945L13.5295 176.955C13.2371 176.955 13.0001 176.68 13.0001 176.364C13.0001 176.074 13.1941 175.832 13.4366 175.782L13.5295 175.773ZM16.1765 175.773C16.4364 175.773 16.6526 175.989 16.6974 176.26L16.7059 176.364C16.7059 176.654 16.5119 176.895 16.2694 176.945L16.1765 176.955C15.8841 176.955 15.6471 176.68 15.6471 176.364C15.6471 176.074 15.8412 175.832 16.0836 175.782L16.1765 175.773ZM10.8824 174C11.1423 174 11.3585 174.217 11.4033 174.487L11.4118 174.591C11.4118 174.881 11.2178 175.122 10.9753 175.172L10.8824 175.182C10.59 175.182 10.353 174.908 10.353 174.591C10.353 174.301 10.547 174.06 10.7895 174.009L10.8824 174ZM13.5295 174C13.7894 174 14.0055 174.217 14.0504 174.487L14.0589 174.591C14.0589 174.881 13.8648 175.122 13.6224 175.172L13.5295 175.182C13.2371 175.182 13.0001 174.908 13.0001 174.591C13.0001 174.301 13.1941 174.06 13.4366 174.009L13.5295 174ZM16.1765 174C16.4364 174 16.6526 174.217 16.6974 174.487L16.7059 174.591C16.7059 174.881 16.5119 175.122 16.2694 175.172L16.1765 175.182C15.8841 175.182 15.6471 174.908 15.6471 174.591C15.6471 174.301 15.8412 174.06 16.0836 174.009L16.1765 174ZM9.29418 171.636H17.7648V171.247C17.7648 170.809 17.4472 170.455 17.0547 170.455H10.0042C9.61166 170.455 9.29418 170.809 9.29418 171.247V171.636Z" fill="#7B705A"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 174C22.353 173.347 22.9324 172.818 23.6471 172.818H34.0001C34.7148 172.818 35.2942 173.347 35.2942 174C35.2942 174.653 34.7148 175.182 34.0001 175.182H23.6471C22.9324 175.182 22.353 174.653 22.353 174Z" fill="#343741"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 174C43.5293 173.347 44.0999 172.818 44.8038 172.818L88.1371 172.818C88.841 172.818 89.4117 173.347 89.4117 174C89.4117 174.653 88.841 175.182 88.1371 175.182L44.8038 175.182C44.0999 175.182 43.5293 174.653 43.5293 174Z" fill="#343741"/> +<path d="M0 98.6C0 97.1641 1.14799 96 2.5641 96H97.4359C98.852 96 100 97.1641 100 98.6V119.4C100 120.836 98.852 122 97.4359 122H2.5641C1.14799 122 0 120.836 0 119.4V98.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 98.6H2.5641L2.5641 119.4H97.4359V98.6ZM2.5641 96C1.14799 96 0 97.1641 0 98.6V119.4C0 120.836 1.14799 122 2.5641 122H97.4359C98.852 122 100 120.836 100 119.4V98.6C100 97.1641 98.852 96 97.4359 96H2.5641Z" fill="#343741"/> +<path d="M15.155 103.091L14.6642 105.413H16.4707L16.0426 107.433H14.2361L13.359 111.723C13.3102 112.016 13.3329 112.238 13.4268 112.387C13.5208 112.537 13.7558 112.617 14.1317 112.629C14.2779 112.635 14.5772 112.62 15.0297 112.586L14.7791 114.692C14.2013 114.847 13.5852 114.919 12.9308 114.908C11.8657 114.896 11.0687 114.637 10.5396 114.131C10.0105 113.625 9.79474 112.937 9.8922 112.068L10.8111 107.433H9.41187L9.82955 105.413H11.2288L11.7196 103.091H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 109C22.353 108.347 22.9324 107.818 23.6471 107.818H34.0001C34.7148 107.818 35.2942 108.347 35.2942 109C35.2942 109.653 34.7148 110.182 34.0001 110.182H23.6471C22.9324 110.182 22.353 109.653 22.353 109Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 109C43.5293 108.347 44.0999 107.818 44.8038 107.818L88.1371 107.818C88.841 107.818 89.4117 108.347 89.4117 109C89.4117 109.653 88.841 110.182 88.1371 110.182L44.8038 110.182C44.0999 110.182 43.5293 109.653 43.5293 109Z" fill="#343741"/> +<path d="M0 66.6C0 65.1641 1.14799 64 2.5641 64H97.4359C98.852 64 100 65.1641 100 66.6V87.4C100 88.8359 98.852 90 97.4359 90H2.5641C1.14799 90 0 88.8359 0 87.4V66.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 66.6H2.5641L2.5641 87.4H97.4359V66.6ZM2.5641 64C1.14799 64 0 65.1641 0 66.6V87.4C0 88.8359 1.14799 90 2.5641 90H97.4359C98.852 90 100 88.8359 100 87.4V66.6C100 65.1641 98.852 64 97.4359 64H2.5641Z" fill="#343741"/> +<path d="M15.155 69.9092L14.6642 72.4633H16.4707L16.0426 74.6852H14.2361L13.359 79.4042C13.3102 79.727 13.3329 79.9707 13.4268 80.1353C13.5208 80.2999 13.7558 80.3885 14.1317 80.4012C14.2779 80.4075 14.5772 80.3917 15.0297 80.3537L14.7791 82.6705C14.2013 82.8414 13.5852 82.9205 12.9308 82.9079C11.8657 82.8952 11.0687 82.6104 10.5396 82.0533C10.0105 81.4963 9.79474 80.7398 9.8922 79.784L10.8111 74.6852H9.41187L9.82955 72.4633H11.2288L11.7196 69.9092H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 76.9999C22.353 76.3472 22.9324 75.8181 23.6471 75.8181H34.0001C34.7148 75.8181 35.2942 76.3472 35.2942 76.9999C35.2942 77.6526 34.7148 78.1818 34.0001 78.1818H23.6471C22.9324 78.1818 22.353 77.6526 22.353 76.9999Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 76.9999C43.5293 76.3472 44.0999 75.8181 44.8038 75.8181L88.1371 75.8181C88.841 75.8181 89.4117 76.3472 89.4117 76.9999C89.4117 77.6526 88.841 78.1818 88.1371 78.1818L44.8038 78.1817C44.0999 78.1817 43.5293 77.6526 43.5293 76.9999Z" fill="#343741"/> +<path d="M0 34.6C0 33.1641 1.14799 32 2.5641 32H97.4359C98.852 32 100 33.1641 100 34.6V55.4C100 56.8359 98.852 58 97.4359 58H2.5641C1.14799 58 0 56.8359 0 55.4V34.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 34.6H2.5641L2.5641 55.4H97.4359V34.6ZM2.5641 32C1.14799 32 0 33.1641 0 34.6V55.4C0 56.8359 1.14799 58 2.5641 58H97.4359C98.852 58 100 56.8359 100 55.4V34.6C100 33.1641 98.852 32 97.4359 32H2.5641Z" fill="#343741"/> +<path d="M15.155 37.9092L14.6642 40.4633H16.4707L16.0426 42.6852H14.2361L13.359 47.4042C13.3102 47.727 13.3329 47.9707 13.4268 48.1353C13.5208 48.2999 13.7558 48.3885 14.1317 48.4012C14.2779 48.4075 14.5772 48.3917 15.0297 48.3537L14.7791 50.6705C14.2013 50.8414 13.5852 50.9205 12.9308 50.9079C11.8657 50.8952 11.0687 50.6104 10.5396 50.0533C10.0105 49.4963 9.79474 48.7398 9.8922 47.784L10.8111 42.6852H9.41187L9.82955 40.4633H11.2288L11.7196 37.9092H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 44.9999C22.353 44.3472 22.9324 43.8181 23.6471 43.8181H34.0001C34.7148 43.8181 35.2942 44.3472 35.2942 44.9999C35.2942 45.6526 34.7148 46.1818 34.0001 46.1818H23.6471C22.9324 46.1818 22.353 45.6526 22.353 44.9999Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 44.9999C43.5293 44.3472 44.0999 43.8181 44.8038 43.8181L88.1371 43.8181C88.841 43.8181 89.4117 44.3472 89.4117 44.9999C89.4117 45.6526 88.841 46.1818 88.1371 46.1818L44.8038 46.1817C44.0999 46.1817 43.5293 45.6526 43.5293 44.9999Z" fill="#343741"/> +<path d="M0 2.6C0 1.16406 1.14799 0 2.5641 0H97.4359C98.852 0 100 1.16406 100 2.6V23.4C100 24.8359 98.852 26 97.4359 26H2.5641C1.14799 26 0 24.8359 0 23.4V2.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 2.6H2.5641L2.5641 23.4H97.4359V2.6ZM2.5641 0C1.14799 0 0 1.16406 0 2.6V23.4C0 24.8359 1.14799 26 2.5641 26H97.4359C98.852 26 100 24.8359 100 23.4V2.6C100 1.16406 98.852 0 97.4359 0H2.5641Z" fill="#343741"/> +<path d="M15.155 5.90918L14.6642 8.46334H16.4707L16.0426 10.6852H14.2361L13.359 15.4042C13.3102 15.727 13.3329 15.9707 13.4268 16.1353C13.5208 16.2999 13.7558 16.3885 14.1317 16.4012C14.2779 16.4075 14.5772 16.3917 15.0297 16.3537L14.7791 18.6705C14.2013 18.8414 13.5852 18.9205 12.9308 18.9079C11.8657 18.8952 11.0687 18.6104 10.5396 18.0533C10.0105 17.4963 9.79474 16.7398 9.8922 15.784L10.8111 10.6852H9.41187L9.82955 8.46334H11.2288L11.7196 5.90918H15.155Z" fill="#4A7194"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 12.9999C22.353 12.3472 22.9324 11.8181 23.6471 11.8181H34.0001C34.7148 11.8181 35.2942 12.3472 35.2942 12.9999C35.2942 13.6526 34.7148 14.1818 34.0001 14.1818H23.6471C22.9324 14.1818 22.353 13.6526 22.353 12.9999Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 12.9999C43.5293 12.3472 44.0999 11.8181 44.8038 11.8181L88.1371 11.8181C88.841 11.8181 89.4117 12.3472 89.4117 12.9999C89.4117 13.6526 88.841 14.1818 88.1371 14.1818L44.8038 14.1817C44.0999 14.1817 43.5293 13.6526 43.5293 12.9999Z" fill="#343741"/> +<path d="M0 130.6C0 129.164 1.14799 128 2.5641 128H97.4359C98.852 128 100 129.164 100 130.6V151.4C100 152.836 98.852 154 97.4359 154H2.5641C1.14799 154 0 152.836 0 151.4V130.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 130.6H2.5641L2.5641 151.4H97.4359V130.6ZM2.5641 128C1.14799 128 0 129.164 0 130.6V151.4C0 152.836 1.14799 154 2.5641 154H97.4359C98.852 154 100 152.836 100 151.4V130.6C100 129.164 98.852 128 97.4359 128H2.5641Z" fill="#343741"/> +<path d="M12.6653 143.581H11.4987L10.4188 146.909H8.83443L9.91433 143.581H8.23535L8.48759 142.096H10.3873L11.0888 139.953H9.41773L9.67786 138.468H11.5697L12.6653 135.091H14.2419L13.1462 138.468H14.3207L15.4164 135.091H17.0007L15.9051 138.468H17.6471L17.3949 139.953H15.4321L14.7306 142.096H16.4569L16.2046 143.581H14.2497L13.1777 146.909H11.5933L12.6653 143.581ZM11.9717 142.096H13.1383L13.8477 139.953H12.6732L11.9717 142.096Z" fill="#387765"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 141C22.353 140.347 22.9324 139.818 23.6471 139.818H34.0001C34.7148 139.818 35.2942 140.347 35.2942 141C35.2942 141.653 34.7148 142.182 34.0001 142.182H23.6471C22.9324 142.182 22.353 141.653 22.353 141Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 141C43.5293 140.347 44.0999 139.818 44.8038 139.818L88.1371 139.818C88.841 139.818 89.4117 140.347 89.4117 141C89.4117 141.653 88.841 142.182 88.1371 142.182L44.8038 142.182C44.0999 142.182 43.5293 141.653 43.5293 141Z" fill="#343741"/> +<path d="M0 162.6C0 161.164 1.14799 160 2.5641 160H97.4359C98.852 160 100 161.164 100 162.6V183.4C100 184.836 98.852 186 97.4359 186H2.5641C1.14799 186 0 184.836 0 183.4V162.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M97.4359 162.6H2.5641L2.5641 183.4H97.4359V162.6ZM2.5641 160C1.14799 160 0 161.164 0 162.6V183.4C0 184.836 1.14799 186 2.5641 186H97.4359C98.852 186 100 184.836 100 183.4V162.6C100 161.164 98.852 160 97.4359 160H2.5641Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M18.8236 178.398C18.8236 179.333 18.1451 180.091 17.3066 180.091H9.75239C8.91455 180.091 8.23535 179.334 8.23535 178.398V169.966C8.23535 169.031 8.9138 168.273 9.75239 168.273H11.4118V167.687C11.4118 167.395 11.6059 167.151 11.8483 167.101L11.9412 167.091C12.2336 167.091 12.4706 167.344 12.4706 167.687V168.273H14.5883V167.687C14.5883 167.395 14.7823 167.151 15.0248 167.101L15.1177 167.091C15.4101 167.091 15.6471 167.344 15.6471 167.687V168.273H17.3066C18.1444 168.273 18.8236 169.03 18.8236 169.966V178.398ZM9.29418 171.818V178.117C9.29418 178.555 9.61176 178.909 10.0042 178.909H17.0547C17.4473 178.909 17.7648 178.555 17.7648 178.117V171.818H9.29418ZM10.8824 176.545C11.1423 176.545 11.3585 176.762 11.4033 177.033L11.4118 177.136C11.4118 177.426 11.2178 177.668 10.9753 177.718L10.8824 177.727C10.59 177.727 10.353 177.453 10.353 177.136C10.353 176.846 10.547 176.605 10.7895 176.555L10.8824 176.545ZM13.5295 176.545C13.7894 176.545 14.0055 176.762 14.0504 177.033L14.0589 177.136C14.0589 177.426 13.8648 177.668 13.6224 177.718L13.5295 177.727C13.2371 177.727 13.0001 177.453 13.0001 177.136C13.0001 176.846 13.1941 176.605 13.4366 176.555L13.5295 176.545ZM16.1765 176.545C16.4364 176.545 16.6526 176.762 16.6974 177.033L16.7059 177.136C16.7059 177.426 16.5119 177.668 16.2694 177.718L16.1765 177.727C15.8841 177.727 15.6471 177.453 15.6471 177.136C15.6471 176.846 15.8412 176.605 16.0836 176.555L16.1765 176.545ZM10.8824 174.773C11.1423 174.773 11.3585 174.989 11.4033 175.26L11.4118 175.364C11.4118 175.654 11.2178 175.895 10.9753 175.945L10.8824 175.955C10.59 175.955 10.353 175.68 10.353 175.364C10.353 175.074 10.547 174.832 10.7895 174.782L10.8824 174.773ZM13.5295 174.773C13.7894 174.773 14.0055 174.989 14.0504 175.26L14.0589 175.364C14.0589 175.654 13.8648 175.895 13.6224 175.945L13.5295 175.955C13.2371 175.955 13.0001 175.68 13.0001 175.364C13.0001 175.074 13.1941 174.832 13.4366 174.782L13.5295 174.773ZM16.1765 174.773C16.4364 174.773 16.6526 174.989 16.6974 175.26L16.7059 175.364C16.7059 175.654 16.5119 175.895 16.2694 175.945L16.1765 175.955C15.8841 175.955 15.6471 175.68 15.6471 175.364C15.6471 175.074 15.8412 174.832 16.0836 174.782L16.1765 174.773ZM10.8824 173C11.1423 173 11.3585 173.217 11.4033 173.487L11.4118 173.591C11.4118 173.881 11.2178 174.122 10.9753 174.172L10.8824 174.182C10.59 174.182 10.353 173.908 10.353 173.591C10.353 173.301 10.547 173.06 10.7895 173.01L10.8824 173ZM13.5295 173C13.7894 173 14.0055 173.217 14.0504 173.487L14.0589 173.591C14.0589 173.881 13.8648 174.122 13.6224 174.172L13.5295 174.182C13.2371 174.182 13.0001 173.908 13.0001 173.591C13.0001 173.301 13.1941 173.06 13.4366 173.01L13.5295 173ZM16.1765 173C16.4364 173 16.6526 173.217 16.6974 173.487L16.7059 173.591C16.7059 173.881 16.5119 174.122 16.2694 174.172L16.1765 174.182C15.8841 174.182 15.6471 173.908 15.6471 173.591C15.6471 173.301 15.8412 173.06 16.0836 173.01L16.1765 173ZM9.29418 170.636H17.7648V170.247C17.7648 169.809 17.4472 169.455 17.0547 169.455H10.0042C9.61166 169.455 9.29418 169.809 9.29418 170.247V170.636Z" fill="#7B705A"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.353 173C22.353 172.347 22.9324 171.818 23.6471 171.818H34.0001C34.7148 171.818 35.2942 172.347 35.2942 173C35.2942 173.653 34.7148 174.182 34.0001 174.182H23.6471C22.9324 174.182 22.353 173.653 22.353 173Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M43.5293 173C43.5293 172.347 44.0999 171.818 44.8038 171.818L88.1371 171.818C88.841 171.818 89.4117 172.347 89.4117 173C89.4117 173.653 88.841 174.182 88.1371 174.182L44.8038 174.182C44.0999 174.182 43.5293 173.653 43.5293 173Z" fill="#343741"/> </g> -<defs> -<radialGradient id="paint0_diamond_169_44976" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(226 110) rotate(90) scale(264.688)"> -<stop stop-color="#EFF7FF"/> -<stop offset="1" stop-color="#DBEDFF"/> -</radialGradient> -</defs> +<g opacity="0.5"> +<path d="M143 66.6C143 65.1641 144.148 64 145.564 64H240.436C241.852 64 243 65.1641 243 66.6V87.4C243 88.8359 241.852 90 240.436 90H145.564C144.148 90 143 88.8359 143 87.4V66.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 66.6H145.564L145.564 87.4H240.436V66.6ZM145.564 64C144.148 64 143 65.1641 143 66.6V87.4C143 88.8359 144.148 90 145.564 90H240.436C241.852 90 243 88.8359 243 87.4V66.6C243 65.1641 241.852 64 240.436 64H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 77C220 76.4477 220.149 76 220.333 76L231.667 76C231.851 76 232 76.4477 232 77C232 77.5523 231.851 78 231.667 78L220.333 78C220.149 78 220 77.5523 220 77Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 71C226.552 71 227 71.1492 227 71.3333L227 82.6667C227 82.8508 226.552 83 226 83C225.448 83 225 82.8508 225 82.6667L225 71.3333C225 71.1492 225.448 71 226 71Z" fill="#343741"/> +<path d="M143 98.6C143 97.1641 144.148 96 145.564 96H240.436C241.852 96 243 97.1641 243 98.6V119.4C243 120.836 241.852 122 240.436 122H145.564C144.148 122 143 120.836 143 119.4V98.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 98.6H145.564L145.564 119.4H240.436V98.6ZM145.564 96C144.148 96 143 97.1641 143 98.6V119.4C143 120.836 144.148 122 145.564 122H240.436C241.852 122 243 120.836 243 119.4V98.6C243 97.1641 241.852 96 240.436 96H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 109C220 108.448 220.149 108 220.333 108L231.667 108C231.851 108 232 108.448 232 109C232 109.552 231.851 110 231.667 110L220.333 110C220.149 110 220 109.552 220 109Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 103C226.552 103 227 103.149 227 103.333L227 114.667C227 114.851 226.552 115 226 115C225.448 115 225 114.851 225 114.667L225 103.333C225 103.149 225.448 103 226 103Z" fill="#343741"/> +<path d="M143 130.6C143 129.164 144.148 128 145.564 128H240.436C241.852 128 243 129.164 243 130.6V151.4C243 152.836 241.852 154 240.436 154H145.564C144.148 154 143 152.836 143 151.4V130.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 130.6H145.564L145.564 151.4H240.436V130.6ZM145.564 128C144.148 128 143 129.164 143 130.6V151.4C143 152.836 144.148 154 145.564 154H240.436C241.852 154 243 152.836 243 151.4V130.6C243 129.164 241.852 128 240.436 128H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 141C220 140.448 220.149 140 220.333 140L231.667 140C231.851 140 232 140.448 232 141C232 141.552 231.851 142 231.667 142L220.333 142C220.149 142 220 141.552 220 141Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 135C226.552 135 227 135.149 227 135.333L227 146.667C227 146.851 226.552 147 226 147C225.448 147 225 146.851 225 146.667L225 135.333C225 135.149 225.448 135 226 135Z" fill="#343741"/> +<path d="M143 162.6C143 161.164 144.148 160 145.564 160H240.436C241.852 160 243 161.164 243 162.6V183.4C243 184.836 241.852 186 240.436 186H145.564C144.148 186 143 184.836 143 183.4V162.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 162.6H145.564L145.564 183.4H240.436V162.6ZM145.564 160C144.148 160 143 161.164 143 162.6V183.4C143 184.836 144.148 186 145.564 186H240.436C241.852 186 243 184.836 243 183.4V162.6C243 161.164 241.852 160 240.436 160H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 173C220 172.448 220.149 172 220.333 172L231.667 172C231.851 172 232 172.448 232 173C232 173.552 231.851 174 231.667 174L220.333 174C220.149 174 220 173.552 220 173Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 167C226.552 167 227 167.149 227 167.333L227 178.667C227 178.851 226.552 179 226 179C225.448 179 225 178.851 225 178.667L225 167.333C225 167.149 225.448 167 226 167Z" fill="#343741"/> +<path d="M143 34.6C143 33.1641 144.148 32 145.564 32H240.436C241.852 32 243 33.1641 243 34.6V55.4C243 56.8359 241.852 58 240.436 58H145.564C144.148 58 143 56.8359 143 55.4V34.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 34.6H145.564L145.564 55.4H240.436V34.6ZM145.564 32C144.148 32 143 33.1641 143 34.6V55.4C143 56.8359 144.148 58 145.564 58H240.436C241.852 58 243 56.8359 243 55.4V34.6C243 33.1641 241.852 32 240.436 32H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 45C220 44.4477 220.149 44 220.333 44L231.667 44C231.851 44 232 44.4477 232 45C232 45.5523 231.851 46 231.667 46L220.333 46C220.149 46 220 45.5523 220 45Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 39C226.552 39 227 39.1492 227 39.3333L227 50.6667C227 50.8508 226.552 51 226 51C225.448 51 225 50.8508 225 50.6667L225 39.3333C225 39.1492 225.448 39 226 39Z" fill="#343741"/> +<path d="M143 2.6C143 1.16406 144.148 0 145.564 0H240.436C241.852 0 243 1.16406 243 2.6V23.4C243 24.8359 241.852 26 240.436 26H145.564C144.148 26 143 24.8359 143 23.4V2.6Z" fill="#EFF4F9"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M240.436 2.6H145.564L145.564 23.4H240.436V2.6ZM145.564 0C144.148 0 143 1.16406 143 2.6V23.4C143 24.8359 144.148 26 145.564 26H240.436C241.852 26 243 24.8359 243 23.4V2.6C243 1.16406 241.852 0 240.436 0H145.564Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M220 13C220 12.4477 220.149 12 220.333 12L231.667 12C231.851 12 232 12.4477 232 13C232 13.5523 231.851 14 231.667 14L220.333 14C220.149 14 220 13.5523 220 13Z" fill="#343741"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M226 7C226.552 7 227 7.14924 227 7.33333L227 18.6667C227 18.8508 226.552 19 226 19C225.448 19 225 18.8508 225 18.6667L225 7.33333C225 7.14924 225.448 7 226 7Z" fill="#343741"/> +</g> +<line opacity="0.5" x1="121.5" y1="1.5" x2="121.5" y2="184.5" stroke="#343741" stroke-width="3" stroke-linecap="round"/> </svg> diff --git a/src/plugins/wizard/public/assets/hand_field.svg b/src/plugins/wizard/public/assets/hand_field.svg index 753a3af354c0..8c38e60edd59 100644 --- a/src/plugins/wizard/public/assets/hand_field.svg +++ b/src/plugins/wizard/public/assets/hand_field.svg @@ -1,44 +1,44 @@ <svg width="206" height="68" viewBox="0 0 206 68" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g filter="url(#filter0_d_169_44976)"> +<g filter="url(#filter0_d_268_49515)"> <rect x="2" y="2" width="176" height="40" rx="2" fill="#EFF4F9" stroke="#343741" stroke-width="4"/> <path d="M23.6411 10L22.8172 14.7154H25.8498L25.1311 18.8172H22.0985L20.6261 27.5293C20.5443 28.1253 20.5822 28.5752 20.74 28.879C20.8978 29.1829 21.2922 29.3465 21.9232 29.3699C22.1686 29.3815 22.6711 29.3523 23.4308 29.2822L23.01 33.5593C22.0401 33.8749 21.0059 34.021 19.9074 33.9976C18.1194 33.9742 16.7813 33.4483 15.8932 32.4199C15.005 31.3916 14.6427 29.9951 14.8063 28.2304L16.3489 18.8172H14L14.7012 14.7154H17.0501L17.874 10H23.6411Z" fill="#4A7194"/> <line x1="37" y1="22" x2="53" y2="22" stroke="#343741" stroke-width="4" stroke-linecap="round"/> <line x1="69" y1="22" x2="162" y2="22" stroke="#343741" stroke-width="4" stroke-linecap="round"/> </g> -<g filter="url(#filter1_d_169_44976)"> +<g filter="url(#filter1_d_268_49515)"> <path fill-rule="evenodd" clip-rule="evenodd" d="M192.347 61.1429L189.898 56.2449L187.449 61.1429H177.653V58.6045C177.653 56.5994 169.816 51.0126 169.816 51.0126C168.734 50.2086 167.857 48.4761 167.857 47.1112V38.2857C167.857 35.4807 170.05 33.2064 172.755 33.2064V40.8254H175.204V28.974C175.204 27.8045 176.118 26.8572 177.245 26.8572C179.5 26.8572 181.327 28.7518 181.327 31.0896V30.2438C181.327 29.0743 182.24 28.127 183.368 28.127C185.622 28.127 187.449 30.0216 187.449 32.3594V31.5137C187.449 30.3442 188.362 29.3969 189.49 29.3062C191.745 29.3969 193.571 31.2915 193.571 33.6292V32.7835C193.571 31.614 194.485 30.6667 195.613 30.6667C197.867 30.6667 199.694 32.5613 199.694 34.8991V46.4596C199.694 47.8063 199.113 49.8316 198.407 50.9975C198.407 50.9975 194.796 56.5994 194.796 58.6045V61.1429H192.347ZM182.551 41.551H185V51.3469H182.551V41.551ZM187.449 41.551H189.898V51.3469H187.449V41.551ZM192.347 41.551H194.796V51.3469H192.347V41.551Z" fill="white"/> </g> -<g filter="url(#filter2_d_169_44976)"> +<g filter="url(#filter2_d_268_49515)"> <path fill-rule="evenodd" clip-rule="evenodd" d="M189.898 56.2449L192.347 61.1429H194.796V58.6044C194.796 57.6331 195.643 55.8178 196.517 54.2041C196.916 53.4667 197.321 52.7714 197.653 52.218C198.094 51.4824 198.407 50.9974 198.407 50.9974C199.113 49.8315 199.694 47.8062 199.694 46.4595V34.899C199.694 32.6255 197.966 30.7711 195.797 30.6709C195.736 30.6681 195.675 30.6667 195.613 30.6667C194.485 30.6667 193.571 31.614 193.571 32.7835V33.6292C193.571 33.3396 193.543 33.0569 193.49 32.7835C193.258 31.5969 192.548 30.587 191.578 29.9655C191.005 29.5985 190.342 29.3669 189.632 29.3142C189.585 29.3107 189.538 29.308 189.49 29.3061C188.362 29.3968 187.449 30.3441 187.449 31.5137V32.3594C187.449 32.0697 187.421 31.7869 187.368 31.5137C187.134 30.3206 186.417 29.3099 185.438 28.7111C184.88 28.3697 184.237 28.1622 183.549 28.1311C183.489 28.1284 183.429 28.127 183.368 28.127C182.24 28.127 181.327 29.0743 181.327 30.2438V31.0895C181.327 30.7999 181.298 30.5171 181.245 30.2438C181.012 29.0529 180.297 28.0436 179.321 27.4444C178.713 27.0713 178.003 26.8571 177.245 26.8571C176.118 26.8571 175.204 27.8044 175.204 28.974V40.8254H172.755V33.2063C172.618 33.2063 172.482 33.2122 172.347 33.2237C171.438 33.3016 170.598 33.6371 169.898 34.1597C168.662 35.0818 167.857 36.5864 167.857 38.2857V47.1112C167.857 48.4761 168.734 50.2086 169.816 51.0125C169.816 51.0125 177.653 56.5994 177.653 58.6044V61.1429H187.449L189.898 56.2449ZM187.449 41.551V51.3469H189.898V41.551H187.449ZM185 41.551H182.551V51.3469H185V41.551ZM192.347 41.551V51.3469H194.796V41.551H192.347ZM168.157 53.3389L168.134 53.3226L168.112 53.3057C167.123 52.5713 166.376 51.5446 165.879 50.5568C165.381 49.5683 165 48.3521 165 47.1112V38.2857C165 34.1356 168.167 30.5784 172.347 30.3598V28.974C172.347 26.3253 174.443 24 177.245 24C178.93 24 180.445 24.6015 181.627 25.5894C182.163 25.3836 182.748 25.2698 183.368 25.2698C185.037 25.2698 186.541 25.8607 187.718 26.833C188.201 26.6309 188.721 26.5017 189.261 26.4582L189.433 26.4444L189.605 26.4513C191.236 26.5169 192.72 27.1363 193.884 28.1243C194.417 27.9215 194.998 27.8095 195.613 27.8095C199.542 27.8095 202.551 31.0821 202.551 34.899V46.4595C202.551 47.484 202.341 48.6031 202.063 49.5784C201.784 50.5573 201.376 51.6108 200.851 52.477L200.83 52.5115M200.809 52.5451L200.802 52.5553L200.776 52.5959L200.671 52.7625C200.578 52.9094 200.445 53.1241 200.284 53.389C199.96 53.921 199.532 54.6442 199.108 55.4197C198.679 56.2043 198.28 56.9962 197.995 57.6751C197.777 58.1941 197.698 58.4841 197.669 58.5894C197.654 58.6466 197.653 58.6493 197.653 58.6044V64H190.581L189.898 62.6337L189.215 64H174.796V59.0728C174.695 58.933 174.54 58.7411 174.317 58.4975C173.708 57.8293 172.84 57.037 171.896 56.2435C170.967 55.4622 170.029 54.7334 169.318 54.1965C168.964 53.9292 168.67 53.7121 168.466 53.5629C168.364 53.4884 168.285 53.431 168.232 53.3929L168.174 53.3506L168.157 53.3389M174.933 59.2893C174.932 59.289 174.924 59.2751 174.912 59.2486C174.928 59.2764 174.934 59.2896 174.933 59.2893Z" fill="#343741"/> </g> <defs> -<filter id="filter0_d_169_44976" x="0" y="0" width="188" height="52" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<filter id="filter0_d_268_49515" x="0" y="0" width="188" height="52" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix"/> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> <feOffset dx="4" dy="4"/> <feGaussianBlur stdDeviation="2"/> <feComposite in2="hardAlpha" operator="out"/> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/> -<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_169_44976"/> -<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_169_44976" result="shape"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> </filter> -<filter id="filter1_d_169_44976" x="164.857" y="24.8572" width="37.8367" height="40.2857" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<filter id="filter1_d_268_49515" x="164.857" y="24.8572" width="37.8367" height="40.2856" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix"/> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> <feOffset dy="1"/> <feGaussianBlur stdDeviation="1.5"/> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> -<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_169_44976"/> -<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_169_44976" result="shape"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> </filter> -<filter id="filter2_d_169_44976" x="162" y="22" width="43.551" height="46" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<filter id="filter2_d_268_49515" x="162" y="22" width="43.551" height="46" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix"/> <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> <feOffset dy="1"/> <feGaussianBlur stdDeviation="1.5"/> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> -<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_169_44976"/> -<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_169_44976" result="shape"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> </filter> </defs> </svg> diff --git a/src/plugins/wizard/public/assets/wizard_icon_secondary_fill.svg b/src/plugins/wizard/public/assets/wizard_icon_secondary_fill.svg new file mode 100644 index 000000000000..cdaad42f6276 --- /dev/null +++ b/src/plugins/wizard/public/assets/wizard_icon_secondary_fill.svg @@ -0,0 +1,25 @@ +<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10 3C10 2.44771 10.4477 2 11 2H29C29.5523 2 30 2.44772 30 3V12C30 12.5523 29.5523 13 29 13H11C10.4477 13 10 12.5523 10 12V3Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M29 3H11V12H29V3ZM11 2C10.4477 2 10 2.44771 10 3V12C10 12.5523 10.4477 13 11 13H29C29.5523 13 30 12.5523 30 12V3C30 2.44772 29.5523 2 29 2H11Z" fill="#017D73"/> +<path d="M2 10C2 9.44771 2.44772 9 3 9H21C21.5523 9 22 9.44772 22 10V19C22 19.5523 21.5523 20 21 20H3C2.44772 20 2 19.5523 2 19V10Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M21 10H3V19H21V10ZM3 9C2.44772 9 2 9.44771 2 10V19C2 19.5523 2.44772 20 3 20H21C21.5523 20 22 19.5523 22 19V10C22 9.44772 21.5523 9 21 9H3Z" fill="#017D73"/> +<g filter="url(#filter0_d_201_310)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4285 29L22.5714 27.2857L21.7143 29H18.2857V28.1116C18.2857 27.4098 15.5427 25.4544 15.5427 25.4544C15.1641 25.173 14.8571 24.5666 14.8571 24.0889V21C14.8571 20.0182 15.6247 19.2222 16.5714 19.2222V21.8889H17.4285V17.7409C17.4285 17.3316 17.7483 17 18.143 17C18.932 17 19.5714 17.6631 19.5714 18.4813V18.1853C19.5714 17.776 19.8911 17.4444 20.2858 17.4444C21.0748 17.4444 21.7143 18.1076 21.7143 18.9258V18.6298C21.7143 18.2204 22.034 17.8889 22.4287 17.8571C23.2177 17.8889 23.8571 18.552 23.8571 19.3702V19.0742C23.8571 18.6649 24.1768 18.3333 24.5715 18.3333C25.3605 18.3333 26 18.9964 26 19.8147V23.8608C26 24.3322 25.7966 25.041 25.5496 25.4491C25.5496 25.4491 24.2857 27.4098 24.2857 28.1116V29H23.4285ZM20 22.1428H20.8571V25.5714H20V22.1428V22.1428ZM21.7142 22.1428H22.5714V25.5714H21.7142V22.1428V22.1428ZM23.4286 22.1428H24.2857V25.5714H23.4286V22.1428V22.1428Z" fill="white"/> +</g> +<path fill-rule="evenodd" clip-rule="evenodd" d="M22.5714 27.2857L23.4285 29H24.2857V28.1116C24.2857 27.7716 24.5823 27.1362 24.8881 26.5714C25.0278 26.3133 25.1695 26.07 25.2857 25.8763C25.4402 25.6188 25.5496 25.4491 25.5496 25.4491C25.7966 25.041 26 24.3322 26 23.8608V19.8147C26 19.0189 25.3952 18.3699 24.6362 18.3348C24.6148 18.3338 24.5932 18.3333 24.5715 18.3333C24.1768 18.3333 23.8571 18.6649 23.8571 19.0742V19.3702C23.8571 19.2689 23.8473 19.1699 23.8286 19.0742C23.7475 18.6589 23.4991 18.3055 23.1595 18.0879C22.959 17.9595 22.7267 17.8784 22.4782 17.86C22.4618 17.8588 22.4453 17.8578 22.4287 17.8571C22.034 17.8889 21.7143 18.2204 21.7143 18.6298V18.9258C21.7143 18.8244 21.7044 18.7254 21.6857 18.6298C21.6041 18.2122 21.3532 17.8585 21.0104 17.6489C20.8151 17.5294 20.5899 17.4568 20.3493 17.4459C20.3283 17.4449 20.3071 17.4444 20.2858 17.4444C19.8911 17.4444 19.5714 17.776 19.5714 18.1853V18.4813C19.5714 18.38 19.5616 18.281 19.5429 18.1853C19.4614 17.7685 19.2112 17.4153 18.8694 17.2055C18.6566 17.0749 18.4082 17 18.143 17C17.7483 17 17.4285 17.3316 17.4285 17.7409V21.8889H16.5714V19.2222C16.5233 19.2222 16.4756 19.2243 16.4285 19.2283C16.1102 19.2555 15.8166 19.373 15.5714 19.5559C15.1388 19.8786 14.8571 20.4052 14.8571 21V24.0889C14.8571 24.5666 15.1641 25.173 15.5427 25.4544C15.5427 25.4544 18.2857 27.4098 18.2857 28.1116V29H21.7143L22.5714 27.2857ZM21.7142 22.1428V25.5714H22.5714V22.1428H21.7142ZM20.8571 22.1428H20V25.5714H20.8571V22.1428ZM23.4286 22.1428V25.5714H24.2857V22.1428H23.4286ZM14.9622 26.2686L14.9542 26.2629L14.9462 26.257C14.6003 25.9999 14.3387 25.6406 14.1647 25.2949C13.9905 24.9489 13.8571 24.5232 13.8571 24.0889V21C13.8571 19.5475 14.9657 18.3024 16.4285 18.2259V17.7409C16.4285 16.8139 17.162 16 18.143 16C18.7325 16 19.2629 16.2105 19.6765 16.5563C19.864 16.4843 20.0689 16.4444 20.2858 16.4444C20.8702 16.4444 21.3963 16.6512 21.8083 16.9915C21.9775 16.9208 22.1593 16.8756 22.3485 16.8604L22.4086 16.8555L22.4689 16.858C23.0398 16.8809 23.5592 17.0977 23.9666 17.4435C24.1529 17.3725 24.3563 17.3333 24.5715 17.3333C25.9468 17.3333 27 18.4787 27 19.8147V23.8608C27 24.2194 26.9264 24.6111 26.8292 24.9524C26.7317 25.2951 26.5886 25.6638 26.405 25.967L26.3977 25.979M26.3901 25.9908L26.3879 25.9943L26.3788 26.0086L26.3419 26.0669C26.3096 26.1183 26.2628 26.1934 26.2064 26.2861C26.0931 26.4724 25.9435 26.7255 25.795 26.9969C25.6449 27.2715 25.505 27.5487 25.4053 27.7863C25.3291 27.9679 25.3015 28.0694 25.2914 28.1063C25.2859 28.1263 25.2857 28.1272 25.2857 28.1116V30H22.8105L22.5714 29.5218L22.3323 30H17.2857V28.2755C17.2502 28.2266 17.1959 28.1594 17.1182 28.0741C16.9049 27.8403 16.601 27.563 16.2706 27.2852C15.9454 27.0118 15.6172 26.7567 15.3685 26.5688C15.2446 26.4752 15.1417 26.3992 15.0703 26.347C15.0347 26.3209 15.0069 26.3008 14.9885 26.2875L14.9679 26.2727L14.9622 26.2686M17.3337 28.3513C17.3337 28.3513 17.3305 28.3463 17.3265 28.337C17.3321 28.3467 17.3337 28.3513 17.3337 28.3513Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M9 15H5V14H9V15Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M17 8H13V7H17V8Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M20 15H11V14H20V15Z" fill="#017D73"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M28 8H19V7H28V8Z" fill="#017D73"/> +<defs> +<filter id="filter0_d_201_310" x="11.8571" y="15" width="17.1429" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_201_310"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_201_310" result="shape"/> +</filter> +</defs> +</svg> diff --git a/src/plugins/wizard/public/embeddable/wizard_component.tsx b/src/plugins/wizard/public/embeddable/wizard_component.tsx index 58087c83ffda..675baf28796a 100644 --- a/src/plugins/wizard/public/embeddable/wizard_component.tsx +++ b/src/plugins/wizard/public/embeddable/wizard_component.tsx @@ -3,11 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { SavedObjectEmbeddableInput, withEmbeddableSubscription } from '../../../embeddable/public'; import { WizardEmbeddable, WizardOutput } from './wizard_embeddable'; -import { validateSchemaState } from '../application/utils/validate_schema_state'; +import { getReactExpressionRenderer } from '../plugin_services'; interface Props { embeddable: WizardEmbeddable; @@ -15,54 +15,20 @@ interface Props { output: WizardOutput; } -function WizardEmbeddableComponentInner({ - embeddable, - input: {}, - output: { savedAttributes }, -}: Props) { - const { ReactExpressionRenderer, toasts, types, indexPatterns, aggs } = embeddable; - const [expression, setExpression] = useState<string>(); - - useEffect(() => { - const { visualizationState: visualization, styleState: style } = savedAttributes || {}; - if (savedAttributes === undefined || visualization === undefined || style === undefined) { - return; - } - - const rootState = { - visualization: JSON.parse(visualization), - style: JSON.parse(style), - }; - - const visualizationType = types.get(rootState.visualization?.activeVisualization?.name ?? ''); - if (!visualizationType) { - throw new Error(`Invalid visualization type ${visualizationType}`); - } - const { toExpression, ui } = visualizationType; - - async function loadExpression() { - const schemas = ui.containerConfig.data.schemas; - const [valid, errorMsg] = validateSchemaState(schemas, rootState); - - if (!valid) { - if (errorMsg) { - toasts.addWarning(errorMsg); - } - setExpression(undefined); - return; - } - const exp = await toExpression(rootState, indexPatterns, aggs); - setExpression(exp); - } - - if (savedAttributes !== undefined) { - loadExpression(); - } - }, [aggs, indexPatterns, savedAttributes, toasts, types]); - - return <ReactExpressionRenderer expression={expression ?? ''} />; - - // TODO: add correct loading and error states +function WizardEmbeddableComponentInner({ embeddable, input: {}, output: { error } }: Props) { + const { expression } = embeddable; + const ReactExpressionRenderer = getReactExpressionRenderer(); + + return ( + <> + {error?.message ? ( + // TODO: add correct loading and error states + <div>{error.message}</div> + ) : ( + <ReactExpressionRenderer expression={expression ?? ''} /> + )} + </> + ); } export const WizardEmbeddableComponent = withEmbeddableSubscription< diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx index 12449674ce6c..bc2d4548dda1 100644 --- a/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx @@ -3,125 +3,289 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import { cloneDeep, isEqual } from 'lodash'; import ReactDOM from 'react-dom'; -import { Subscription } from 'rxjs'; +import { merge, Subscription } from 'rxjs'; -import { WizardSavedObjectAttributes } from '../../common'; +import { PLUGIN_ID, WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from '../../common'; import { Embeddable, EmbeddableOutput, + ErrorEmbeddable, IContainer, SavedObjectEmbeddableInput, } from '../../../embeddable/public'; -import { IToasts, SavedObjectsClientContract } from '../../../../core/public'; -import { WizardEmbeddableComponent } from './wizard_component'; -import { ReactExpressionRendererType } from '../../../expressions/public'; -import { TypeServiceStart } from '../services/type_service'; -import { DataPublicPluginStart } from '../../../data/public'; +import { + ExpressionRenderError, + ExpressionsStart, + IExpressionLoaderParams, +} from '../../../expressions/public'; +import { + Filter, + opensearchFilters, + Query, + TimefilterContract, + TimeRange, +} from '../../../data/public'; +import { validateSchemaState } from '../application/utils/validate_schema_state'; +import { getExpressionLoader, getTypeService } from '../plugin_services'; -export const WIZARD_EMBEDDABLE = 'WIZARD_EMBEDDABLE'; +// Apparently this needs to match the saved object type for the clone and replace panel actions to work +export const WIZARD_EMBEDDABLE = WIZARD_SAVED_OBJECT; + +export interface WizardEmbeddableConfiguration { + savedWizard: WizardSavedObjectAttributes; + // TODO: add indexPatterns as part of configuration + // indexPatterns?: IIndexPattern[]; + editPath: string; + editUrl: string; + editable: boolean; +} export interface WizardOutput extends EmbeddableOutput { /** * Will contain the saved object attributes of the Wizard Saved Object that matches * `input.savedObjectId`. If the id is invalid, this may be undefined. */ - savedAttributes?: WizardSavedObjectAttributes; + savedWizard?: WizardSavedObjectAttributes; } +type ExpressionLoader = InstanceType<ExpressionsStart['ExpressionLoader']>; + export class WizardEmbeddable extends Embeddable<SavedObjectEmbeddableInput, WizardOutput> { public readonly type = WIZARD_EMBEDDABLE; - private subscription: Subscription; + private handler?: ExpressionLoader; + private timeRange?: TimeRange; + private query?: Query; + private filters?: Filter[]; + private abortController?: AbortController; + public expression: string = ''; + private autoRefreshFetchSubscription: Subscription; + private subscriptions: Subscription[] = []; private node?: HTMLElement; - private savedObjectsClient: SavedObjectsClientContract; - public ReactExpressionRenderer: ReactExpressionRendererType; - public toasts: IToasts; - public types: TypeServiceStart; - public indexPatterns: DataPublicPluginStart['indexPatterns']; - public aggs: DataPublicPluginStart['search']['aggs']; - private savedObjectId?: string; + private savedWizard?: WizardSavedObjectAttributes; + private serializedState?: { visualization: string; style: string }; constructor( + timefilter: TimefilterContract, + { savedWizard, editPath, editUrl, editable }: WizardEmbeddableConfiguration, initialInput: SavedObjectEmbeddableInput, { parent, - savedObjectsClient, - data, - ReactExpressionRenderer, - toasts, - types, }: { parent?: IContainer; - data: DataPublicPluginStart; - savedObjectsClient: SavedObjectsClientContract; - ReactExpressionRenderer: ReactExpressionRendererType; - toasts: IToasts; - types: TypeServiceStart; } ) { - // TODO: can default title come from saved object? - super(initialInput, { defaultTitle: 'wizard' }, parent); - this.savedObjectsClient = savedObjectsClient; - this.ReactExpressionRenderer = ReactExpressionRenderer; - this.toasts = toasts; - this.types = types; - this.indexPatterns = data.indexPatterns; - this.aggs = data.search.aggs; - - this.subscription = this.getInput$().subscribe(async () => { - // There is a little more work today for this embeddable because it has - // more output it needs to update in response to input state changes. - let savedAttributes: WizardSavedObjectAttributes | undefined; - - // Since this is an expensive task, we save a local copy of the previous - // savedObjectId locally and only retrieve the new saved object if the id - // actually changed. - if (this.savedObjectId !== this.input.savedObjectId) { - this.savedObjectId = this.input.savedObjectId; - const wizardSavedObject = await this.savedObjectsClient.get<WizardSavedObjectAttributes>( - 'wizard', - this.input.savedObjectId - ); - savedAttributes = wizardSavedObject?.attributes; + super( + initialInput, + { + defaultTitle: savedWizard.title, + editPath, + editApp: PLUGIN_ID, + editUrl, + editable, + savedWizard, + }, + parent + ); + + this.savedWizard = savedWizard; + + this.autoRefreshFetchSubscription = timefilter + .getAutoRefreshFetch$() + .subscribe(this.updateHandler.bind(this)); + + this.subscriptions.push( + merge(this.getOutput$(), this.getInput$()).subscribe(() => { + this.handleChanges(); + }) + ); + } + + private getSerializedState = () => { + const { visualizationState: visualization = '{}', styleState: style = '{}' } = + this.savedWizard || {}; + return { + visualization, + style, + }; + }; + + private getExpression = async () => { + if (!this.serializedState) { + return; + } + const { visualization, style } = this.serializedState; + const rootState = { + visualization: JSON.parse(visualization), + style: JSON.parse(style), + }; + const visualizationName = rootState.visualization?.activeVisualization?.name ?? ''; + const visualizationType = getTypeService().get(visualizationName); + if (!visualizationType) { + this.onContainerError(new Error(`Invalid visualization type ${visualizationName}`)); + return; + } + const { toExpression, ui } = visualizationType; + const schemas = ui.containerConfig.data.schemas; + const [valid, errorMsg] = validateSchemaState(schemas, rootState); + + if (!valid) { + if (errorMsg) { + this.onContainerError(new Error(errorMsg)); + return; } + } else { + // TODO: handle error in Expression creation + const exp = await toExpression(rootState); + return exp; + } + }; - this.updateOutput({ - savedAttributes, - title: savedAttributes?.title, - }); - }); + // Needed to enable inspection panel option + public getInspectorAdapters = () => { + if (!this.handler) { + return undefined; + } + return this.handler.inspect(); + }; + + // Needed to add informational tooltip + public getDescription() { + return this.savedWizard?.description; } public render(node: HTMLElement) { - if (this.node) { - ReactDOM.unmountComponentAtNode(this.node); + if (this.output.error) { + // TODO: Can we find a more elegant way to throw, propagate, and render errors? + const errorEmbeddable = new ErrorEmbeddable( + this.output.error as Error, + this.input, + this.parent + ); + return errorEmbeddable.render(node); } - this.node = node; - ReactDOM.render(<WizardEmbeddableComponent embeddable={this} />, node); + this.timeRange = cloneDeep(this.input.timeRange); + + const div = document.createElement('div'); + div.className = `wizard visualize panel-content panel-content--fullWidth`; + node.appendChild(div); + + this.node = div; + super.render(this.node); + + // TODO: Investigate migrating to using `./wizard_component` for React rendering instead + const ExpressionLoader = getExpressionLoader(); + this.handler = new ExpressionLoader(this.node, undefined, { + onRenderError: (_element: HTMLElement, error: ExpressionRenderError) => { + this.onContainerError(error); + }, + }); + + if (this.savedWizard?.description) { + div.setAttribute('data-description', this.savedWizard.description); + } + + div.setAttribute('data-test-subj', 'wizardLoader'); + + this.subscriptions.push(this.handler.loading$.subscribe(this.onContainerLoading)); + this.subscriptions.push(this.handler.render$.subscribe(this.onContainerRender)); + + this.updateHandler(); } - /** - * Lets re-sync our saved object to make sure it's up to date! - */ public async reload() { - this.savedObjectId = this.input.savedObjectId; - const wizardSavedObject = await this.savedObjectsClient.get<WizardSavedObjectAttributes>( - 'wizard', - this.input.savedObjectId - ); - const savedAttributes = wizardSavedObject?.attributes; - this.updateOutput({ - savedAttributes, - title: wizardSavedObject?.attributes?.title, - }); + this.updateHandler(); } public destroy() { super.destroy(); - this.subscription.unsubscribe(); + this.subscriptions.forEach((s) => s.unsubscribe()); if (this.node) { ReactDOM.unmountComponentAtNode(this.node); } + + if (this.handler) { + this.handler.destroy(); + this.handler.getElement().remove(); + } + this.autoRefreshFetchSubscription.unsubscribe(); + } + + private async updateHandler() { + const expressionParams: IExpressionLoaderParams = { + searchContext: { + timeRange: this.timeRange, + query: this.input.query, + filters: this.input.filters, + }, + }; + if (this.abortController) { + this.abortController.abort(); + } + this.abortController = new AbortController(); + const abortController = this.abortController; + + if (this.handler && !abortController.signal.aborted) { + this.handler.update(this.expression, expressionParams); + } + } + + public async handleChanges() { + // TODO: refactor (here and in visualize) to remove lodash dependency - immer probably a better choice + + let dirty = false; + + // Check if timerange has changed + if (!isEqual(this.input.timeRange, this.timeRange)) { + this.timeRange = cloneDeep(this.input.timeRange); + dirty = true; + } + + // Check if filters has changed + if (!opensearchFilters.onlyDisabledFiltersChanged(this.input.filters, this.filters)) { + this.filters = this.input.filters; + dirty = true; + } + + // Check if query has changed + if (!isEqual(this.input.query, this.query)) { + this.query = this.input.query; + dirty = true; + } + + // Check if rootState has changed + if (!isEqual(this.getSerializedState(), this.serializedState)) { + this.serializedState = this.getSerializedState(); + this.expression = (await this.getExpression()) ?? ''; + dirty = true; + } + + if (this.handler && dirty) { + this.updateHandler(); + } } + + onContainerLoading = () => { + this.renderComplete.dispatchInProgress(); + this.updateOutput({ loading: true, error: undefined }); + }; + + onContainerRender = () => { + this.renderComplete.dispatchComplete(); + this.updateOutput({ loading: false, error: undefined }); + }; + + onContainerError = (error: ExpressionRenderError) => { + if (this.abortController) { + this.abortController.abort(); + } + this.renderComplete.dispatchError(); + this.updateOutput({ loading: false, error }); + }; + + // TODO: we may eventually need to add support for visualizations that use triggers like filter or brush, but current wizard vis types don't support triggers + // public supportedTriggers(): TriggerId[] { + // return this.visType.getSupportedTriggers?.() ?? []; + // } } diff --git a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx index 6e8a49a05d3f..f2d92d001303 100644 --- a/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx @@ -4,38 +4,26 @@ */ import { i18n } from '@osd/i18n'; -import { - IUiSettingsClient, - NotificationsStart, - SavedObjectsClientContract, -} from '../../../../core/public'; -import { DataPublicPluginStart } from '../../../data/public'; import { EmbeddableFactory, EmbeddableFactoryDefinition, EmbeddableOutput, - EmbeddableStart, ErrorEmbeddable, IContainer, SavedObjectEmbeddableInput, } from '../../../embeddable/public'; -import { ExpressionsStart } from '../../../expressions/public'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public'; -import { WizardSavedObjectAttributes } from '../../common'; -import { TypeServiceStart } from '../services/type_service'; +import { + EDIT_PATH, + PLUGIN_ID, + PLUGIN_NAME, + WizardSavedObjectAttributes, + WIZARD_SAVED_OBJECT, +} from '../../common'; import { DisabledEmbeddable } from './disabled_embeddable'; import { WizardEmbeddable, WizardOutput, WIZARD_EMBEDDABLE } from './wizard_embeddable'; import wizardIcon from '../assets/wizard_icon.svg'; - -interface StartServices { - data: DataPublicPluginStart; - expressions: ExpressionsStart; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - savedObjectsClient: SavedObjectsClientContract; - notifications: NotificationsStart; - types: TypeServiceStart; - uiSettings: IUiSettingsClient; -} +import { getHttp, getSavedWizardLoader, getTimeFilter, getUISettings } from '../plugin_services'; // TODO: use or remove? export type WizardEmbeddableFactory = EmbeddableFactory< @@ -56,13 +44,19 @@ export class WizardEmbeddableFactoryDefinition public readonly type = WIZARD_EMBEDDABLE; public readonly savedObjectMetaData = { // TODO: Update to include most vis functionality - name: 'Wizard', + name: PLUGIN_NAME, includeFields: ['visualizationState'], - type: 'wizard', + type: WIZARD_SAVED_OBJECT, getIconForSavedObject: () => wizardIcon, }; - constructor(private getStartServices: () => Promise<StartServices>) {} + // TODO: Would it be better to explicitly declare start service dependencies? + constructor() {} + + public canCreateNew() { + // Because wizard creation starts with the visualization modal, no need to have a separate entry for wizard until it's separate + return false; + } public async isEditable() { // TODO: Add proper access controls @@ -70,44 +64,53 @@ export class WizardEmbeddableFactoryDefinition return true; } - public createFromSavedObject = ( + public async createFromSavedObject( savedObjectId: string, input: Partial<SavedObjectEmbeddableInput> & { id: string }, parent?: IContainer - ): Promise<WizardEmbeddable | ErrorEmbeddable | DisabledEmbeddable> => { - return this.create({ ...input, savedObjectId }, parent); - }; + ): Promise<WizardEmbeddable | ErrorEmbeddable | DisabledEmbeddable> { + try { + const savedWizard = await getSavedWizardLoader().get(savedObjectId); + + const editPath = `${EDIT_PATH}/${savedObjectId}`; + + const editUrl = getHttp().basePath.prepend(`/app/${PLUGIN_ID}${editPath}`); - public async create(input: SavedObjectEmbeddableInput, parent?: IContainer) { - // TODO: Use savedWizardLoader here instead - const { - data, - expressions: { ReactExpressionRenderer }, - notifications: { toasts }, - savedObjectsClient, - types, - uiSettings, - } = await this.getStartServices(); + const isLabsEnabled = getUISettings().get<boolean>(VISUALIZE_ENABLE_LABS_SETTING); - const isLabsEnabled = uiSettings.get<boolean>(VISUALIZE_ENABLE_LABS_SETTING); + if (!isLabsEnabled) { + return new DisabledEmbeddable(PLUGIN_NAME, input); + } - if (!isLabsEnabled) { - return new DisabledEmbeddable('Wizard', input); + return new WizardEmbeddable( + getTimeFilter(), + { + savedWizard, + editUrl, + editPath, + editable: true, + }, + { + ...input, + savedObjectId: input.savedObjectId ?? '', + }, + { + parent, + } + ); + } catch (e) { + console.error(e); // eslint-disable-line no-console + return new ErrorEmbeddable(e as Error, input, parent); } + } - return new WizardEmbeddable(input, { - parent, - data, - savedObjectsClient, - ReactExpressionRenderer, - toasts, - types, - }); + public async create(_input: SavedObjectEmbeddableInput, _parent?: IContainer) { + return undefined; } public getDisplayName() { return i18n.translate('wizard.displayName', { - defaultMessage: 'Wizard', + defaultMessage: PLUGIN_ID, }); } } diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts index 4946c93761e9..9c56562f4ba1 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -20,11 +20,22 @@ import { WizardStart, } from './types'; import { WizardEmbeddableFactoryDefinition, WIZARD_EMBEDDABLE } from './embeddable'; +import wizardIconSecondaryFill from './assets/wizard_icon_secondary_fill.svg'; import wizardIcon from './assets/wizard_icon.svg'; import { EDIT_PATH, PLUGIN_ID, PLUGIN_NAME, WIZARD_SAVED_OBJECT } from '../common'; import { TypeService } from './services/type_service'; import { getPreloadedStore } from './application/utils/state_management'; -import { setAggService, setIndexPatterns } from './plugin_services'; +import { + setAggService, + setIndexPatterns, + setHttp, + setSavedWizardLoader, + setExpressionLoader, + setTimeFilter, + setUISettings, + setTypeService, + setReactExpressionRenderer, +} from './plugin_services'; import { createSavedWizardLoader } from './saved_visualizations'; import { registerDefaultTypes } from './visualizations'; import { ConfigSchema } from '../config'; @@ -63,10 +74,6 @@ export class WizardPlugin // TODO: Add the redirect await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); - // Register plugin services - setAggService(data.search.aggs); - setIndexPatterns(data.indexPatterns); - // Register Default Visualizations const services: WizardServices = { @@ -94,19 +101,7 @@ export class WizardPlugin // TODO: investigate simplification via getter a la visualizations: // const start = createStartServicesGetter(core.getStartServices)); // const embeddableFactory = new WizardEmbeddableFactoryDefinition({ start }); - const embeddableFactory = new WizardEmbeddableFactoryDefinition(async () => { - const [coreStart, pluginsStart, _wizardStart] = await core.getStartServices(); - // TODO: refactor to pass minimal service methods? - return { - savedObjectsClient: coreStart.savedObjects.client, - data: pluginsStart.data, - getEmbeddableFactory: pluginsStart.embeddable.getEmbeddableFactory, - expressions: pluginsStart.expressions, - notifications: coreStart.notifications, - types: this.typeService.start(), - uiSettings: coreStart.uiSettings, - }; - }); + const embeddableFactory = new WizardEmbeddableFactoryDefinition(); embeddable.registerEmbeddableFactory(WIZARD_EMBEDDABLE, embeddableFactory); // Register the plugin as an alias to create visualization @@ -116,7 +111,7 @@ export class WizardPlugin description: i18n.translate('wizard.visPicker.description', { defaultMessage: 'Create visualizations using the new Drag & Drop experience', }), - icon: wizardIcon, + icon: wizardIconSecondaryFill, stage: 'experimental', aliasApp: PLUGIN_ID, aliasPath: '#/', @@ -143,18 +138,31 @@ export class WizardPlugin }; } - public start(core: CoreStart, { data }: WizardPluginStartDependencies): WizardStart { - const typeService = this.typeService; + public start(core: CoreStart, { data, expressions }: WizardPluginStartDependencies): WizardStart { + const typeService = this.typeService.start(); + + const savedWizardLoader = createSavedWizardLoader({ + savedObjectsClient: core.savedObjects.client, + indexPatterns: data.indexPatterns, + search: data.search, + chrome: core.chrome, + overlays: core.overlays, + }); + + // Register plugin services + setAggService(data.search.aggs); + setExpressionLoader(expressions.ExpressionLoader); + setReactExpressionRenderer(expressions.ReactExpressionRenderer); + setHttp(core.http); + setIndexPatterns(data.indexPatterns); + setSavedWizardLoader(savedWizardLoader); + setTimeFilter(data.query.timefilter.timefilter); + setTypeService(typeService); + setUISettings(core.uiSettings); return { - ...typeService.start(), - savedWizardLoader: createSavedWizardLoader({ - savedObjectsClient: core.savedObjects.client, - indexPatterns: data.indexPatterns, - search: data.search, - chrome: core.chrome, - overlays: core.overlays, - }), + ...typeService, + savedWizardLoader, }; } diff --git a/src/plugins/wizard/public/plugin_services.ts b/src/plugins/wizard/public/plugin_services.ts index 67b562d6eca9..8f01ea6e9b6b 100644 --- a/src/plugins/wizard/public/plugin_services.ts +++ b/src/plugins/wizard/public/plugin_services.ts @@ -4,12 +4,36 @@ */ import { createGetterSetter } from '../../opensearch_dashboards_utils/common'; -import { DataPublicPluginStart } from '../../data/public'; +import { DataPublicPluginStart, TimefilterContract } from '../../data/public'; +import { SavedWizardLoader } from './saved_visualizations'; +import { HttpStart, IUiSettingsClient } from '../../../core/public'; +import { ExpressionsStart } from '../../expressions/public'; +import { TypeServiceStart } from './services/type_service'; export const [getAggService, setAggService] = createGetterSetter< DataPublicPluginStart['search']['aggs'] >('data.search.aggs'); +export const [getExpressionLoader, setExpressionLoader] = createGetterSetter< + ExpressionsStart['ExpressionLoader'] +>('expressions.ExpressionLoader'); + +export const [getReactExpressionRenderer, setReactExpressionRenderer] = createGetterSetter< + ExpressionsStart['ReactExpressionRenderer'] +>('expressions.ReactExpressionRenderer'); + +export const [getHttp, setHttp] = createGetterSetter<HttpStart>('Http'); + export const [getIndexPatterns, setIndexPatterns] = createGetterSetter< DataPublicPluginStart['indexPatterns'] >('data.indexPatterns'); + +export const [getSavedWizardLoader, setSavedWizardLoader] = createGetterSetter<SavedWizardLoader>( + 'SavedWizardLoader' +); + +export const [getTimeFilter, setTimeFilter] = createGetterSetter<TimefilterContract>('TimeFilter'); + +export const [getTypeService, setTypeService] = createGetterSetter<TypeServiceStart>('TypeService'); + +export const [getUISettings, setUISettings] = createGetterSetter<IUiSettingsClient>('UISettings'); diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.tsx index 62fddfff85bb..90f30d8f8a95 100644 --- a/src/plugins/wizard/public/services/type_service/visualization_type.tsx +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -15,11 +15,7 @@ export class VisualizationType implements IVisualizationType { public readonly icon: IconType; public readonly stage: 'beta' | 'production'; public readonly ui: IVisualizationType['ui']; - public readonly toExpression: ( - state: RootState, - indexPatterns?, - aggs? - ) => Promise<string | undefined>; + public readonly toExpression: (state: RootState) => Promise<string | undefined>; constructor(options: VisualizationTypeOptions) { this.name = options.name; diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.ts index 0a459c23a7a1..ce930d9b8e40 100644 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.ts +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -91,18 +91,13 @@ export interface MetricRootState extends RootState { style: MetricOptionsDefaults; } -export const toExpression = async ( - { style: styleState, visualization }: MetricRootState, - indexPatterns, - aggs -) => { +export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { const { activeVisualization, indexPattern: indexId = '' } = visualization; const { aggConfigParams } = activeVisualization || {}; - const indexPatternsService = indexPatterns ?? getIndexPatterns(); + const indexPatternsService = getIndexPatterns(); const indexPattern = await indexPatternsService.get(indexId); - const aggService = aggs ?? getAggService(); - const aggConfigs = aggService.createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + const aggConfigs = getAggService().createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); // soon this becomes: const opensearchaggs = vis.data.aggs!.toExpressionAst(); const opensearchaggs = buildExpressionFunction<OpenSearchaggsExpressionFunctionDefinition>( From 8993ce0b78d20f743909e5095614b676f5478d95 Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Mon, 25 Jul 2022 19:22:14 -0700 Subject: [PATCH 27/47] [D&D] Fix index pattern state and loading (#1949) Remove useIndexPattern hook in favor of useIndexPatterns fixes #1917 Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../components/data_tab/field_selector.tsx | 4 ++-- .../components/data_tab/secondary_panel.tsx | 4 ++-- .../components/data_tab/use/use_dropbox.tsx | 4 ++-- .../public/application/components/top_nav.tsx | 4 ++-- .../public/application/utils/use/index.ts | 2 +- .../utils/use/use_index_pattern.tsx | 21 ------------------- 6 files changed, 9 insertions(+), 30 deletions(-) diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx index 353f642afcf6..a794093bc00d 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx @@ -16,7 +16,7 @@ import { FieldSelectorField } from './field_selector_field'; import './field_selector.scss'; import { useTypedSelector } from '../../utils/state_management'; -import { useIndexPattern } from '../../utils/use'; +import { useIndexPatterns } from '../../utils/use'; import { getAvailableFields } from './utils'; interface IFieldCategories { @@ -33,7 +33,7 @@ const META_FIELDS: string[] = [ ]; export const FieldSelector = () => { - const indexPattern = useIndexPattern(); + const indexPattern = useIndexPatterns().selected; const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); const [filteredFields, setFilteredFields] = useState<IndexPatternField[]>([]); diff --git a/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx index c897a9b018c5..21bdf256422d 100644 --- a/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx @@ -8,7 +8,7 @@ import { cloneDeep } from 'lodash'; import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public'; import { Title } from './title'; -import { useIndexPattern, useVisualizationType } from '../../utils/use'; +import { useIndexPatterns, useVisualizationType } from '../../utils/use'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../../types'; import { IAggType } from '../../../../../data/public'; @@ -20,7 +20,7 @@ export function SecondaryPanel() { const [touched, setTouched] = useState(false); const dispatch = useTypedDispatch(); const vizType = useVisualizationType(); - const indexPattern = useIndexPattern(); + const indexPattern = useIndexPatterns().selected; const { services: { data: { diff --git a/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx index 60290fc96e93..decad4a2c334 100644 --- a/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx @@ -16,7 +16,7 @@ import { reorderAgg, updateAggConfigParams, } from '../../../utils/state_management/visualization_slice'; -import { useIndexPattern } from '../../../utils/use/use_index_pattern'; +import { useIndexPatterns } from '../../../utils/use/use_index_pattern'; import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../../../types'; @@ -31,7 +31,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { const { id: dropboxId, label, schema } = props; const [validAggTypes, setValidAggTypes] = useState<string[]>([]); const dispatch = useTypedDispatch(); - const indexPattern = useIndexPattern(); + const indexPattern = useIndexPatterns().selected; const { services: { data: { diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx index 7a4bfa1010f4..fd1d387c6a50 100644 --- a/src/plugins/wizard/public/application/components/top_nav.tsx +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -11,7 +11,7 @@ import { getTopNavConfig } from '../utils/get_top_nav_config'; import { WizardServices } from '../../types'; import './top_nav.scss'; -import { useIndexPattern, useSavedWizardVis } from '../utils/use'; +import { useIndexPatterns, useSavedWizardVis } from '../utils/use'; import { useTypedSelector } from '../utils/state_management'; export const TopNav = () => { @@ -48,7 +48,7 @@ export const TopNav = () => { ); }, [hasUnappliedChanges, rootState, savedWizardVis, services, visualizationIdFromUrl]); - const indexPattern = useIndexPattern(); + const indexPattern = useIndexPatterns().selected; return ( <div className="wizTopNav"> diff --git a/src/plugins/wizard/public/application/utils/use/index.ts b/src/plugins/wizard/public/application/utils/use/index.ts index c9203242e63c..e8d1087ce0a0 100644 --- a/src/plugins/wizard/public/application/utils/use/index.ts +++ b/src/plugins/wizard/public/application/utils/use/index.ts @@ -4,5 +4,5 @@ */ export { useVisualizationType } from './use_visualization_type'; -export { useIndexPattern, useIndexPatterns } from './use_index_pattern'; +export { useIndexPatterns } from './use_index_pattern'; export { useSavedWizardVis } from './use_saved_wizard_vis'; diff --git a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx index 6ce448b9f055..b5c60ee20944 100644 --- a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx +++ b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx @@ -8,27 +8,6 @@ import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_re import { WizardServices } from '../../../types'; import { useTypedSelector } from '../state_management'; -export const useIndexPattern = (): IndexPattern | undefined => { - const { indexPattern: indexId = '' } = useTypedSelector((state) => state.visualization); - const [indexPattern, setIndexPattern] = useState<IndexPattern>(); - const { - services: { - data: { indexPatterns }, - }, - } = useOpenSearchDashboards<WizardServices>(); - - useEffect(() => { - const handleIndexUpdate = async () => { - const currentIndex = await indexPatterns.get(indexId); - setIndexPattern(currentIndex); - }; - - handleIndexUpdate(); - }, [indexId, indexPatterns]); - - return indexPattern; -}; - export const useIndexPatterns = () => { const { indexPattern: indexId = '' } = useTypedSelector((state) => state.visualization); const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>([]); From 4c54b029c3173249c2a10bbfb3f7ce66c5129bcb Mon Sep 17 00:00:00 2001 From: Josh Romero <rmerqg@amazon.com> Date: Tue, 26 Jul 2022 01:41:58 -0700 Subject: [PATCH 28/47] [D&D] Fix duplicate title warning (#1950) fixes #1918 Signed-off-by: Josh Romero <rmerqg@amazon.com> --- .../public/application/utils/get_top_nav_config.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx index 670513187843..d0404b5b762a 100644 --- a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx @@ -91,6 +91,7 @@ export const getTopNavConfig = ( if (!savedWizardVis) { return; } + const currentTitle = savedWizardVis.title; savedWizardVis.visualizationState = JSON.stringify(visualizationState); savedWizardVis.styleState = JSON.stringify(styleState); savedWizardVis.title = newTitle; @@ -126,11 +127,13 @@ export const getTopNavConfig = ( pathname: `${EDIT_PATH}/${id}`, }); } - - return { id }; + } else { + // reset title if save not successful + savedWizardVis.title = currentTitle; } - throw new Error('Saved but no id returned'); + // Even if id='', which it will be for a duplicate title warning, we still want to return it, to avoid closing the modal + return { id }; } catch (error: any) { // eslint-disable-next-line no-console console.error(error); @@ -145,6 +148,9 @@ export const getTopNavConfig = ( text: error.message, 'data-test-subj': 'saveVisualizationError', }); + + // reset title if save not successful + savedWizardVis.title = currentTitle; return { error }; } }; From bbe244d7ba37d70a4b80c1b0b2a695edf5824386 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc1993@gmail.com> Date: Tue, 26 Jul 2022 09:18:19 -0700 Subject: [PATCH 29/47] [D&D] Adds autosave while editing aggregation (#1953) * fest: Adds autosave while editing agg Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: autosave order Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: spelling Co-authored-by: Josh Romero <rmerqg@amazon.com> Co-authored-by: Josh Romero <rmerqg@amazon.com> --- .../components/data_tab/config_panel.tsx | 6 ++- .../components/data_tab/secondary_panel.tsx | 40 ++++++++++++---- .../components/data_tab/use/use_dropbox.tsx | 8 ++-- .../application/components/workspace.tsx | 2 +- .../utils/state_management/metadata_slice.ts | 48 +++++++++++++++++++ .../utils/state_management/preload.ts | 3 ++ .../utils/state_management/store.ts | 2 + .../state_management/visualization_slice.ts | 13 ++--- 8 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx index 1c0e67693471..be0ec12bbff2 100644 --- a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx @@ -13,7 +13,9 @@ import { SecondaryPanel } from './secondary_panel'; export function ConfigPanel() { const vizType = useVisualizationType(); - const draftAgg = useTypedSelector((state) => state.visualization.activeVisualization?.draftAgg); + const editingState = useTypedSelector( + (state) => state.visualization.activeVisualization?.draftAgg + ); const schemas = vizType.ui.containerConfig.data.schemas; if (!schemas) return null; @@ -21,7 +23,7 @@ export function ConfigPanel() { const mainPanel = mapSchemaToAggPanel(schemas); return ( - <EuiForm className={`wizConfig ${draftAgg ? 'showSecondary' : ''}`}> + <EuiForm className={`wizConfig ${editingState ? 'showSecondary' : ''}`}> <div className="wizConfig__section">{mainPanel}</div> <SecondaryPanel /> </EuiForm> diff --git a/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx index 21bdf256422d..a0fa9c160227 100644 --- a/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx @@ -12,11 +12,14 @@ import { useIndexPatterns, useVisualizationType } from '../../utils/use'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../../types'; import { IAggType } from '../../../../../data/public'; -import { saveAgg, editAgg } from '../../utils/state_management/visualization_slice'; +import { saveDraftAgg, editDraftAgg } from '../../utils/state_management/visualization_slice'; +import { setValid } from '../../utils/state_management/metadata_slice'; + +const EDITOR_KEY = 'CONFIG_PANEL'; export function SecondaryPanel() { const draftAgg = useTypedSelector((state) => state.visualization.activeVisualization!.draftAgg); - const [valid, setValid] = useState(true); + const valid = useTypedSelector((state) => state.metadata.editorState.valid[EDITOR_KEY]); const [touched, setTouched] = useState(false); const dispatch = useTypedDispatch(); const vizType = useVisualizationType(); @@ -46,9 +49,26 @@ export function SecondaryPanel() { const showAggParamEditor = !!(aggConfig && indexPattern); const closeMenu = useCallback(() => { - // Save the agg if valid else discard - dispatch(saveAgg(valid)); - }, [dispatch, valid]); + dispatch(editDraftAgg(undefined)); + }, [dispatch]); + + const handleSetValid = useCallback( + (isValid: boolean) => { + // Set validity state globally + dispatch( + setValid({ + key: EDITOR_KEY, + valid: isValid, + }) + ); + + // Autosave changes if valid + if (valid) { + dispatch(saveDraftAgg()); + } + }, + [dispatch, valid] + ); return ( <div className="wizConfig__section wizConfig--secondary"> @@ -58,7 +78,7 @@ export function SecondaryPanel() { className="wizConfig__aggEditor" agg={aggConfig!} indexPattern={indexPattern!} - setValidity={setValid} + setValidity={handleSetValid} setTouched={setTouched} schemas={schemas} formIsTouched={false} @@ -66,8 +86,8 @@ export function SecondaryPanel() { metricAggs={[]} state={{ data: {}, - description: 'Falalala', - title: 'Title for the aggParams', + description: '', + title: '', }} setAggParamValue={function <T extends string | number | symbol>( aggId: string, @@ -75,11 +95,11 @@ export function SecondaryPanel() { value: any ): void { aggConfig.params[paramName] = value; - dispatch(editAgg(aggConfig.serialize())); + dispatch(editDraftAgg(aggConfig.serialize())); }} onAggTypeChange={function (aggId: string, aggType: IAggType): void { aggConfig.type = aggType; - dispatch(editAgg(aggConfig.serialize())); + dispatch(editDraftAgg(aggConfig.serialize())); }} /> )} diff --git a/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx index decad4a2c334..af9122d8ebd7 100644 --- a/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx @@ -12,7 +12,7 @@ import { useTypedDispatch, useTypedSelector } from '../../../utils/state_managem import { DropboxDisplay, DropboxProps } from '../dropbox'; import { useDrop } from '../../../utils/drag_drop'; import { - editAgg, + editDraftAgg, reorderAgg, updateAggConfigParams, } from '../../../utils/state_management/visualization_slice'; @@ -88,18 +88,18 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { throw new Error('Missing new aggConfig'); } - dispatch(editAgg(newAggConfig.serialize())); + dispatch(editDraftAgg(newAggConfig.serialize())); }, [aggConfigs, aggService, aggs, dispatch, indexPattern, schema.name]); const onEditField = useCallback( - (aggId) => { + (aggId: string) => { const aggConfig = aggConfigs?.aggs.find((agg) => agg.id === aggId); if (!aggConfig) { throw new Error('Could not find agg in aggConfigs'); } - dispatch(editAgg(aggConfig.serialize())); + dispatch(editDraftAgg(aggConfig.serialize())); }, [aggConfigs?.aggs, dispatch] ); diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index 39d75a7c43d2..ab320c8257ad 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -95,7 +95,7 @@ export const Workspace: FC = ({ children }) => { ) : ( <EuiFlexItem className="wizWorkspace__empty"> <EuiEmptyPrompt - title={<h2>Drop some fields to start</h2>} + title={<h2>Add a field to start</h2>} body={ <> <p>Drag a field to the configuration panel to generate a visualization.</p> diff --git a/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts b/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts new file mode 100644 index 000000000000..6e0902220079 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { WizardServices } from '../../../types'; + +export interface MetadataState { + editorState: { + valid: { + // Validity for each section in the editor + [key: string]: boolean; + }; + }; +} + +const initialState: MetadataState = { + editorState: { + valid: {}, + }, +}; + +export const getPreloadedState = async ({ + types, + data, +}: WizardServices): Promise<MetadataState> => { + const preloadedState = { ...initialState }; + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'metadata', + initialState, + reducers: { + setValid: (state, action: PayloadAction<{ key: string; valid: boolean }>) => { + const { key, valid } = action.payload; + state.editorState.valid[key] = valid; + }, + setState: (_state, action: PayloadAction<MetadataState>) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setValid, setState } = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/state_management/preload.ts b/src/plugins/wizard/public/application/utils/state_management/preload.ts index d9cefa21a064..1d96608e33cc 100644 --- a/src/plugins/wizard/public/application/utils/state_management/preload.ts +++ b/src/plugins/wizard/public/application/utils/state_management/preload.ts @@ -7,6 +7,7 @@ import { PreloadedState } from '@reduxjs/toolkit'; import { WizardServices } from '../../..'; import { getPreloadedState as getPreloadedStyleState } from './style_slice'; import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; +import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; import { RootState } from './store'; export const getPreloadedState = async ( @@ -14,9 +15,11 @@ export const getPreloadedState = async ( ): Promise<PreloadedState<RootState>> => { const styleState = await getPreloadedStyleState(services); const visualizationState = await getPreloadedVisualizationState(services); + const metadataState = await getPreloadedMetadataState(services); return { style: styleState, visualization: visualizationState, + metadata: metadataState, }; }; diff --git a/src/plugins/wizard/public/application/utils/state_management/store.ts b/src/plugins/wizard/public/application/utils/state_management/store.ts index d8fff094cd1e..932a5f4d8914 100644 --- a/src/plugins/wizard/public/application/utils/state_management/store.ts +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -6,12 +6,14 @@ import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; import { reducer as styleReducer } from './style_slice'; import { reducer as visualizationReducer } from './visualization_slice'; +import { reducer as metadataReducer } from './metadata_slice'; import { WizardServices } from '../../..'; import { getPreloadedState } from './preload'; const rootReducer = combineReducers({ style: styleReducer, visualization: visualizationReducer, + metadata: metadataReducer, }); export const configurePreloadedStore = (preloadedState: PreloadedState<RootState>) => { diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts index 06c865e14395..fe1277f33432 100644 --- a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -59,15 +59,13 @@ export const slice = createSlice({ setSearchField: (state, action: PayloadAction<string>) => { state.searchField = action.payload; }, - editAgg: (state, action: PayloadAction<CreateAggConfigParams>) => { + editDraftAgg: (state, action: PayloadAction<CreateAggConfigParams | undefined>) => { state.activeVisualization!.draftAgg = action.payload; }, - saveAgg: (state, action: PayloadAction<boolean>) => { - const saveDraft = action.payload; + saveDraftAgg: (state, action: PayloadAction<undefined>) => { const draftAgg = state.activeVisualization!.draftAgg; - // Delete the aggConfigParam if the save is not true - if (saveDraft && draftAgg) { + if (draftAgg) { const aggIndex = state.activeVisualization!.aggConfigParams.findIndex( (agg) => agg.id === draftAgg.id ); @@ -78,7 +76,6 @@ export const slice = createSlice({ state.activeVisualization!.aggConfigParams.splice(aggIndex, 1, draftAgg); } } - delete state.activeVisualization!.draftAgg; }, reorderAgg: ( state, @@ -116,9 +113,9 @@ export const { setActiveVisualization, setIndexPattern, setSearchField, - editAgg, + editDraftAgg, + saveDraftAgg, updateAggConfigParams, - saveAgg, reorderAgg, setState, } = slice.actions; From 606f1d63bc7bfa2d13debb228ba92a8598ddd803 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 27 Jul 2022 14:44:22 -0700 Subject: [PATCH 30/47] [D&D] Fixes autosave with debounce (#1965) * fix: autosave editing Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: improve workspace animation Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: show invalid field when editing Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: header offset Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../wizard/public/application/_variables.scss | 2 +- .../wizard/public/application/app.scss | 4 +++ .../components/data_tab/secondary_panel.tsx | 26 ++++++++++++++----- .../application/components/workspace.scss | 14 +++++++--- .../utils/state_management/metadata_slice.ts | 10 +++---- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss index 789aa00373e1..ab00f4eabbff 100644 --- a/src/plugins/wizard/public/application/_variables.scss +++ b/src/plugins/wizard/public/application/_variables.scss @@ -1,5 +1,5 @@ @import "@elastic/eui/src/global_styling/variables/header"; @import "@elastic/eui/src/global_styling/variables/form"; -$osdHeaderOffset: $euiHeaderHeightCompensation * 2; +$osdHeaderOffset: $euiHeaderHeightCompensation; $wizSideNavWidth: 470px; diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss index b02bc79395d0..370c1153443b 100644 --- a/src/plugins/wizard/public/application/app.scss +++ b/src/plugins/wizard/public/application/app.scss @@ -9,3 +9,7 @@ "sideNav workspace"; height: calc(100vh - #{$osdHeaderOffset}); } + +.headerIsExpanded .wizLayout { + height: calc(100vh - #{$osdHeaderOffset * 2}); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx index a0fa9c160227..dfbb148f07a2 100644 --- a/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx @@ -5,6 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { cloneDeep } from 'lodash'; +import { useDebounce } from 'react-use'; import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public'; import { Title } from './title'; @@ -13,13 +14,15 @@ import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_re import { WizardServices } from '../../../types'; import { IAggType } from '../../../../../data/public'; import { saveDraftAgg, editDraftAgg } from '../../utils/state_management/visualization_slice'; -import { setValid } from '../../utils/state_management/metadata_slice'; +import { setValidity } from '../../utils/state_management/metadata_slice'; const EDITOR_KEY = 'CONFIG_PANEL'; export function SecondaryPanel() { const draftAgg = useTypedSelector((state) => state.visualization.activeVisualization!.draftAgg); - const valid = useTypedSelector((state) => state.metadata.editorState.valid[EDITOR_KEY]); + const isEditorValid = useTypedSelector( + (state) => state.metadata.editorState.validity[EDITOR_KEY] + ); const [touched, setTouched] = useState(false); const dispatch = useTypedDispatch(); const vizType = useVisualizationType(); @@ -56,18 +59,27 @@ export function SecondaryPanel() { (isValid: boolean) => { // Set validity state globally dispatch( - setValid({ + setValidity({ key: EDITOR_KEY, valid: isValid, }) ); + }, + [dispatch] + ); - // Autosave changes if valid - if (valid) { + // Autosave is agg value has changed and edits are valid + useDebounce( + () => { + if (isEditorValid) { dispatch(saveDraftAgg()); + } else { + // To indicate that an invalid edit was made + setTouched(true); } }, - [dispatch, valid] + 200, + [draftAgg, isEditorValid] ); return ( @@ -81,7 +93,7 @@ export function SecondaryPanel() { setValidity={handleSetValid} setTouched={setTouched} schemas={schemas} - formIsTouched={false} + formIsTouched={touched} groupName={selectedSchema?.group ?? 'none'} metricAggs={[]} state={{ diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss index f83a148ec44d..bf7c3257274d 100644 --- a/src/plugins/wizard/public/application/components/workspace.scss +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -16,9 +16,9 @@ } &__handFieldSvg { - animation: wizDragAnimation 2s ease-in-out infinite alternate; + animation: wizDragAnimation 6s ease-in-out infinite forwards; position: absolute; - top: 43%; + top: 34.5%; } } @@ -27,7 +27,15 @@ transform: none; } + 30% { + transform: translate(116%, -80%); + } + + 60% { + transform: none; + } + 100% { - transform: translate(65%, -30%); + transform: none; } } diff --git a/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts b/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts index 6e0902220079..f1cb4bcb9566 100644 --- a/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts @@ -8,7 +8,7 @@ import { WizardServices } from '../../../types'; export interface MetadataState { editorState: { - valid: { + validity: { // Validity for each section in the editor [key: string]: boolean; }; @@ -17,7 +17,7 @@ export interface MetadataState { const initialState: MetadataState = { editorState: { - valid: {}, + validity: {}, }, }; @@ -34,9 +34,9 @@ export const slice = createSlice({ name: 'metadata', initialState, reducers: { - setValid: (state, action: PayloadAction<{ key: string; valid: boolean }>) => { + setValidity: (state, action: PayloadAction<{ key: string; valid: boolean }>) => { const { key, valid } = action.payload; - state.editorState.valid[key] = valid; + state.editorState.validity[key] = valid; }, setState: (_state, action: PayloadAction<MetadataState>) => { return action.payload; @@ -45,4 +45,4 @@ export const slice = createSlice({ }); export const { reducer } = slice; -export const { setValid, setState } = slice.actions; +export const { setValidity, setState } = slice.actions; From 4a260f54af4c0b4f30037dd58f71d7ed94b0ca58 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:03:31 -0700 Subject: [PATCH 31/47] Update src/plugins/saved_objects_management/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/saved_objects_management/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md index 3e2a67dd0b42..e53dda608c34 100644 --- a/src/plugins/saved_objects_management/README.md +++ b/src/plugins/saved_objects_management/README.md @@ -8,6 +8,7 @@ From the primary UI page, this plugin allows you to: 3. Inspect/edit raw saved object values without validation For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are also free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly). + ## Making a new saved object type manageable 1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See `SavedObjectsTypeManagementDefinition` for explanation of its properties: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285) From 0c1ab88e1b31593c110ba0d6c4c38c7e1767ade2 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:03:40 -0700 Subject: [PATCH 32/47] Update src/plugins/saved_objects_management/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/saved_objects_management/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md index e53dda608c34..e47c7910e981 100644 --- a/src/plugins/saved_objects_management/README.md +++ b/src/plugins/saved_objects_management/README.md @@ -11,7 +11,7 @@ For 3., this plugin can also be used to provide a route/page for editing, such a ## Making a new saved object type manageable -1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See `SavedObjectsTypeManagementDefinition` for explanation of its properties: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285) +1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See [`SavedObjectsTypeManagementDefinition`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285) for explanation of its properties) 2. Register saved object type via `core.savedObjects.registerType(...)` as part of plugin server setup method 3. Implement a way to save the object via `savedObjectsClient.create(...)` 4. After these steps, you should be able to save objects and view/search for them in Saved Objects management (`/app/management/opensearch-dashboards/objects`) From 3c0c6ba1bb68793ced02d90717c6154e4cd3b8c9 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:03:46 -0700 Subject: [PATCH 33/47] Update src/plugins/saved_objects_management/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/saved_objects_management/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md index e47c7910e981..31d28f677ed5 100644 --- a/src/plugins/saved_objects_management/README.md +++ b/src/plugins/saved_objects_management/README.md @@ -21,7 +21,7 @@ For 3., this plugin can also be used to provide a route/page for editing, such a 1. Make sure `management.getInAppUrl` method of the `SavedObjectsType` is defined with a `path` (which will specify the link target) and the `uiCapabilitiesPath` 2. For `uiCapabilitiesPath` to work without additional hardcoding, it should be in the format `{plugin}.show`, so that [the default logic of `src/plugins/saved_objects_management/public/lib/in_app_url.ts`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L48-L50) will correctly match. Otherwise, you'll need to [add a case for your `uiCapabilities` path](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/a9984f63a38e964007ab94fae99237a14d8f9ee2/src/plugins/saved_objects_management/public/lib/in_app_url.ts#L45-L47) to that function 3. Create default plugin capabilities provider -3. Register plugin capabilities via `core.capabilities.registerProvider(...);` as part of plugin server setup method +4. Register plugin capabilities via `core.capabilities.registerProvider(...);` as part of plugin server setup method ## Using saved objects management to inspect/edit new plugin objects From 1b36b11d80dd3ffcf28dac3e43940f19fb07e0b9 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:04:03 -0700 Subject: [PATCH 34/47] Update src/plugins/wizard/server/index.ts Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/wizard/server/index.ts b/src/plugins/wizard/server/index.ts index 811998b8a954..cd5e3aa3a30e 100644 --- a/src/plugins/wizard/server/index.ts +++ b/src/plugins/wizard/server/index.ts @@ -8,7 +8,7 @@ import { ConfigSchema, configSchema } from '../config'; import { WizardPlugin } from './plugin'; // This exports static code and TypeScript types, -// as well as, OpenSearch Dashboards Platform `plugin()` initializer. +// as well as the OpenSearch Dashboards Platform `plugin()` initializer. export function plugin(initializerContext: PluginInitializerContext) { return new WizardPlugin(initializerContext); From ae552207595ae3379b0abda5b9bc3b5f671c2b64 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:04:15 -0700 Subject: [PATCH 35/47] Update src/plugins/wizard/server/types.ts Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/server/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/wizard/server/types.ts b/src/plugins/wizard/server/types.ts index 5d26185a0374..16780c13d60a 100644 --- a/src/plugins/wizard/server/types.ts +++ b/src/plugins/wizard/server/types.ts @@ -4,6 +4,7 @@ */ // eslint-disable-next-line @typescript-eslint/no-empty-interface +// We need to export plugin server types, even if empty export interface WizardPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WizardPluginStart {} From 568092114b787923cab7dbaeb747ea8161131c2c Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:11:52 -0700 Subject: [PATCH 36/47] Update src/plugins/wizard/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index 9cd95d4810db..6c51d754d050 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -1,6 +1,6 @@ # Wizard -A OpenSearch Dashboards plugin for the visualization experience that makes exploring data and creating visualizations much easier. It will act as an additional way to create visualizations alongside the exiting tools within the current visualizations plugin. The tool will be incremental to the visualization tools available to the user in OpenSearch Dashboards today. +An OpenSearch Dashboards plugin for a visualization experience that makes exploring data and creating visualizations much easier. It will act as an additional way to create visualizations alongside the exiting tools within the current visualizations plugin. The tool will be incremental to the visualization tools available to users in OpenSearch Dashboards today. ## Usage From c6125214f5ef32b5503f30534240882c34e94450 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:12:20 -0700 Subject: [PATCH 37/47] Update src/plugins/wizard/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/README.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index 6c51d754d050..5cdeea94ef48 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -34,26 +34,3 @@ Outline: - Currently only the metric viz is setup so schema properties that other vis types need may not be setup fully and need to be set correctly. - `to_expression` is quite custom and can be abstracted into a common utility for different visualizations. Adding more vis types should make it clear as to how this can be done - -## Development (TODO: Delete before merging into mainline) - -All work for this feature currently happens on the `feature/d-and-d` branch - -### Git workflow - -Set main repo as the `upstream` remote -```sh -git remote add upstream https://github.com/opensearch-project/OpenSearch-Dashboards.git -``` - -Keeping the `feature/d-and-d` branch up to date locally - -```sh -git fetch upstream -git checkout feature/d-and-d -git merge upstream/feature/d-and-d -``` - -See the [OpenSearch Dashboards contributing -guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/CONTRIBUTING.md) for instructions -setting up your development environment. From 8cf8dac538986461f4861c20a4967938e1f02ae4 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:12:53 -0700 Subject: [PATCH 38/47] Update src/plugins/wizard/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index 5cdeea94ef48..2726124d1d0f 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -10,7 +10,7 @@ Visualize -> Create Visualization -> Wizard ## Add a visualization (TODO: Cleanup before merging into mainline) -All new visualizations currently reside in [public/visualizations](./public/visualizations). To add a new one, create a new visualization folder and add the required code to setup and register a new vis type. +All new visualizations currently reside in [public/visualizations](./public/visualizations). To add a new one, create a new visualization directory and add the required code (below) to setup and register a new vis type. ### Anatomy of a visualization From 9fb67f60a072da3b2b9db41b2fa4753ad4457a36 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:13:23 -0700 Subject: [PATCH 39/47] Update src/plugins/wizard/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index 2726124d1d0f..ff8fb8f40f0f 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -32,5 +32,5 @@ Outline: **Notes:** - Currently only the metric viz is setup so schema properties that other vis types need may not be setup fully and need to be set correctly. -- `to_expression` is quite custom and can be abstracted into a common utility for different visualizations. Adding more vis types should make it clear as to how this can be done +- `to_expression` has not yet been abstracted into a common utility for different visualizations. Adding more visualization types should make it easier to identify which parts of expression creation are common, and which are visualization-specific. From a1627fddc92e4f71d3b32085c633dd0c0b8d14fb Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:14:03 -0700 Subject: [PATCH 40/47] Update src/plugins/wizard/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index ff8fb8f40f0f..bfd16009dd58 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -31,6 +31,6 @@ Outline: **Notes:** -- Currently only the metric viz is setup so schema properties that other vis types need may not be setup fully and need to be set correctly. +- Currently only the metric viz is defined, so schema properties that other vis types might need may be missing and require further setup. - `to_expression` has not yet been abstracted into a common utility for different visualizations. Adding more visualization types should make it easier to identify which parts of expression creation are common, and which are visualization-specific. From da9dbe6122587f60b86c1a66ce8fed6efb5f303c Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:14:19 -0700 Subject: [PATCH 41/47] Update src/plugins/wizard/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index bfd16009dd58..90c4e2205c00 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -16,8 +16,8 @@ All new visualizations currently reside in [public/visualizations](./public/visu ``` metric/ -├─ metric_viz_type.ts ├─ index.ts +├─ metric_viz_type.ts ├─ to_expression.ts ├─ components/ ├─ metric_viz_options.tsx From 402b8c2c291ad996af3d777ae1a80f73bdf5a3a0 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:14:34 -0700 Subject: [PATCH 42/47] Update src/plugins/wizard/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/wizard/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index 90c4e2205c00..58a03a3e4adc 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -24,10 +24,10 @@ metric/ ``` Outline: -- index.ts: Exposes the create<Viz>Config function that is used to register the viz type -- <vizName>_viz_type.ts: Contains the config that the type service needs to register the new vis type. -- to_expression.ts: The expression function that the plugin will use to render the visualization given the state of the plugin -- <vizName>_viz_options.tsx: The component that will render the other properties that user can set in the `Style` tab. +- `index.ts`: Exposes the `create<Viz>Config` function that is used to register the viz type +- `<vizName>_viz_type.ts`: Contains the config that the type service needs to register the new vis type +- `to_expression.ts`: The expression function that the plugin will use to render the visualization given the state of the plugin +- `<vizName>_viz_options.tsx`: The component that will render the other properties that user can set in the `Style` tab **Notes:** From 5429e8e28fb06590b89e5ee867ee2e546f756ac7 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Wed, 3 Aug 2022 17:19:16 -0700 Subject: [PATCH 43/47] Update src/plugins/saved_objects_management/README.md Co-authored-by: Josh Romero <rmerqg@amazon.com> --- src/plugins/saved_objects_management/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md index 31d28f677ed5..c896a1f43582 100644 --- a/src/plugins/saved_objects_management/README.md +++ b/src/plugins/saved_objects_management/README.md @@ -13,7 +13,7 @@ For 3., this plugin can also be used to provide a route/page for editing, such a 1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See [`SavedObjectsTypeManagementDefinition`](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285) for explanation of its properties) 2. Register saved object type via `core.savedObjects.registerType(...)` as part of plugin server setup method -3. Implement a way to save the object via `savedObjectsClient.create(...)` +3. Implement a way to save the object (e.g. via `savedObjectsClient.create(...)` or a `savedObjectLoader`) 4. After these steps, you should be able to save objects and view/search for them in Saved Objects management (`/app/management/opensearch-dashboards/objects`) ## Enabling edit links from saved objects management From 159f4a6e24764ffb12a5a2b26fad35222f5982cb Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Thu, 4 Aug 2022 10:39:16 -0700 Subject: [PATCH 44/47] [D&D] Final fixes (#2071) * chore: Disable wizard by default Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: remove routes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: remove translations Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: nit fixes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: adds license to scss files Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: drop hover dark mode Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: nit fixes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- config/opensearch_dashboards.yml | 2 +- .../saved_objects_management/README.md | 2 +- src/plugins/wizard/.i18nrc.json | 7 ------ src/plugins/wizard/README.md | 2 +- .../common/wizard_saved_object_attributes.ts | 3 +-- src/plugins/wizard/config.ts | 2 +- .../wizard/public/application/_util.scss | 4 ++++ .../wizard/public/application/_variables.scss | 4 ++++ .../wizard/public/application/app.scss | 4 ++++ src/plugins/wizard/public/application/app.tsx | 5 ++-- .../components/data_tab/config_panel.scss | 7 ++++++ .../components/data_tab/dropbox.scss | 12 ++++++---- .../components/data_tab/field_selector.scss | 4 ++++ .../data_tab/field_selector_field.scss | 4 ++++ .../components/data_tab/index.scss | 4 ++++ .../components/data_tab/use/use_dropbox.tsx | 2 +- .../components/searchable_dropdown.scss | 4 ++++ .../application/components/side_nav.scss | 4 ++++ .../application/components/top_nav.scss | 4 ++++ .../application/components/workspace.scss | 4 ++++ .../embeddable/disabled_visualization.scss | 4 ++++ .../public/services/type_service/types.ts | 2 +- .../type_service/visualization_type.tsx | 2 +- src/plugins/wizard/server/plugin.ts | 5 ---- src/plugins/wizard/server/routes/index.ts | 23 ------------------- src/plugins/wizard/server/types.ts | 2 +- 26 files changed, 70 insertions(+), 52 deletions(-) delete mode 100644 src/plugins/wizard/.i18nrc.json delete mode 100644 src/plugins/wizard/server/routes/index.ts diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index 7757ba6761ad..2375ea123fe1 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -187,4 +187,4 @@ # Set the value of this setting to true to start exploring wizard # functionality in Visualization. -# wizard.enabled: true +# wizard.enabled: false diff --git a/src/plugins/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md index c896a1f43582..4afae978c258 100644 --- a/src/plugins/saved_objects_management/README.md +++ b/src/plugins/saved_objects_management/README.md @@ -1,4 +1,4 @@ -# Save objects management +# Saved objects management Provides a UI (via the `management` plugin) to find and manage all saved objects in one place (you can see the primary page by navigating to `/app/management/opensearch-dashboards/objects`). Not to be confused with the `savedObjects` plugin, which provides all the core capabilities of saved objects. diff --git a/src/plugins/wizard/.i18nrc.json b/src/plugins/wizard/.i18nrc.json deleted file mode 100644 index 2b511494a460..000000000000 --- a/src/plugins/wizard/.i18nrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "prefix": "wizard", - "paths": { - "wizard": "." - }, - "translations": ["translations/ja-JP.json"] -} diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md index 58a03a3e4adc..bb2d08fecbbb 100755 --- a/src/plugins/wizard/README.md +++ b/src/plugins/wizard/README.md @@ -8,7 +8,7 @@ To use this plugin, navigate to: Visualize -> Create Visualization -> Wizard -## Add a visualization (TODO: Cleanup before merging into mainline) +## Add a visualization All new visualizations currently reside in [public/visualizations](./public/visualizations). To add a new one, create a new visualization directory and add the required code (below) to setup and register a new vis type. diff --git a/src/plugins/wizard/common/wizard_saved_object_attributes.ts b/src/plugins/wizard/common/wizard_saved_object_attributes.ts index 9fd67783347b..1dc740d68637 100644 --- a/src/plugins/wizard/common/wizard_saved_object_attributes.ts +++ b/src/plugins/wizard/common/wizard_saved_object_attributes.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { integer } from '@opensearch-project/opensearch/api/types'; import { SavedObjectAttributes } from '../../../core/types'; export const WIZARD_SAVED_OBJECT = 'wizard'; @@ -13,5 +12,5 @@ export interface WizardSavedObjectAttributes extends SavedObjectAttributes { description?: string; visualizationState?: string; styleState?: string; - version: integer; + version: number; } diff --git a/src/plugins/wizard/config.ts b/src/plugins/wizard/config.ts index b6be3f718eea..79412f5c02ee 100644 --- a/src/plugins/wizard/config.ts +++ b/src/plugins/wizard/config.ts @@ -6,7 +6,7 @@ import { schema, TypeOf } from '@osd/config-schema'; export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.boolean({ defaultValue: false }), }); export type ConfigSchema = TypeOf<typeof configSchema>; diff --git a/src/plugins/wizard/public/application/_util.scss b/src/plugins/wizard/public/application/_util.scss index e1ef0bc048a7..165879c2ab12 100644 --- a/src/plugins/wizard/public/application/_util.scss +++ b/src/plugins/wizard/public/application/_util.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ @mixin scrollNavParent($template-row: none) { display: grid; min-height: 0; diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss index ab00f4eabbff..2baa9db275f2 100644 --- a/src/plugins/wizard/public/application/_variables.scss +++ b/src/plugins/wizard/public/application/_variables.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ @import "@elastic/eui/src/global_styling/variables/header"; @import "@elastic/eui/src/global_styling/variables/form"; diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss index 370c1153443b..5748cc4464cd 100644 --- a/src/plugins/wizard/public/application/app.scss +++ b/src/plugins/wizard/public/application/app.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ @import "variables"; .wizLayout { diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx index 7d578ee77cda..7c83e152418a 100644 --- a/src/plugins/wizard/public/application/app.tsx +++ b/src/plugins/wizard/public/application/app.tsx @@ -6,12 +6,11 @@ import React from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { EuiPage } from '@elastic/eui'; -import { SideNav } from './components/side_nav'; import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; +import { SideNav } from './components/side_nav'; +import { TopNav } from './components/top_nav'; import { Workspace } from './components/workspace'; - import './app.scss'; -import { TopNav } from './components/top_nav'; export const WizardApp = () => { // Render the application DOM. diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.scss b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss index b088dca2822c..fa7457592712 100644 --- a/src/plugins/wizard/public/application/components/data_tab/config_panel.scss +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss @@ -1,6 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ .wizConfig { + @include euiYScrollWithShadows; + background: $euiColorLightestShade; border-left: $euiBorderThin; + border-right: $euiBorderThin; position: relative; overflow-x: hidden; diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.scss b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss index 118c475ec590..a2013e4a02e3 100644 --- a/src/plugins/wizard/public/application/components/data_tab/dropbox.scss +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ .dropBox { margin-top: $euiSize; border-bottom: $euiBorderThin; @@ -49,12 +53,12 @@ animation: pop-in $euiAnimSpeedFast $euiAnimSlightResistance forwards; &.validField { - background-color: #a8d9e7; - border-color: #a8d9e7; + background-color: tintOrShade($euiColorPrimary, 80%, 70%); + border-color: tintOrShade($euiColorPrimary, 80%, 70%); &.canDrop { - background-color: rgba(0, 161, 201, 30%); - border-color: #006bb4; + background-color: tintOrShade($euiColorPrimary, 60%, 40%); + border-color: tintOrShade($euiColorPrimary, 30%, 20%); border-style: dashed; } } diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss index a7d43f6464f7..b2fb337e1dc2 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ @import "../../util"; .wizFieldSelector { diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss index ef163b2f084f..a7f093ad22a1 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ .wizFieldSelectorField { @include euiBottomShadowSmall; diff --git a/src/plugins/wizard/public/application/components/data_tab/index.scss b/src/plugins/wizard/public/application/components/data_tab/index.scss index dab7ed4cdda3..d242eac4ee2c 100644 --- a/src/plugins/wizard/public/application/components/data_tab/index.scss +++ b/src/plugins/wizard/public/application/components/data_tab/index.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ @import "../../util"; .wizDataTab { diff --git a/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx index af9122d8ebd7..8ec5df55020a 100644 --- a/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx @@ -65,7 +65,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { // Event handlers for each dropbox action type const onAddField = useCallback(() => { if (!aggConfigs || !indexPattern) { - throw new Error('Cannot create new field, missing parameters'); + throw new Error('Cannot create new field, missing aggConfigs or indexPattern'); } const aggConfig = aggConfigs.createAggConfig( diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss index bd997b5a075d..59f9771b35ac 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.scss +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ @import "../variables"; .searchableDropdown { diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss index fe3318c32132..021cd34ce190 100644 --- a/src/plugins/wizard/public/application/components/side_nav.scss +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ @import "../util"; @import "../variables"; diff --git a/src/plugins/wizard/public/application/components/top_nav.scss b/src/plugins/wizard/public/application/components/top_nav.scss index 0e9c5f46aa3f..cad0e3eebee2 100644 --- a/src/plugins/wizard/public/application/components/top_nav.scss +++ b/src/plugins/wizard/public/application/components/top_nav.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ .wizTopNav { grid-area: topNav; border-bottom: $euiBorderThin; diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss index bf7c3257274d..0b7b851cfddb 100644 --- a/src/plugins/wizard/public/application/components/workspace.scss +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ .wizWorkspace { display: grid; -ms-grid-rows: auto $euiSizeM 1fr; diff --git a/src/plugins/wizard/public/embeddable/disabled_visualization.scss b/src/plugins/wizard/public/embeddable/disabled_visualization.scss index 73f22386e80e..792bb1777ad6 100644 --- a/src/plugins/wizard/public/embeddable/disabled_visualization.scss +++ b/src/plugins/wizard/public/embeddable/disabled_visualization.scss @@ -1,3 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ .wizDisabledVisualization { width: 100%; display: grid; diff --git a/src/plugins/wizard/public/services/type_service/types.ts b/src/plugins/wizard/public/services/type_service/types.ts index b999d66ddf30..8542c0da0538 100644 --- a/src/plugins/wizard/public/services/type_service/types.ts +++ b/src/plugins/wizard/public/services/type_service/types.ts @@ -21,7 +21,7 @@ export interface VisualizationTypeOptions<T = any> { readonly title: string; readonly description?: string; readonly icon: IconType; - readonly stage?: 'beta' | 'production'; + readonly stage?: 'experimental' | 'production'; readonly ui: { containerConfig: { data: DataTabConfig; diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.tsx b/src/plugins/wizard/public/services/type_service/visualization_type.tsx index 90f30d8f8a95..305b7a716cf6 100644 --- a/src/plugins/wizard/public/services/type_service/visualization_type.tsx +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -13,7 +13,7 @@ export class VisualizationType implements IVisualizationType { public readonly title: string; public readonly description: string; public readonly icon: IconType; - public readonly stage: 'beta' | 'production'; + public readonly stage: 'experimental' | 'production'; public readonly ui: IVisualizationType['ui']; public readonly toExpression: (state: RootState) => Promise<string | undefined>; diff --git a/src/plugins/wizard/server/plugin.ts b/src/plugins/wizard/server/plugin.ts index 26781a236f84..25b7f27c1f81 100644 --- a/src/plugins/wizard/server/plugin.ts +++ b/src/plugins/wizard/server/plugin.ts @@ -13,7 +13,6 @@ import { import { WizardPluginSetup, WizardPluginStart } from './types'; import { capabilitiesProvider } from './capabilities_provider'; -import { defineRoutes } from './routes'; import { wizardSavedObjectType } from './saved_objects'; export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart> { @@ -25,10 +24,6 @@ export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart public setup({ capabilities, http, savedObjects }: CoreSetup) { this.logger.debug('wizard: Setup'); - const router = http.createRouter(); - - // Register server side APIs - defineRoutes(router); // Register saved object types savedObjects.registerType(wizardSavedObjectType); diff --git a/src/plugins/wizard/server/routes/index.ts b/src/plugins/wizard/server/routes/index.ts deleted file mode 100644 index 90b698ccb992..000000000000 --- a/src/plugins/wizard/server/routes/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { IRouter } from '../../../../core/server'; - -export function defineRoutes(router: IRouter) { - // Add server siude routes if needed like the example below - // router.get( - // { - // path: '/api/wizard/example', - // validate: false, - // }, - // async (context, request, response) => { - // return response.ok({ - // body: { - // time: new Date().toISOString(), - // }, - // }); - // } - // ); -} diff --git a/src/plugins/wizard/server/types.ts b/src/plugins/wizard/server/types.ts index 16780c13d60a..69f9ea0996d3 100644 --- a/src/plugins/wizard/server/types.ts +++ b/src/plugins/wizard/server/types.ts @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface // We need to export plugin server types, even if empty +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WizardPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface WizardPluginStart {} From 63e0bc43dd7986aa15d7a560542249a25ad7a5fa Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran <ashwinpc@amazon.com> Date: Thu, 4 Aug 2022 13:39:40 -0700 Subject: [PATCH 45/47] [D&D] Initial functional tests (#2070) * fix: searchable dropdown Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: broken empty test Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * test(FTR): Adds basic functional tests for D&D Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * test(FTR): Adds CI group 13 to test workflow Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * fix: nit fixes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: att test to jenkinsfile Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> * chore: minor nit fixes Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .github/workflows/build_and_test_workflow.yml | 2 +- Jenkinsfile | 1 + TESTING.md | 17 ++- .../components/data_tab/dropbox.tsx | 10 +- .../application/components/data_tab/title.tsx | 4 +- .../components/searchable_dropdown.scss | 43 ++++--- .../components/searchable_dropdown.tsx | 2 + .../application/components/workspace.tsx | 4 +- .../metric/to_expression.test.ts | 6 - test/common/config.js | 1 + test/examples/embeddables/dashboard.ts | 2 +- test/functional/apps/wizard/_base.ts | 48 ++++++++ .../apps/wizard/_experimental_vis.ts | 51 ++++++++ test/functional/apps/wizard/index.ts | 39 ++++++ test/functional/config.js | 5 + test/functional/page_objects/index.ts | 2 + test/functional/page_objects/wizard_page.ts | 113 ++++++++++++++++++ test/mocha_decorations.d.ts | 3 +- .../test_suites/panel_actions/index.js | 5 +- 19 files changed, 325 insertions(+), 33 deletions(-) delete mode 100644 src/plugins/wizard/public/visualizations/metric/to_expression.test.ts create mode 100644 test/functional/apps/wizard/_base.ts create mode 100644 test/functional/apps/wizard/_experimental_vis.ts create mode 100644 test/functional/apps/wizard/index.ts create mode 100644 test/functional/page_objects/wizard_page.ts diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index d82d0f45bb04..5a5b2a3ef2ae 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -75,7 +75,7 @@ jobs: name: Run functional tests strategy: matrix: - group: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ] + group: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 ] steps: - run: echo Running functional tests for ciGroup${{ matrix.group }} diff --git a/Jenkinsfile b/Jenkinsfile index c221237aeb93..2b967bc59d01 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -68,6 +68,7 @@ def functionalDynamicParallelSteps(image){ "ciGroup10", "ciGroup11", "ciGroup12", + "ciGroup13", ] for (int i = 0; i < ciGroups.size(); i++) { def currentCiGroup = ciGroups[i]; diff --git a/TESTING.md b/TESTING.md index 15c2792d3307..62a2dfd53956 100644 --- a/TESTING.md +++ b/TESTING.md @@ -50,7 +50,7 @@ To run specific functional tests, you can run by CI group: To debug functional tests: Say that you would want to debug a test in CI group 1, you can run the following command in your environment: -`node --debug-brk --inspect scripts/functional_tests.js --config test/functional/config.js --include ciGroup1 --debug` +`node --inspect-brk --inspect scripts/functional_tests.js --config test/functional/config.js --include ciGroup1 --debug` This will print off an address, to which you could open your chrome browser on your instance and navigate to `chrome://inspect/#devices` and inspect the functional test runner `scripts/functional_tests.js`. @@ -89,9 +89,24 @@ Automated testing is provided with Jenkins for Continuous Integration. Jenkins e Selenium tests are run in headless mode on CI. Locally the same tests will be executed in a real browser. You can activate headless mode by setting the environment variable: `export TEST_BROWSER_HEADLESS=1` +Since local Selenium tests are run in a real browser, the dev environment should have a desktop environment and Google Chrome or Chromium installed to run the tests. + By default the version of OpenSearch Dashboards will pull the snapshot of the same version of OpenSearch if available while running a few integration tests and for running functional tests. However, if the version of OpenSearch Dashboards is not available, you can build OpenSearch locally and point the functional test runner to the executable with: `export TEST_OPENSEARCH_FROM=[local directory of OpenSearch executable]` +Selinium tests require a chromedriver and a corresponding version of chrome to run properly. Depending on the version of chromedriver used, you may need to use a version of Google Chrome that is not the latest version. You can do this by running: + +```sh +# Enter the version of chrome that you want to install +CHROME_VERSION=100.0.4896.127-1 + +# Download Chrome to a temp directory +curl -sSL "https://dl.google.com/linux/linux_signing_key.pub" | sudo apt-key add - && wget -O /tmp/chrome.deb "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb" + +# Install/Downgrade Chrome +sudo apt-get install -y --allow-downgrades /tmp/chrome.deb +``` + # Misc Although Jest is the standard for this project, there are a few Mocha tests that still exist. You can run these tests by running: `yarn test:mocha` diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx index 9c6c9e290f8c..f6b7a6ca221b 100644 --- a/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx @@ -96,7 +96,12 @@ const DropboxComponent = ({ draggableId={id} index={index} > - <EuiPanel key={index} paddingSize="s" className="dropBox__field"> + <EuiPanel + key={index} + paddingSize="s" + className="dropBox__field" + data-test-subj={`dropBoxField-${dropboxId}-${index}`} + > <EuiText size="s" className="dropBox__field_text" onClick={() => onEditField(id)}> <a role="button" tabIndex={0}> {label} @@ -108,6 +113,7 @@ const DropboxComponent = ({ aria-label="clear-field" iconSize="s" onClick={() => animateDelete(id)} + data-test-subj="dropBoxRemoveBtn" /> </EuiPanel> </EuiDraggable> @@ -115,6 +121,7 @@ const DropboxComponent = ({ </EuiDroppable> {fields.length < limit && ( <EuiPanel + data-test-subj={`dropBoxAddField-${dropboxId}`} className={`dropBox__field dropBox__dropTarget ${ isValidDropTarget ? 'validField' : '' } ${canDrop ? 'canDrop' : ''}`} @@ -126,6 +133,7 @@ const DropboxComponent = ({ aria-label="clear-field" iconSize="s" onClick={() => onAddField()} + data-test-subj="dropBoxAddBtn" /> </EuiPanel> )} diff --git a/src/plugins/wizard/public/application/components/data_tab/title.tsx b/src/plugins/wizard/public/application/components/data_tab/title.tsx index 1f48db369669..8083efd05b0b 100644 --- a/src/plugins/wizard/public/application/components/data_tab/title.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/title.tsx @@ -18,7 +18,9 @@ export interface TitleProps { } export const Title = ({ title, isSecondary, closeMenu }: TitleProps) => { - const icon = isSecondary && <EuiIcon type="arrowLeft" onClick={closeMenu} />; + const icon = isSecondary && ( + <EuiIcon type="arrowLeft" onClick={closeMenu} data-test-subj="panelCloseBtn" /> + ); return ( <> <div className="wizConfig__title"> diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss index 59f9771b35ac..de03454dffbe 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.scss +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -6,25 +6,34 @@ .searchableDropdown { overflow: "hidden"; -} -.searchableDropdown .euiPopover, -.searchableDropdown .euiPopover__anchor { - width: 100%; -} + .euiFormControlLayout__childrenWrapper { + display: flex; + } -.searchableDropdown--fixedWidthChild { - width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2); -} + &--topDisplay { + padding-right: $euiSizeL; + font-size: $euiFontSizeS; + flex-grow: 1; -.searchableDropdown--topDisplay { - padding-right: $euiSizeL; - font-size: $euiFontSizeS; -} + .euiButtonEmpty__content { + justify-content: flex-start; + } + } + + &--fixedWidthChild { + width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2); + } + + &--selectableWrapper .euiSelectableList { + // When clicking on the selectable content it will "highlight" itself with a box shadow + // This turns that off + box-shadow: none !important; + margin: ($euiFormControlPadding * -1) - 4; + } -.searchableDropdown--selectableWrapper .euiSelectableList { - // When clicking on the selectable content it will "highlight" itself with a box shadow - // This turns that off - box-shadow: none !important; - margin: ($euiFormControlPadding * -1) - 4; + .euiPopover, + .euiPopover__anchor { + width: 100%; + } } diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx index 0d489b818167..3ff8300e8d48 100644 --- a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx @@ -118,6 +118,7 @@ export const SearchableDropdown = ({ <div className="searchableDropdown--selectableWrapper"> <EuiSelectable aria-label="Selectable options" + data-test-subj="searchableDropdownList" searchable options={localOptions} onChange={selectNewOption} @@ -145,6 +146,7 @@ export const SearchableDropdown = ({ size="s" style={{ textAlign: 'left' }} className="searchableDropdown--topDisplay" + data-test-subj="searchableDropdownValue" onClick={onButtonClick} > {selectedText} diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index ab320c8257ad..087cb656c622 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -89,11 +89,11 @@ export const Workspace: FC = ({ children }) => { <ExperimentalInfo /> </EuiFlexItem> </EuiFlexGroup> - <EuiPanel className="wizCanvas"> + <EuiPanel className="wizCanvas" data-test-subj="visualizationLoader"> {expression ? ( <ReactExpressionRenderer expression={expression} searchContext={searchContext} /> ) : ( - <EuiFlexItem className="wizWorkspace__empty"> + <EuiFlexItem className="wizWorkspace__empty" data-test-subj="emptyWorkspace"> <EuiEmptyPrompt title={<h2>Add a field to start</h2>} body={ diff --git a/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts b/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts deleted file mode 100644 index 9fd364ad256c..000000000000 --- a/src/plugins/wizard/public/visualizations/metric/to_expression.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// TODO: Cleanup the TODOs in './to_expression.ts' before writing tests for this function diff --git a/test/common/config.js b/test/common/config.js index 5db5748087a3..26abcc2fa586 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -78,6 +78,7 @@ export default function () { `--opensearchDashboards.branding.mark.defaultUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_default.svg`, `--opensearchDashboards.branding.mark.darkModeUrl=https://opensearch.org/assets/brand/SVG/Mark/opensearch_mark_darkmode.svg`, `--opensearchDashboards.branding.applicationTitle=OpenSearch`, + `--wizard.enabled=true`, ], }, services, diff --git a/test/examples/embeddables/dashboard.ts b/test/examples/embeddables/dashboard.ts index 2dac8c05849e..434bb9cb69cc 100644 --- a/test/examples/embeddables/dashboard.ts +++ b/test/examples/embeddables/dashboard.ts @@ -30,7 +30,7 @@ import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; -export const testDashboardInput = { +const testDashboardInput = { panels: { '1': { gridData: { diff --git a/test/functional/apps/wizard/_base.ts b/test/functional/apps/wizard/_base.ts new file mode 100644 index 000000000000..99cce7fce489 --- /dev/null +++ b/test/functional/apps/wizard/_base.ts @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'wizard']); + const log = getService('log'); + const retry = getService('retry'); + + describe('Basic tests for wizard app ', function () { + before(async () => { + log.debug('navigateToApp wizard'); + await PageObjects.wizard.navigateToCreateWizard(); + }); + + it('should be able to switch data sources', async () => { + const dataSourceValue = await PageObjects.wizard.selectDataSource( + PageObjects.wizard.index.LOGSTASH_NON_TIME_BASED + ); + + expect(dataSourceValue).to.equal(PageObjects.wizard.index.LOGSTASH_NON_TIME_BASED); + // TODO: Switch with a datasource with unique fields to test if it exists + }); + + it('should show visualization when a field is added', async () => { + await PageObjects.wizard.addField('metric', 'Average', 'machine.ram'); + const avgMachineRam = ['13,104,036,080.615', 'Average machine.ram']; + + await retry.try(async function tryingForTime() { + const metricValue = await PageObjects.wizard.getMetric(); + expect(avgMachineRam).to.eql(metricValue); + }); + }); + + it('should clear visualization when field is deleted', async () => { + await PageObjects.wizard.removeField('metric', 0); + + await retry.try(async function tryingForTime() { + const isEmptyWorkspace = await PageObjects.wizard.isEmptyWorkspace(); + expect(isEmptyWorkspace).to.be(true); + }); + }); + }); +} diff --git a/test/functional/apps/wizard/_experimental_vis.ts b/test/functional/apps/wizard/_experimental_vis.ts new file mode 100644 index 000000000000..e36c0254e22a --- /dev/null +++ b/test/functional/apps/wizard/_experimental_vis.ts @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from '@osd/expect'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../../src/plugins/visualizations/common/constants'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['visualize', 'wizard']); + const log = getService('log'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + + describe('experimental settings for wizard app ', function () { + it('should show an notification when creating wizard visualization', async () => { + log.debug('navigateToApp visualize'); + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.waitForVisualizationSelectPage(); + + // Try to find the wizard Vis type. + const wizardVisTypeExists = await PageObjects.visualize.hasVisType('wizard'); + expect(wizardVisTypeExists).to.be(true); + + // Create a new visualization + await PageObjects.visualize.clickVisType('wizard'); + + // Check that the experimental banner is there and state that this is experimental + const info = await PageObjects.wizard.getExperimentalInfo(); + expect(await info.getVisibleText()).to.contain('experimental'); + }); + + it('should not be available in the picker when disabled', async () => { + log.debug('navigateToApp visualize'); + await opensearchDashboardsServer.uiSettings.replace({ + [VISUALIZE_ENABLE_LABS_SETTING]: false, + }); + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.waitForVisualizationSelectPage(); + + // Try to find the wizard Vis type. + const wizardVisTypeExists = await PageObjects.visualize.hasVisType('wizard'); + expect(wizardVisTypeExists).to.be(false); + }); + + after(async () => { + // unset the experimental ui setting + await opensearchDashboardsServer.uiSettings.unset(VISUALIZE_ENABLE_LABS_SETTING); + }); + }); +} diff --git a/test/functional/apps/wizard/index.ts b/test/functional/apps/wizard/index.ts new file mode 100644 index 000000000000..24c4eb50c263 --- /dev/null +++ b/test/functional/apps/wizard/index.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FtrProviderContext } from '../../ftr_provider_context.d'; +import { UI_SETTINGS } from '../../../../src/plugins/data/common'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const browser = getService('browser'); + const log = getService('log'); + const opensearchArchiver = getService('opensearchArchiver'); + const opensearchDashboardsServer = getService('opensearchDashboardsServer'); + + describe('wizard app', function () { + this.tags('ciGroup13'); + + before(async function () { + log.debug('Starting wizard before method'); + await browser.setWindowSize(1280, 800); + await opensearchArchiver.loadIfNeeded('logstash_functional'); + await opensearchArchiver.loadIfNeeded('long_window_logstash'); + await opensearchArchiver.loadIfNeeded('visualize'); + await opensearchDashboardsServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + [UI_SETTINGS.FORMAT_BYTES_DEFAULT_PATTERN]: '0,0.[000]b', + }); + }); + + after(async () => { + await opensearchArchiver.unload('logstash_functional'); + await opensearchArchiver.unload('long_window_logstash'); + await opensearchArchiver.unload('visualize'); + }); + + loadTestFile(require.resolve('./_base')); + loadTestFile(require.resolve('./_experimental_vis')); + }); +} diff --git a/test/functional/config.js b/test/functional/config.js index 20d3360f0fe5..bb6be73ebd82 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -48,6 +48,7 @@ export default async function ({ readConfigFile }) { require.resolve('./apps/status_page'), require.resolve('./apps/timeline'), require.resolve('./apps/visualize'), + require.resolve('./apps/wizard'), ], pageObjects, services, @@ -91,6 +92,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/visualize', hash: '/', }, + wizard: { + pathname: '/app/wizard', + hash: '/', + }, dashboard: { pathname: '/app/dashboards', hash: '/list', diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 46f8e60e73db..d09445d47026 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -44,6 +44,7 @@ import { TimePickerProvider } from './time_picker'; import { TimelinePageProvider } from './timeline_page'; import { VisualBuilderPageProvider } from './visual_builder_page'; import { VisualizePageProvider } from './visualize_page'; +import { WizardPageProvider } from './wizard_page'; import { VisualizeEditorPageProvider } from './visualize_editor_page'; import { VisualizeChartPageProvider } from './visualize_chart_page'; import { TileMapPageProvider } from './tile_map_page'; @@ -68,6 +69,7 @@ export const pageObjects = { timePicker: TimePickerProvider, visualBuilder: VisualBuilderPageProvider, visualize: VisualizePageProvider, + wizard: WizardPageProvider, visEditor: VisualizeEditorPageProvider, visChart: VisualizeChartPageProvider, tileMap: TileMapPageProvider, diff --git a/test/functional/page_objects/wizard_page.ts b/test/functional/page_objects/wizard_page.ts new file mode 100644 index 000000000000..08ed41df57d5 --- /dev/null +++ b/test/functional/page_objects/wizard_page.ts @@ -0,0 +1,113 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function WizardPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + const browser = getService('browser'); + const comboBox = getService('comboBox'); + const { common, header } = getPageObjects(['common', 'header']); + + /** + * This page object contains the visualization type selection, the landing page, + * and the open/save dialog functions + */ + class WizardPage { + index = { + LOGSTASH_TIME_BASED: 'logstash-*', + LOGSTASH_NON_TIME_BASED: 'logstash*', + }; + + public async navigateToCreateWizard() { + await common.navigateToApp('wizard'); + await header.waitUntilLoadingHasFinished(); + } + + public async getExperimentalInfo() { + return await testSubjects.find('experimentalVisInfo'); + } + + public async findFieldByName(name: string) { + const fieldSearch = await testSubjects.find('fieldFilterSearchInput'); + await fieldSearch.type(name); + } + + public async getDataSourceSelector() { + const dataSourceDropdown = await testSubjects.find('searchableDropdownValue'); + return await dataSourceDropdown.getVisibleText(); + } + + public async selectDataSource(dataSource: string) { + await testSubjects.click('searchableDropdownValue'); + await find.clickByCssSelector( + `[data-test-subj="searchableDropdownList"] [title="${dataSource}"]` + ); + const dataSourceDropdown = await testSubjects.find('searchableDropdownValue'); + return await dataSourceDropdown.getVisibleText(); + } + + public async addField( + dropBoxId: string, + aggValue: string, + fieldValue: string, + returnToMainPanel = true + ) { + await testSubjects.click(`dropBoxAddField-${dropBoxId} > dropBoxAddBtn`); + await common.sleep(500); + const aggComboBoxElement = await testSubjects.find('defaultEditorAggSelect'); + await comboBox.setElement(aggComboBoxElement, aggValue); + await common.sleep(500); + const fieldComboBoxElement = await testSubjects.find('visDefaultEditorField'); + await comboBox.setElement(fieldComboBoxElement, fieldValue); + await common.sleep(500); + + if (returnToMainPanel) { + await testSubjects.click('panelCloseBtn'); + await common.sleep(500); + } + } + + public async removeField(dropBoxId: string, aggNth: number) { + await testSubjects.click(`dropBoxField-${dropBoxId}-${aggNth} > dropBoxRemoveBtn`); + await common.sleep(500); + } + + // TODO: Fix. Currently it is not able to locate the dropbox location correctly, even if it identifies the element correctly + public async dragDropField(field: string, dropBoxId: string) { + const fieldEle = await testSubjects.find(`field-${field}-showDetails`); + const dropBoxEle = await testSubjects.find(`dropBoxAddField-${dropBoxId}`); + await browser.dragAndDrop({ location: fieldEle }, { location: dropBoxEle }); + } + + public async clearFieldSearchInput() { + const fieldSearch = await testSubjects.find('fieldFilterSearchInput'); + await fieldSearch.clearValue(); + } + + public async getMetric() { + const elements = await find.allByCssSelector( + '[data-test-subj="visualizationLoader"] .mtrVis__container' + ); + const values = await Promise.all( + elements.map(async (element) => { + const text = await element.getVisibleText(); + return text; + }) + ); + return values + .filter((item) => item.length > 0) + .reduce((arr: string[], item) => arr.concat(item.split('\n')), []); + } + + public async isEmptyWorkspace() { + const elements = await find.allByCssSelector('[data-test-subj="emptyWorkspace"]'); + return elements.length === 1; + } + } + + return new WizardPage(); +} diff --git a/test/mocha_decorations.d.ts b/test/mocha_decorations.d.ts index f2e9f2a2dcac..025fd6664c3d 100644 --- a/test/mocha_decorations.d.ts +++ b/test/mocha_decorations.d.ts @@ -42,7 +42,8 @@ type Tags = | 'ciGroup9' | 'ciGroup10' | 'ciGroup11' - | 'ciGroup12'; + | 'ciGroup12' + | 'ciGroup13'; // We need to use the namespace here to match the Mocha definition declare module 'mocha' { diff --git a/test/plugin_functional/test_suites/panel_actions/index.js b/test/plugin_functional/test_suites/panel_actions/index.js index 3fddfd8261e8..a94cfbed1c53 100644 --- a/test/plugin_functional/test_suites/panel_actions/index.js +++ b/test/plugin_functional/test_suites/panel_actions/index.js @@ -30,11 +30,12 @@ import path from 'path'; -export const OPENSEARCH_DASHBOARDS_ARCHIVE_PATH = path.resolve( +const OPENSEARCH_DASHBOARDS_ARCHIVE_PATH = path.resolve( __dirname, '../../../functional/fixtures/opensearch_archiver/dashboard/current/opensearch_dashboards' ); -export const DATA_ARCHIVE_PATH = path.resolve( + +const DATA_ARCHIVE_PATH = path.resolve( __dirname, '../../../functional/fixtures/opensearch_archiver/dashboard/current/data' ); From aa4df76bf212b5c0c2b89ec1fc87a8333f26b41c Mon Sep 17 00:00:00 2001 From: Ashwin Pc <ashwinpc@amazon.com> Date: Thu, 4 Aug 2022 23:07:00 +0000 Subject: [PATCH 46/47] chore: downgrade redux-toolkit for plugin compatibility Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- package.json | 2 +- yarn.lock | 40 +++++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index c83730e6a3a9..b796d60d92c1 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "@osd/std": "1.0.0", "@osd/ui-framework": "1.0.0", "@osd/ui-shared-deps": "1.0.0", - "@reduxjs/toolkit": "^1.6.2", + "@reduxjs/toolkit": "^1.6.1", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index ef19a747860b..7aa2b9359a83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2511,6 +2511,16 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503" integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg== +"@reduxjs/toolkit@^1.6.1": + version "1.8.3" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.3.tgz#9c6a9c497bde43a67618d37a4175a00ae12efeb2" + integrity sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg== + dependencies: + immer "^9.0.7" + redux "^4.1.2" + redux-thunk "^2.4.1" + reselect "^4.1.5" + "@rushstack/node-core-library@3.45.1": version "3.45.1" resolved "https://registry.yarnpkg.com/@rushstack/node-core-library/-/node-core-library-3.45.1.tgz#787361b61a48d616eb4b059641721a3dc138f001" @@ -2544,16 +2554,6 @@ colors "~1.2.1" string-argv "~0.3.1" -"@reduxjs/toolkit@^1.6.2": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37" - integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA== - dependencies: - immer "^9.0.6" - redux "^4.1.0" - redux-thunk "^2.3.0" - reselect "^4.0.0" - "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301" @@ -10009,6 +10009,11 @@ immer@^9.0.6: resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== +immer@^9.0.7: + version "9.0.15" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc" + integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -14981,7 +14986,7 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" -redux-thunk@^2.3.0: +redux-thunk@^2.3.0, redux-thunk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q== @@ -14993,10 +14998,10 @@ redux@^4.0.0, redux@^4.0.4, redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" -redux@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" - integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== +redux@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== dependencies: "@babel/runtime" "^7.9.2" @@ -15339,6 +15344,11 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== +reselect@^4.1.5: + version "4.1.6" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.6.tgz#19ca2d3d0b35373a74dc1c98692cdaffb6602656" + integrity sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ== + resize-observer-polyfill@^1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/@4lolo/resize-observer-polyfill/-/resize-observer-polyfill-1.5.2.tgz#58868fc7224506236b5550d0c68357f0a874b84b" From d79f3e072d2db9a58eb48782ff0dad5893a90047 Mon Sep 17 00:00:00 2001 From: Ashwin Pc <ashwinpc@amazon.com> Date: Thu, 4 Aug 2022 23:10:34 +0000 Subject: [PATCH 47/47] chore: add docker config flag Signed-off-by: Ashwin Pc <ashwinpc@amazon.com> --- .../docker_generator/resources/bin/opensearch-dashboards-docker | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker index 9b90d70711f5..cd81f8d16c3f 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/opensearch-dashboards-docker @@ -149,6 +149,7 @@ opensearch_dashboards_vars=( telemetry.optIn telemetry.optInStatusUrl telemetry.sendUsageFrom + wizard.enabled ) longopts=''