diff --git a/.github/workflows/pr_check_workflow.yml b/.github/workflows/pr_check_workflow.yml index d1d2d4b2db65..cfe3ef6235f4 100644 --- a/.github/workflows/pr_check_workflow.yml +++ b/.github/workflows/pr_check_workflow.yml @@ -69,7 +69,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/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml index fc90e92ab64e..e60e2e3da983 100644 --- a/config/opensearch_dashboards.yml +++ b/config/opensearch_dashboards.yml @@ -216,3 +216,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: false diff --git a/package.json b/package.json index 54e37652d56c..da1cf759e692 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "@osd/std": "1.0.0", "@osd/ui-framework": "1.0.0", "@osd/ui-shared-deps": "1.0.0", + "@reduxjs/toolkit": "^1.6.1", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", 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='' 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/saved_objects_management/README.md b/src/plugins/saved_objects_management/README.md new file mode 100644 index 000000000000..4afae978c258 --- /dev/null +++ b/src/plugins/saved_objects_management/README.md @@ -0,0 +1,42 @@ +# 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. + +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 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`](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 (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 + +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 +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 + +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 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 +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 ) => { - 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/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; setAggParamValue: ( aggId: AggId, paramName: T, 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; schemas: Schema[]; hideCustomLabel?: boolean; } 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_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/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/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/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`
-

-

-

-

- - + + + +

- - + + + +

- - + + + +

- - + + + +

-

-

-

-

- - + + + +

- - + + + +

- - + + + +

- - + + + +

{ - // 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) { @@ -222,7 +220,7 @@ class TypeSelection extends React.Component { 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', @@ -278,7 +276,7 @@ class TypeSelection extends React.Component ); diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md new file mode 100755 index 000000000000..bb2d08fecbbb --- /dev/null +++ b/src/plugins/wizard/README.md @@ -0,0 +1,36 @@ +# Wizard + +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 + +To use this plugin, navigate to: + +Visualize -> Create Visualization -> Wizard + +## 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. + +### Anatomy of a visualization + +``` +metric/ +├─ index.ts +├─ metric_viz_type.ts +├─ to_expression.ts +├─ components/ + ├─ metric_viz_options.tsx +``` + +Outline: +- `index.ts`: Exposes the `createConfig` function that is used to register the viz type +- `_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 +- `_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 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. + diff --git a/src/plugins/wizard/common/index.ts b/src/plugins/wizard/common/index.ts new file mode 100644 index 000000000000..b55653d41549 --- /dev/null +++ b/src/plugins/wizard/common/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 new file mode 100644 index 000000000000..1dc740d68637 --- /dev/null +++ b/src/plugins/wizard/common/wizard_saved_object_attributes.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from '../../../core/types'; + +export const WIZARD_SAVED_OBJECT = 'wizard'; + +export interface WizardSavedObjectAttributes extends SavedObjectAttributes { + title: string; + description?: string; + visualizationState?: string; + styleState?: string; + version: number; +} diff --git a/src/plugins/wizard/config.ts b/src/plugins/wizard/config.ts new file mode 100644 index 000000000000..79412f5c02ee --- /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: false }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/wizard/opensearch_dashboards.json b/src/plugins/wizard/opensearch_dashboards.json new file mode 100644 index 000000000000..020ca5f2ab9b --- /dev/null +++ b/src/plugins/wizard/opensearch_dashboards.json @@ -0,0 +1,22 @@ +{ + "id": "wizard", + "version": "1.0.0", + "opensearchDashboardsVersion": "opensearchDashboards", + "server": true, + "ui": true, + "requiredPlugins": [ + "navigation", + "charts", + "data", + "opensearchDashboardsReact", + "opensearchDashboardsUtils", + "savedObjects", + "embeddable", + "expressions", + "dashboard", + "visualizations", + "opensearchUiShared", + "visDefaultEditor" + ], + "optionalPlugins": [] +} diff --git a/src/plugins/wizard/public/application/_util.scss b/src/plugins/wizard/public/application/_util.scss new file mode 100644 index 000000000000..165879c2ab12 --- /dev/null +++ b/src/plugins/wizard/public/application/_util.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@mixin scrollNavParent($template-row: none) { + display: grid; + min-height: 0; + + @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 new file mode 100644 index 000000000000..2baa9db275f2 --- /dev/null +++ b/src/plugins/wizard/public/application/_variables.scss @@ -0,0 +1,9 @@ +/* + * 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"; + +$osdHeaderOffset: $euiHeaderHeightCompensation; +$wizSideNavWidth: 470px; diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss new file mode 100644 index 000000000000..5748cc4464cd --- /dev/null +++ b/src/plugins/wizard/public/application/app.scss @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "variables"; + +.wizLayout { + padding: 0; + display: grid; + grid-template: min-content 1fr / #{$wizSideNavWidth} 1fr; + grid-template-areas: + "topNav topNav" + "sideNav workspace"; + height: calc(100vh - #{$osdHeaderOffset}); +} + +.headerIsExpanded .wizLayout { + height: calc(100vh - #{$osdHeaderOffset * 2}); +} diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx new file mode 100644 index 000000000000..7c83e152418a --- /dev/null +++ b/src/plugins/wizard/public/application/app.tsx @@ -0,0 +1,28 @@ +/* + * 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 { 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'; + +export const WizardApp = () => { + // Render the application DOM. + return ( + + + + + + + + + + ); +}; 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..08c4f86008bf --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_source_select.tsx @@ -0,0 +1,50 @@ +/* + * 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 { 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: , + }; +} + +export const DataSourceSelect = () => { + const { indexPatterns, loading, error, selected } = useIndexPatterns(); + const dispatch = useTypedDispatch(); + + return ( + { + 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/data_tab/config_panel.scss b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss new file mode 100644 index 000000000000..fa7457592712 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss @@ -0,0 +1,45 @@ +/* + * 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; + + &__section { + width: 100%; + transition: transform $euiAnimSpeedNormal 0s $euiAnimSlightResistance; + } + + &__title { + padding: $euiSizeS; + padding-bottom: 0; + + &.showDivider { + border-bottom: 1px solid $euiColorLightShade; + } + } + + &__content { + padding: $euiSizeS; + } + + &__aggEditor { + padding: 0 $euiSizeM; + } + + &--secondary { + position: absolute; + top: 0; + left: 100%; + } + + &.showSecondary > .wizConfig__section { + transform: translateX(-100%); + } +} 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..be0ec12bbff2 --- /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 } from '@elastic/eui'; +import React from 'react'; +import { useVisualizationType } from '../../utils/use'; +import { useTypedSelector } from '../../utils/state_management'; +import './config_panel.scss'; +import { mapSchemaToAggPanel } from './schema_to_dropbox'; +import { SecondaryPanel } from './secondary_panel'; + +export function ConfigPanel() { + const vizType = useVisualizationType(); + const editingState = useTypedSelector( + (state) => state.visualization.activeVisualization?.draftAgg + ); + const schemas = vizType.ui.containerConfig.data.schemas; + + if (!schemas) return null; + + const mainPanel = mapSchemaToAggPanel(schemas); + + return ( + +

{mainPanel}
+ + + ); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/dropbox.scss b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss new file mode 100644 index 000000000000..a2013e4a02e3 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.scss @@ -0,0 +1,106 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.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 / 2; + padding: $euiSizeS - ($euiSizeXS / 2) $euiSizeS $euiSizeS $euiSizeS; + background-color: $euiColorLightShade; + border-radius: $euiBorderRadius; + } + + &__field { + display: grid; + grid-template-columns: 1fr auto; + grid-gap: $euiSizeS; + padding: $euiSizeS $euiSizeM; + align-items: center; + } + + &__draggable { + 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 { + text-overflow: ellipsis; + overflow: hidden; + } + + &__dropTarget { + color: $euiColorDarkShade; + grid-template-columns: 1fr auto; + transform-origin: top; + animation: pop-in $euiAnimSpeedFast $euiAnimSlightResistance forwards; + + &.validField { + background-color: tintOrShade($euiColorPrimary, 80%, 70%); + border-color: tintOrShade($euiColorPrimary, 80%, 70%); + + &.canDrop { + background-color: tintOrShade($euiColorPrimary, 60%, 40%); + border-color: tintOrShade($euiColorPrimary, 30%, 20%); + border-style: dashed; + } + } + } +} + +@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 new file mode 100644 index 000000000000..f6b7a6ca221b --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/dropbox.tsx @@ -0,0 +1,152 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFormRow, + EuiPanel, + EuiText, + DropResult, +} from '@elastic/eui'; +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; + id: string; +} + +interface DropboxProps extends IDropState { + id: string; + label: string; + fields: DropboxDisplay[]; + limit?: number; + onAddField: () => void; + onEditField: (id: string) => void; + onDeleteField: (id: string) => void; + onReorderField: ({ + sourceAggId, + destinationAggId, + }: { + sourceAggId: string; + destinationAggId: string; + }) => void; + dropProps: IDropAttributes; +} + +const DropboxComponent = ({ + id: dropboxId, + label: boxLabel, + fields, + onAddField, + onDeleteField, + onEditField, + onReorderField, + limit = 1, + isValidDropTarget, + canDrop, + dropProps, +}: DropboxProps) => { + const prefersReducedMotion = usePrefersReducedMotion(); + const [closing, setClosing] = useState(false); + const handleDragEnd = useCallback( + ({ source, destination }: DropResult) => { + if (!destination) return; + + onReorderField({ + sourceAggId: fields[source.index].id, + destinationAggId: fields[destination.index].id, + }); + }, + [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 ( + + +
+ + {fields.map(({ id, label }, index) => ( + + + onEditField(id)}> + + {label} + + + animateDelete(id)} + data-test-subj="dropBoxRemoveBtn" + /> + + + ))} + + {fields.length < limit && ( + + Click or drop to add + onAddField()} + data-test-subj="dropBoxAddBtn" + /> + + )} +
+
+
+ ); +}; + +const Dropbox = React.memo((dropBox: UseDropboxProps) => { + const props = useDropbox(dropBox); + + return ; +}); + +export { Dropbox, DropboxComponent, DropboxProps }; 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..62dcf2c2b953 --- /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/visualization_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..b2fb337e1dc2 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@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/components/data_tab/field_selector.tsx new file mode 100644 index 000000000000..a794093bc00d --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { EuiFlexItem, EuiAccordion, 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'; +import { useIndexPatterns } from '../../utils/use'; +import { getAvailableFields } from './utils'; + +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 indexPattern = useIndexPatterns().selected; + const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); + const [filteredFields, setFilteredFields] = useState([]); + + useEffect(() => { + const indexFields = indexPattern?.fields ?? []; + const filteredSubset = getAvailableFields(indexFields).filter((field) => + field.displayName.includes(fieldSearchValue) + ); + + setFilteredFields(filteredSubset); + return; + }, [fieldSearchValue, indexPattern?.fields]); + + const fields = useMemo( + () => + filteredFields?.reduce( + (fieldGroups, currentField) => { + const category = getFieldCategory(currentField); + fieldGroups[category].push(currentField); + + return fieldGroups; + }, + { + categorical: [], + numerical: [], + meta: [], + } + ), + [filteredFields] + ); + + 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..a7f093ad22a1 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.wizFieldSelectorField { + @include euiBottomShadowSmall; + + background-color: $euiColorEmptyShade; + border: $euiBorderThin; + margin-top: $euiSizeXS; + + & > .osdFieldButton__button { + padding: 0; + } + + & .osdFieldButton__name { + padding: $euiSizeS $euiSizeS $euiSizeS 0; + } + + & > button { + 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 new file mode 100644 index 000000000000..963660d8eb81 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx @@ -0,0 +1,92 @@ +/* + * 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 '../../../../../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; +} + +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({ + namespace: 'field-data', + value: { + displayName, + name, + type, + }, + }); + + 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..d242eac4ee2c --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/index.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@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..1f880f20698d --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { FieldSelector } from './field_selector'; + +import './index.scss'; +import { ConfigPanel } from './config_panel'; + +export const DATA_TAB_ID = 'data_tab'; + +export const DataTab = () => { + return ( +
+ + +
+ ); +}; diff --git a/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx b/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx new file mode 100644 index 000000000000..37c55e25be99 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/schema_to_dropbox.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +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) => { + return ; + }); + + return ( + <> + + <div className="wizConfig__content">{panelComponents}</div> + </> + ); +}; 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 new file mode 100644 index 000000000000..dfbb148f07a2 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/secondary_panel.tsx @@ -0,0 +1,120 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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'; +import { useIndexPatterns, useVisualizationType } from '../../utils/use'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { WizardServices } from '../../../types'; +import { IAggType } from '../../../../../data/public'; +import { saveDraftAgg, editDraftAgg } from '../../utils/state_management/visualization_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 isEditorValid = useTypedSelector( + (state) => state.metadata.editorState.validity[EDITOR_KEY] + ); + const [touched, setTouched] = useState(false); + const dispatch = useTypedDispatch(); + const vizType = useVisualizationType(); + const indexPattern = useIndexPatterns().selected; + 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 selectedSchema = useMemo( + () => schemas.find((schema) => schema.name === aggConfig?.schema), + [aggConfig?.schema, schemas] + ); + + const showAggParamEditor = !!(aggConfig && indexPattern); + + const closeMenu = useCallback(() => { + dispatch(editDraftAgg(undefined)); + }, [dispatch]); + + const handleSetValid = useCallback( + (isValid: boolean) => { + // Set validity state globally + dispatch( + setValidity({ + key: EDITOR_KEY, + valid: isValid, + }) + ); + }, + [dispatch] + ); + + // 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); + } + }, + 200, + [draftAgg, isEditorValid] + ); + + return ( + <div className="wizConfig__section wizConfig--secondary"> + <Title title={selectedSchema?.title ?? 'Edit'} isSecondary closeMenu={closeMenu} /> + {showAggParamEditor && ( + <DefaultEditorAggParams + className="wizConfig__aggEditor" + agg={aggConfig!} + indexPattern={indexPattern!} + setValidity={handleSetValid} + setTouched={setTouched} + schemas={schemas} + formIsTouched={touched} + groupName={selectedSchema?.group ?? 'none'} + metricAggs={[]} + state={{ + data: {}, + description: '', + title: '', + }} + setAggParamValue={function <T extends string | number | symbol>( + aggId: string, + paramName: T, + value: any + ): void { + aggConfig.params[paramName] = value; + dispatch(editDraftAgg(aggConfig.serialize())); + }} + onAggTypeChange={function (aggId: string, aggType: IAggType): void { + aggConfig.type = aggType; + dispatch(editDraftAgg(aggConfig.serialize())); + }} + /> + )} + </div> + ); +} diff --git a/src/plugins/wizard/public/application/components/data_tab/title.tsx b/src/plugins/wizard/public/application/components/data_tab/title.tsx new file mode 100644 index 000000000000..8083efd05b0b --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/title.tsx @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiIcon, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +export interface TitleProps { + title: string; + isSecondary?: boolean; + closeMenu?: () => void; +} + +export const Title = ({ title, isSecondary, closeMenu }: TitleProps) => { + const icon = isSecondary && ( + <EuiIcon type="arrowLeft" onClick={closeMenu} data-test-subj="panelCloseBtn" /> + ); + return ( + <> + <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/components/data_tab/use/index.ts b/src/plugins/wizard/public/application/components/data_tab/use/index.ts new file mode 100644 index 000000000000..64265e655fa7 --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/use/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useDropbox } from './use_dropbox'; 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 new file mode 100644 index 000000000000..8ec5df55020a --- /dev/null +++ b/src/plugins/wizard/public/application/components/data_tab/use/use_dropbox.tsx @@ -0,0 +1,209 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 { DropboxDisplay, DropboxProps } from '../dropbox'; +import { useDrop } from '../../../utils/drag_drop'; +import { + editDraftAgg, + reorderAgg, + updateAggConfigParams, +} from '../../../utils/state_management/visualization_slice'; +import { useIndexPatterns } 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 interface UseDropboxProps extends Pick<DropboxProps, 'id' | 'label'> { + schema: Schema; +} + +export const useDropbox = (props: UseDropboxProps): DropboxProps => { + const { id: dropboxId, label, schema } = props; + const [validAggTypes, setValidAggTypes] = useState<string[]>([]); + const dispatch = useTypedDispatch(); + const indexPattern = useIndexPatterns().selected; + const { + services: { + data: { + search: { aggs: aggService }, + }, + }, + } = useOpenSearchDashboards<WizardServices>(); + const aggConfigParams = useTypedSelector( + (state) => state.visualization.activeVisualization?.aggConfigParams + ); + + const aggConfigs = useMemo(() => { + return indexPattern && aggService.createAggConfigs(indexPattern, cloneDeep(aggConfigParams)); + }, [aggConfigParams, aggService, indexPattern]); + + const aggs = useMemo(() => aggConfigs?.aggs ?? [], [aggConfigs?.aggs]); + + const dropboxAggs = aggs.filter((agg) => agg.schema === schema.name); + + const displayFields: DropboxDisplay[] = useMemo( + () => + dropboxAggs?.map( + (agg): DropboxDisplay => ({ + id: agg.id, + label: agg.makeLabel(), + }) + ) || [], + [dropboxAggs] + ); + + // Event handlers for each dropbox action type + const onAddField = useCallback(() => { + if (!aggConfigs || !indexPattern) { + throw new Error('Cannot create new field, missing aggConfigs or indexPattern'); + } + + const aggConfig = aggConfigs.createAggConfig( + { + 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, + } + ); + + 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'); + } + + dispatch(editDraftAgg(newAggConfig.serialize())); + }, [aggConfigs, aggService, aggs, dispatch, indexPattern, schema.name]); + + const onEditField = useCallback( + (aggId: string) => { + const aggConfig = aggConfigs?.aggs.find((agg) => agg.id === aggId); + + if (!aggConfig) { + throw new Error('Could not find agg in aggConfigs'); + } + + dispatch(editDraftAgg(aggConfig.serialize())); + }, + [aggConfigs?.aggs, dispatch] + ); + + const onDeleteField = useCallback( + (aggId: string) => { + const newAggs = aggConfigs?.aggs.filter((agg) => agg.id !== aggId); + + if (newAggs) { + dispatch(updateAggConfigParams(newAggs.map((agg) => agg.serialize()))); + } + }, + [aggConfigs?.aggs, dispatch] + ); + + const onDropField = useCallback( + (data: FieldDragDataType['value']) => { + 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: allowedAggTypes[0] || validAggTypes[0], + schema: schema.name, + params: { + field: fieldName, + }, + }); + + if (aggConfigs) { + dispatch(updateAggConfigParams(aggConfigs.aggs.map((agg) => agg.serialize()))); + } + }, + [aggConfigs, dispatch, schema.defaults, schema.name, validAggTypes] + ); + + const onReorderField = useCallback( + ({ sourceAggId, destinationAggId }) => { + dispatch( + reorderAgg({ + sourceId: sourceAggId, + destinationId: destinationAggId, + }) + ); + }, + [dispatch] + ); + + const [dropProps, { isValidDropTarget, dragData, ...dropState }] = useDrop( + 'field-data', + onDropField + ); + + useEffect(() => { + const getValidAggTypes = () => { + if (!dragData || schema.group === 'none') return []; + + const indexField = getIndexPatternField(dragData.name, indexPattern?.fields ?? []); + + if (!indexField) return []; + + // 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) + ); + }; + + setValidAggTypes(getValidAggTypes()); + + return () => { + setValidAggTypes([]); + }; + }, [aggService.types, dragData, indexPattern?.fields, schema.aggFilter, schema.group]); + + const canDrop = validAggTypes.length > 0 && schema.max > dropboxAggs.length; + + return { + id: dropboxId, + label, + limit: schema.max, + fields: displayFields, + onAddField, + onEditField, + onDeleteField, + onReorderField, + ...dropState, + dragData, + isValidDropTarget: canDrop, + dropProps, + }; +}; + +const getIndexPatternField = (indexFieldName: string, availableFields: IndexPatternField[]) => + availableFields.find(({ name }) => name === indexFieldName); 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/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/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/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss new file mode 100644 index 000000000000..de03454dffbe --- /dev/null +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "../variables"; + +.searchableDropdown { + overflow: "hidden"; + + .euiFormControlLayout__childrenWrapper { + display: flex; + } + + &--topDisplay { + padding-right: $euiSizeL; + font-size: $euiFontSizeS; + flex-grow: 1; + + .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; + } + + .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 new file mode 100644 index 000000000000..3ff8300e8d48 --- /dev/null +++ b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx @@ -0,0 +1,177 @@ +/* + * 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" + data-test-subj="searchableDropdownList" + 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" + size="s" + style={{ textAlign: 'left' }} + className="searchableDropdown--topDisplay" + data-test-subj="searchableDropdownValue" + 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 new file mode 100644 index 000000000000..021cd34ce190 --- /dev/null +++ b/src/plugins/wizard/public/application/components/side_nav.scss @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +@import "../util"; +@import "../variables"; + +.wizSidenav { + @include scrollNavParent(auto 1fr); + + grid-area: sideNav; + border-right: $euiBorderThin; +} + +.wizSidenavTabs { + .euiTab__content { + text-transform: capitalize; + } + + @include scrollNavParent(min-content 1fr); + + & > [role="tabpanel"] { + @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 new file mode 100644 index 000000000000..4e4291f1c447 --- /dev/null +++ b/src/plugins/wizard/public/application/components/side_nav.tsx @@ -0,0 +1,48 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactElement } from 'react'; +import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import './side_nav.scss'; +import { useVisualizationType } from '../utils/use'; +import { DataSourceSelect } from './data_source_select'; +import { DataTab } from './data_tab'; +import { StyleTabConfig } from '../../services/type_service'; + +export const SideNav = () => { + const { + ui: { containerConfig }, + } = useVisualizationType(); + + const tabs: EuiTabbedContentTab[] = Object.entries(containerConfig).map( + ([containerName, config]) => { + let content: null | ReactElement = 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"> + <div className="wizDatasourceSelect"> + <DataSourceSelect /> + </div> + <EuiTabbedContent tabs={tabs} className="wizSidenavTabs" /> + </section> + ); +}; 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..cad0e3eebee2 --- /dev/null +++ b/src/plugins/wizard/public/application/components/top_nav.scss @@ -0,0 +1,8 @@ +/* + * 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/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx new file mode 100644 index 000000000000..fd1d387c6a50 --- /dev/null +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 { WizardServices } from '../../types'; + +import './top_nav.scss'; +import { useIndexPatterns, useSavedWizardVis } from '../utils/use'; +import { useTypedSelector } from '../utils/state_management'; + +export const TopNav = () => { + // id will only be set for the edit route + const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); + const { services } = useOpenSearchDashboards<WizardServices>(); + const { + setHeaderActionMenu, + navigation: { + ui: { TopNavMenu }, + }, + } = services; + const rootState = useTypedSelector((state) => state); + const hasUnappliedChanges = useTypedSelector( + (state) => !!state.visualization.activeVisualization?.draftAgg + ); + + const savedWizardVis = useSavedWizardVis(visualizationIdFromUrl); + + const config = useMemo(() => { + if (savedWizardVis === undefined) return; + + const { visualization: visualizationState, style: styleState } = rootState; + + return getTopNavConfig( + { + visualizationIdFromUrl, + savedWizardVis, + visualizationState, + styleState, + hasUnappliedChanges, + }, + services + ); + }, [hasUnappliedChanges, rootState, savedWizardVis, services, visualizationIdFromUrl]); + + const indexPattern = useIndexPatterns().selected; + + return ( + <div className="wizTopNav"> + <TopNavMenu + appName={PLUGIN_ID} + config={config} + setMenuMountPoint={setHeaderActionMenu} + indexPatterns={indexPattern ? [indexPattern] : []} + showSearchBar + showSaveQuery + useDefaultBehaviors + /> + </div> + ); +}; 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..0b7b851cfddb --- /dev/null +++ b/src/plugins/wizard/public/application/components/workspace.scss @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.wizWorkspace { + display: grid; + -ms-grid-rows: auto $euiSizeM 1fr; + grid-template-rows: auto 1fr; + grid-area: workspace; + grid-gap: $euiSizeM; + padding: $euiSizeM; + background-color: $euiColorEmptyShade; + + &__empty { + height: 100%; + } + + &__container { + position: relative; + } + + &__handFieldSvg { + animation: wizDragAnimation 6s ease-in-out infinite forwards; + position: absolute; + top: 34.5%; + } +} + +@keyframes wizDragAnimation { + 0% { + transform: none; + } + + 30% { + transform: translate(116%, -80%); + } + + 60% { + transform: none; + } + + 100% { + transform: none; + } +} 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..087cb656c622 --- /dev/null +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -0,0 +1,179 @@ +/* + * 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, 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'; +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'; +import { ExperimentalInfo } from './experimental_info'; + +export const Workspace: FC = ({ children }) => { + const { + 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(() => { + 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); + setExpression(exp); + } + + 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"> + <EuiFlexItem grow={false}> + <TypeSelectorPopover /> + </EuiFlexItem> + <EuiFlexItem> + <ExperimentalInfo /> + </EuiFlexItem> + </EuiFlexGroup> + <EuiPanel className="wizCanvas" data-test-subj="visualizationLoader"> + {expression ? ( + <ReactExpressionRenderer expression={expression} searchContext={searchContext} /> + ) : ( + <EuiFlexItem className="wizWorkspace__empty" data-test-subj="emptyWorkspace"> + <EuiEmptyPrompt + title={<h2>Add a field to start</h2>} + body={ + <> + <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 + className="wizWorkspace__handFieldSvg" + type={hand_field} + size="original" + /> + </span> + </> + } + /> + </EuiFlexItem> + )} + </EuiPanel> + </section> + ); +}; + +const TypeSelectorPopover = () => { + const [isPopoverOpen, setPopover] = useState(false); + const { + services: { types }, + } = useOpenSearchDashboards<WizardServices>(); + const dispatch = useTypedDispatch(); + const visualizationTypes = types.all(); + const activeVisualization = useVisualizationType(); + + 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: <EuiIcon type={icon} />, + onClick: () => { + closePopover(); + // TODO: Fix changing viz type + // dispatch(setActiveVisualization(name)); + }, + toolTipContent: description, + toolTipPosition: 'right', + }) + ), + }, + ], + [visualizationTypes] + ); + + const button = ( + <EuiButton iconType={activeVisualization?.icon} onClick={onButtonClick}> + {activeVisualization?.title} + </EuiButton> + ); + + return ( + <EuiPopover + id="contextMenuExample" + ownFocus + button={button} + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + <EuiContextMenu initialPanelId={0} panels={panels} /> + </EuiPopover> + ); +}; diff --git a/src/plugins/wizard/public/application/index.tsx b/src/plugins/wizard/public/application/index.tsx new file mode 100644 index 000000000000..28ee13a80bf6 --- /dev/null +++ b/src/plugins/wizard/public/application/index.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-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 = ( + { element, history }: AppMountParameters, + services: WizardServices, + store: Store +) => { + ReactDOM.render( + <Router history={history}> + <OpenSearchDashboardsContextProvider services={services}> + <ReduxProvider store={store}> + <services.i18n.Context> + <Switch> + <Route path={[`${EDIT_PATH}/:id`, '/']} exact={false}> + <WizardApp /> + </Route> + </Switch> + </services.i18n.Context> + </ReduxProvider> + </OpenSearchDashboardsContextProvider> + </Router>, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; 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/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..c0f8725a501a --- /dev/null +++ b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx @@ -0,0 +1,135 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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: DragDataType; + setData?: (data: DragDataType) => void; + isDragging: boolean; + setIsDragging?: any; +} + +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<DragDataType>(EMPTY_DATA); + return ( + <DragDropContext.Provider + value={{ + data, + setData, + + isDragging, + setIsDragging, + }} + > + {children} + </DragDropContext.Provider> + ); +}; + +const useDragDropContext = () => useContext(DragDropContext); + +function useDrag(dragData: DragDataType) { + const { setData, setIsDragging } = useDragDropContext(); + const dragElementProps = { + draggable: true, + onDragStart: (event: DragEvent) => { + setIsDragging(true); + setData!(dragData); + }, + onDragEnd: (event: DragEvent) => { + setIsDragging(false); + setData!({ + namespace: null, + value: null, + }); + }, + }; + return [dragElementProps]; +} + +export interface IDropAttributes { + onDragOver: (event: DragEvent) => void; + onDrop: (event: DragEvent) => void; + onDragEnter: (event: DragEvent) => void; + onDragLeave: (event: DragEvent) => void; +} + +export interface IDropState { + isDragging: boolean; + canDrop: boolean; + isValidDropTarget: boolean; + dragData: DragDataType['value']; +} +const useDrop = ( + namespace: DragDataType['namespace'], + onDropCallback: (data: DragDataType['value']) => void +): [IDropAttributes, IDropState] => { + const { data, isDragging, setIsDragging, setData } = useDragDropContext(); + const [canDrop, setCanDrop] = useState(0); + + const dropAttributes: IDropAttributes = { + onDragOver: (event) => { + event.preventDefault(); + }, + onDrop: (event) => { + setIsDragging(false); + setCanDrop(0); + onDropCallback(data.value); + setData!({ + namespace: null, + value: null, + }); + }, + onDragEnter: (event) => { + if (data?.namespace === namespace) { + setCanDrop((state) => state + 1); + } + }, + onDragLeave: (event) => { + setCanDrop((state) => state - 1); + }, + }; + + useEffect(() => { + if (!isDragging) setCanDrop(0); + }, [isDragging]); + + return [ + dropAttributes, + { + isDragging, + canDrop: canDrop > 0, + 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/drag_drop/types.ts b/src/plugins/wizard/public/application/utils/drag_drop/types.ts new file mode 100644 index 000000000000..8ac8deb73e44 --- /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 '../../../../../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/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 new file mode 100644 index 000000000000..d0404b5b762a --- /dev/null +++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx @@ -0,0 +1,173 @@ +/* + * 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 '../..'; +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 = ( + { + visualizationIdFromUrl, + savedWizardVis, + visualizationState, + styleState, + hasUnappliedChanges, + }: TopNavConfigParams, + { history, toastNotifications, i18n: { Context: I18nContext } }: WizardServices +) => { + const topNavConfig: TopNavMenuData[] = [ + { + id: 'save', + iconType: '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', + 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 ({ + newTitle, + newCopyOnSave, + isTitleDuplicateConfirmed, + onTitleDuplicate, + newDescription, + returnToOrigin, + }: OnSaveProps & { returnToOrigin: boolean }) => { + if (!savedWizardVis) { + return; + } + const currentTitle = savedWizardVis.title; + savedWizardVis.visualizationState = JSON.stringify(visualizationState); + savedWizardVis.styleState = JSON.stringify(styleState); + savedWizardVis.title = newTitle; + savedWizardVis.description = newDescription; + savedWizardVis.copyOnSave = newCopyOnSave; + + try { + const id = await savedWizardVis.save({ + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + returnToOrigin, + }); + + if (id) { + toastNotifications.addSuccess({ + title: i18n.translate( + 'wizard.topNavMenu.saveVisualization.successNotificationText', + { + defaultMessage: `Saved '{visTitle}'`, + values: { + visTitle: savedWizardVis.title, + }, + } + ), + 'data-test-subj': 'saveVisualizationSuccess', + }); + + // Update URL + if (id !== visualizationIdFromUrl) { + history.push({ + ...history.location, + pathname: `${EDIT_PATH}/${id}`, + }); + } + } else { + // reset title if save not successful + savedWizardVis.title = currentTitle; + } + + // 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); + + toastNotifications.addDanger({ + title: i18n.translate('wizard.topNavMenu.saveVisualization.failureNotificationText', { + defaultMessage: `Error on saving '{visTitle}'`, + values: { + visTitle: newTitle, + }, + }), + text: error.message, + 'data-test-subj': 'saveVisualizationError', + }); + + // reset title if save not successful + savedWizardVis.title = currentTitle; + return { error }; + } + }; + + const saveModal = ( + <SavedObjectSaveModalOrigin + documentInfo={savedWizardVis} + onSave={onSave} + objectType={'wizard'} + onClose={() => {}} + /> + ); + + showSaveModal(saveModal, I18nContext); + }, + }, + ]; + + return topNavConfig; +}; 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..607fe05b1623 --- /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 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/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/metadata_slice.ts b/src/plugins/wizard/public/application/utils/state_management/metadata_slice.ts new file mode 100644 index 000000000000..f1cb4bcb9566 --- /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: { + validity: { + // Validity for each section in the editor + [key: string]: boolean; + }; + }; +} + +const initialState: MetadataState = { + editorState: { + validity: {}, + }, +}; + +export const getPreloadedState = async ({ + types, + data, +}: WizardServices): Promise<MetadataState> => { + const preloadedState = { ...initialState }; + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'metadata', + initialState, + reducers: { + setValidity: (state, action: PayloadAction<{ key: string; valid: boolean }>) => { + const { key, valid } = action.payload; + state.editorState.validity[key] = valid; + }, + setState: (_state, action: PayloadAction<MetadataState>) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setValidity, 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 new file mode 100644 index 000000000000..1d96608e33cc --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/preload.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 ( + services: WizardServices +): 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 new file mode 100644 index 000000000000..932a5f4d8914 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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>) => { + 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<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 new file mode 100644 index 000000000000..55579c759ccf --- /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'; + +export 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 new file mode 100644 index 000000000000..fe1277f33432 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts @@ -0,0 +1,121 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { CreateAggConfigParams } from '../../../../../data/common'; +import { WizardServices } from '../../../types'; + +export interface VisualizationState { + indexPattern?: string; + searchField: string; + activeVisualization?: { + name: string; + aggConfigParams: CreateAggConfigParams[]; + draftAgg?: CreateAggConfigParams; + }; +} + +const initialState: VisualizationState = { + searchField: '', +}; + +export const getPreloadedState = async ({ + types, + data, +}: WizardServices): Promise<VisualizationState> => { + const preloadedState = { ...initialState }; + + const defaultVisualization = types.all()[0]; + const defaultIndexPattern = await data.indexPatterns.getDefault(); + const name = defaultVisualization.name; + if (name && defaultIndexPattern) { + preloadedState.activeVisualization = { + name, + aggConfigParams: [], + }; + + preloadedState.indexPattern = defaultIndexPattern.id; + } + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'visualization', + initialState, + reducers: { + 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; + }, + editDraftAgg: (state, action: PayloadAction<CreateAggConfigParams | undefined>) => { + state.activeVisualization!.draftAgg = action.payload; + }, + saveDraftAgg: (state, action: PayloadAction<undefined>) => { + const draftAgg = state.activeVisualization!.draftAgg; + + if (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); + } + } + }, + reorderAgg: ( + 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; + }, + setState: (_state, action: PayloadAction<VisualizationState>) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { + setActiveVisualization, + setIndexPattern, + setSearchField, + editDraftAgg, + saveDraftAgg, + updateAggConfigParams, + reorderAgg, + setState, +} = slice.actions; 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..e8d1087ce0a0 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useVisualizationType } from './use_visualization_type'; +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 new file mode 100644 index 000000000000..b5c60ee20944 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { 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 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: IndexPattern; + 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(true); + const patterns = await Promise.all(ids.map((id) => data.indexPatterns.get(id))); + setIndexPatterns(patterns); + } catch (e) { + setError(e as Error); + } finally { + setLoading(false); + } + }; + + handleUpdate(); + }, [data.indexPatterns]); + + return { + indexPatterns, + error, + loading, + selected: foundSelected!, + }; +}; 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..108beaefdb14 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/use/use_saved_wizard_vis.ts @@ -0,0 +1,89 @@ +/* + * 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'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; + +export const useSavedWizardVis = (visualizationIdFromUrl: string | undefined) => { + const { services } = useOpenSearchDashboards<WizardServices>(); + 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/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..002c83759b3c --- /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?.name ?? ''); + + if (!visualizationType) { + throw new Error(`Invalid visualization type ${activeVisualization}`); + } + + return visualizationType; +}; 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/assets/fields_bg.svg b/src/plugins/wizard/public/assets/fields_bg.svg new file mode 100644 index 000000000000..d7ac9e455f0c --- /dev/null +++ b/src/plugins/wizard/public/assets/fields_bg.svg @@ -0,0 +1,61 @@ +<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 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> +<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 new file mode 100644 index 000000000000..8c38e60edd59 --- /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_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_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_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_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_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> +</filter> +<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_268_49515"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_268_49515" result="shape"/> +</filter> +<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_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.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/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/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..792bb1777ad6 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/disabled_visualization.scss @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.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/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..675baf28796a --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_component.tsx @@ -0,0 +1,38 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +import { SavedObjectEmbeddableInput, withEmbeddableSubscription } from '../../../embeddable/public'; +import { WizardEmbeddable, WizardOutput } from './wizard_embeddable'; +import { getReactExpressionRenderer } from '../plugin_services'; + +interface Props { + embeddable: WizardEmbeddable; + input: SavedObjectEmbeddableInput; + output: WizardOutput; +} + +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< + SavedObjectEmbeddableInput, + 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..bc2d4548dda1 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable.tsx @@ -0,0 +1,291 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { cloneDeep, isEqual } from 'lodash'; +import ReactDOM from 'react-dom'; +import { merge, Subscription } from 'rxjs'; + +import { PLUGIN_ID, WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from '../../common'; +import { + Embeddable, + EmbeddableOutput, + ErrorEmbeddable, + IContainer, + SavedObjectEmbeddableInput, +} from '../../../embeddable/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'; + +// 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. + */ + savedWizard?: WizardSavedObjectAttributes; +} + +type ExpressionLoader = InstanceType<ExpressionsStart['ExpressionLoader']>; + +export class WizardEmbeddable extends Embeddable<SavedObjectEmbeddableInput, WizardOutput> { + public readonly type = WIZARD_EMBEDDABLE; + 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 savedWizard?: WizardSavedObjectAttributes; + private serializedState?: { visualization: string; style: string }; + + constructor( + timefilter: TimefilterContract, + { savedWizard, editPath, editUrl, editable }: WizardEmbeddableConfiguration, + initialInput: SavedObjectEmbeddableInput, + { + parent, + }: { + parent?: IContainer; + } + ) { + 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; + } + }; + + // 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.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.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(); + } + + public async reload() { + this.updateHandler(); + } + + public destroy() { + super.destroy(); + 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 new file mode 100644 index 000000000000..f2d92d001303 --- /dev/null +++ b/src/plugins/wizard/public/embeddable/wizard_embeddable_factory.tsx @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { i18n } from '@osd/i18n'; +import { + EmbeddableFactory, + EmbeddableFactoryDefinition, + EmbeddableOutput, + ErrorEmbeddable, + IContainer, + SavedObjectEmbeddableInput, +} from '../../../embeddable/public'; +import { VISUALIZE_ENABLE_LABS_SETTING } from '../../../visualizations/public'; +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'; +import { getHttp, getSavedWizardLoader, getTimeFilter, getUISettings } from '../plugin_services'; + +// TODO: use or remove? +export type WizardEmbeddableFactory = EmbeddableFactory< + SavedObjectEmbeddableInput, + WizardOutput | EmbeddableOutput, + WizardEmbeddable | DisabledEmbeddable, + WizardSavedObjectAttributes +>; + +export class WizardEmbeddableFactoryDefinition + implements + EmbeddableFactoryDefinition< + SavedObjectEmbeddableInput, + WizardOutput | EmbeddableOutput, + WizardEmbeddable | DisabledEmbeddable, + WizardSavedObjectAttributes + > { + public readonly type = WIZARD_EMBEDDABLE; + public readonly savedObjectMetaData = { + // TODO: Update to include most vis functionality + name: PLUGIN_NAME, + includeFields: ['visualizationState'], + type: WIZARD_SAVED_OBJECT, + getIconForSavedObject: () => wizardIcon, + }; + + // 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 + // return getCapabilities().visualize.save as boolean; + return true; + } + + public async createFromSavedObject( + savedObjectId: string, + input: Partial<SavedObjectEmbeddableInput> & { id: string }, + parent?: IContainer + ): 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}`); + + const isLabsEnabled = getUISettings().get<boolean>(VISUALIZE_ENABLE_LABS_SETTING); + + if (!isLabsEnabled) { + return new DisabledEmbeddable(PLUGIN_NAME, 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); + } + } + + public async create(_input: SavedObjectEmbeddableInput, _parent?: IContainer) { + return undefined; + } + + public getDisplayName() { + return i18n.translate('wizard.displayName', { + defaultMessage: PLUGIN_ID, + }); + } +} diff --git a/src/plugins/wizard/public/index.ts b/src/plugins/wizard/public/index.ts new file mode 100644 index 000000000000..713e9448b933 --- /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, WizardStart } from './types'; diff --git a/src/plugins/wizard/public/plugin.test.ts b/src/plugins/wizard/public/plugin.test.ts new file mode 100644 index 000000000000..6dafa46c86ff --- /dev/null +++ b/src/plugins/wizard/public/plugin.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { 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 visualization', () => { + 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( + 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 new file mode 100644 index 000000000000..9c56562f4ba1 --- /dev/null +++ b/src/plugins/wizard/public/plugin.ts @@ -0,0 +1,170 @@ +/* + * 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, + 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, + setHttp, + setSavedWizardLoader, + setExpressionLoader, + setTimeFilter, + setUISettings, + setTypeService, + setReactExpressionRenderer, +} 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<ConfigSchema>) {} + + public setup( + core: CoreSetup<WizardPluginStartDependencies, WizardStart>, + { embeddable, visualizations }: WizardPluginSetupDependencies + ) { + const typeService = this.typeService; + registerDefaultTypes(typeService.setup()); + + // Register the plugin to core + core.application.register({ + id: PLUGIN_ID, + 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, selfStart] = await core.getStartServices(); + 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 Default Visualizations + + const services: WizardServices = { + ...coreStart, + toastNotifications: coreStart.notifications.toasts, + data, + savedObjectsPublic: savedObjects, + navigation, + expressions, + history: params.history, + setHeaderActionMenu: params.setHeaderActionMenu, + types: typeService.start(), + savedWizardLoader: selfStart.savedWizardLoader, + }; + + // Instantiate the store + const store = await getPreloadedStore(services); + + // Render the application + return renderApp(params, services, store); + }, + }); + + // Register embeddable + // TODO: investigate simplification via getter a la visualizations: + // const start = createStartServicesGetter(core.getStartServices)); + // const embeddableFactory = new WizardEmbeddableFactoryDefinition({ start }); + const embeddableFactory = new WizardEmbeddableFactoryDefinition(); + embeddable.registerEmbeddableFactory(WIZARD_EMBEDDABLE, embeddableFactory); + + // Register the plugin as an alias to create visualization + visualizations.registerAlias({ + name: PLUGIN_ID, + title: PLUGIN_NAME, + description: i18n.translate('wizard.visPicker.description', { + defaultMessage: 'Create visualizations using the new Drag & Drop experience', + }), + icon: wizardIconSecondaryFill, + 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 { + ...typeService.setup(), + }; + } + + 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, + savedWizardLoader, + }; + } + + public stop() {} +} diff --git a/src/plugins/wizard/public/plugin_services.ts b/src/plugins/wizard/public/plugin_services.ts new file mode 100644 index 000000000000..8f01ea6e9b6b --- /dev/null +++ b/src/plugins/wizard/public/plugin_services.ts @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createGetterSetter } from '../../opensearch_dashboards_utils/common'; +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/saved_visualizations/_saved_vis.ts b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts new file mode 100644 index 000000000000..66a02a974dd6 --- /dev/null +++ b/src/plugins/wizard/public/saved_visualizations/_saved_vis.ts @@ -0,0 +1,54 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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_SAVED_OBJECT; + + // if type:wizard has no mapping, we push this mapping into OpenSearch + public static mapping = { + title: 'text', + description: 'text', + visualizationState: 'text', + styleState: 'text', + 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: '', + visualizationState: '{}', + styleState: '{}', + version: 1, + }, + }); + this.showInRecentlyAccessed = true; + this.getFullPath = () => `/app/${PLUGIN_ID}${EDIT_PATH}/${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/index.ts b/src/plugins/wizard/public/services/type_service/index.ts new file mode 100644 index 000000000000..9baeb6dbc9f4 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +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..89e1ecb59154 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/type_service.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisualizationTypeOptions } from './types'; +import { TypeService } from './type_service'; + +const DEFAULT_VIZ_PROPS: VisualizationTypeOptions = { + name: 'some-name', + icon: 'some-icon', + title: 'Some Title', + ui: {} as any, // Not required for this test + toExpression: async (state) => { + return 'test'; + }, +}; + +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 visualizations 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 new file mode 100644 index 000000000000..ddbd735fb9e8 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/type_service.ts @@ -0,0 +1,88 @@ +/* + * 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 { CoreService } from '../../../../../core/types'; +import { VisualizationTypeOptions } from './types'; +import { VisualizationType } from './visualization_type'; + +/** + * Visualization Types Service + * + * @internal + */ +export class TypeService implements CoreService<TypeServiceSetup, TypeServiceStart> { + private types: Record<string, VisualizationType> = {}; + + private registerVisualizationType(visDefinition: VisualizationType) { + if (this.types[visDefinition.name]) { + throw new Error(`A visualization with this the name ${visDefinition.name} 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<TypeService['setup']>; +export type TypeServiceStart = ReturnType<TypeService['start']>; 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..8542c0da0538 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/types.ts @@ -0,0 +1,32 @@ +/* + * 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 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?: 'experimental' | 'production'; + readonly ui: { + containerConfig: { + data: DataTabConfig; + style: StyleTabConfig<T>; + }; + }; + readonly toExpression: (state: RootState) => Promise<string | undefined>; +} 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..305b7a716cf6 --- /dev/null +++ b/src/plugins/wizard/public/services/type_service/visualization_type.tsx @@ -0,0 +1,29 @@ +/* + * 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: string; + public readonly title: string; + public readonly description: string; + public readonly icon: IconType; + public readonly stage: 'experimental' | 'production'; + public readonly ui: IVisualizationType['ui']; + public readonly toExpression: (state: RootState) => Promise<string | undefined>; + + 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.ui = options.ui; + this.toExpression = options.toExpression; + } +} diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts new file mode 100644 index 000000000000..f8371b832bdc --- /dev/null +++ b/src/plugins/wizard/public/types.ts @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { History } from 'history'; +import { SavedObject, SavedObjectsStart } from '../../saved_objects/public'; +import { EmbeddableSetup, EmbeddableStart } 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 } 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; + visualizations: VisualizationsSetup; +} +export interface WizardPluginStartDependencies { + embeddable: EmbeddableStart; + navigation: NavigationPublicPluginStart; + data: DataPublicPluginStart; + savedObjects: SavedObjectsStart; + dashboard: DashboardStart; + expressions: ExpressionsStart; +} + +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/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts new file mode 100644 index 000000000000..52d3a7234f2a --- /dev/null +++ b/src/plugins/wizard/public/visualizations/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { TypeServiceSetup } from '../services/type_service'; +import { createMetricConfig } from './metric'; + +export function registerDefaultTypes(typeServiceSetup: TypeServiceSetup) { + 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..b5ef3bdb75cb --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/components/metric_viz_options.tsx @@ -0,0 +1,187 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from 'react'; +import { i18n } from '@osd/i18n'; +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, + 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; + 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] + ); + + const metricColorModeLabel = i18n.translate('visTypeMetric.params.color.useForLabel', { + defaultMessage: 'Use color for', + }); + + return ( + <EuiPanel paddingSize="s" hasShadow={false} hasBorder={false} color="transparent"> + <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" /> + + <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={(_, value) => + setOption((draft) => { + draft.metric.colorsRange = value; + }) + } + setTouched={() => {}} + setValidity={() => {}} + /> + + <EuiFormRow fullWidth display="rowCompressed" label={metricColorModeLabel}> + <EuiButtonGroup + buttonSize="compressed" + idSelected={metric.metricColorMode} + isDisabled={metric.colorsRange.length === 1} + isFullWidth={true} + legend={metricColorModeLabel} + options={METRIC_COLOR_MODES} + onChange={(value) => + setOption((draft) => { + draft.metric.metricColorMode = value as ColorModes; + }) + } + /> + </EuiFormRow> + + <ColorSchemaOptions + colorSchema={metric.colorSchema} + colorSchemas={colorSchemas} + disabled={metric.colorsRange.length === 1 || metric.metricColorMode === ColorModes.NONE} + invertColors={metric.invertColors} + 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 here is used for custom colors which is not currently supported. Update when supported + uiState={new PersistedState({})} + /> + </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> + </EuiPanel> + ); +} + +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..ce85db45c51b --- /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 { 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'; +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: RangeValues[]; + 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: { + aggTypes: ['avg', 'cardinality'], + }, + }, + { + 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: { + aggTypes: ['terms'], + }, + }, + ]), + }, + 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..ce930d9b8e40 --- /dev/null +++ b/src/plugins/wizard/public/visualizations/metric/to_expression.ts @@ -0,0 +1,175 @@ +/* + * 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; +}; + +export interface MetricRootState extends RootState { + style: MetricOptionsDefaults; +} + +export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { + const { activeVisualization, indexPattern: indexId = '' } = visualization; + const { aggConfigParams } = activeVisualization || {}; + + 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 `opensearchDashboards | opensearch_dashboards_context | ${ast.toString()}`; +}; 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/index.ts b/src/plugins/wizard/server/index.ts new file mode 100644 index 000000000000..cd5e3aa3a30e --- /dev/null +++ b/src/plugins/wizard/server/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginConfigDescriptor, PluginInitializerContext } from '../../../core/server'; +import { ConfigSchema, configSchema } from '../config'; +import { WizardPlugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as the OpenSearch Dashboards Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new WizardPlugin(initializerContext); +} + +export { WizardPluginSetup, WizardPluginStart } from './types'; + +export const config: PluginConfigDescriptor<ConfigSchema> = { + exposeToBrowser: { + enabled: true, + }, + schema: configSchema, +}; diff --git a/src/plugins/wizard/server/plugin.ts b/src/plugins/wizard/server/plugin.ts new file mode 100644 index 000000000000..25b7f27c1f81 --- /dev/null +++ b/src/plugins/wizard/server/plugin.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; + +import { WizardPluginSetup, WizardPluginStart } from './types'; +import { capabilitiesProvider } from './capabilities_provider'; +import { wizardSavedObjectType } from './saved_objects'; + +export class WizardPlugin implements Plugin<WizardPluginSetup, WizardPluginStart> { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup({ capabilities, http, savedObjects }: CoreSetup) { + this.logger.debug('wizard: Setup'); + + // Register saved object types + savedObjects.registerType(wizardSavedObjectType); + + // Register capabilities + capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(_core: CoreStart) { + this.logger.debug('wizard: Started'); + return {}; + } + + public stop() {} +} 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..eabc7abf2761 --- /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 { 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 new file mode 100644 index 000000000000..f5820d0f3f29 --- /dev/null +++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObject, SavedObjectsType } from '../../../../core/server'; +import { + EDIT_PATH, + PLUGIN_ID, + WizardSavedObjectAttributes, + WIZARD_SAVED_OBJECT, +} from '../../common'; + +export const wizardSavedObjectType: SavedObjectsType = { + name: WIZARD_SAVED_OBJECT, + hidden: false, + namespaceType: 'single', + management: { + // 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, + getEditUrl: ({ id }: SavedObject) => + `/management/opensearch-dashboards/objects/savedWizard/${encodeURIComponent(id)}`, + getInAppUrl({ id }: SavedObject) { + return { + path: `/app/${PLUGIN_ID}${EDIT_PATH}/${encodeURIComponent(id)}`, + uiCapabilitiesPath: 'wizard.show', + }; + }, + }, + migrations: {}, + mappings: { + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + visualizationState: { + type: 'text', + index: false, + }, + styleState: { + type: 'text', + index: false, + }, + version: { type: 'integer' }, + }, + }, +}; diff --git a/src/plugins/wizard/server/types.ts b/src/plugins/wizard/server/types.ts new file mode 100644 index 000000000000..69f9ea0996d3 --- /dev/null +++ b/src/plugins/wizard/server/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// 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 {} 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" } + ] +} 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/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'); 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' ); diff --git a/yarn.lock b/yarn.lock index 5804cdfa640c..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" @@ -9999,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" @@ -14971,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== @@ -14983,6 +14998,13 @@ redux@^4.0.0, redux@^4.0.4, redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" +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" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -15322,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"