diff --git a/package.json b/package.json
index b18c6dc863a5..c83730e6a3a9 100644
--- a/package.json
+++ b/package.json
@@ -140,6 +140,7 @@
"@osd/std": "1.0.0",
"@osd/ui-framework": "1.0.0",
"@osd/ui-shared-deps": "1.0.0",
+ "@reduxjs/toolkit": "^1.6.2",
"@types/yauzl": "^2.9.1",
"JSONStream": "1.3.5",
"abortcontroller-polyfill": "^1.4.0",
diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
index 0c72df9a5fe5..2685e7cc8d21 100644
--- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
+++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
@@ -264,10 +264,9 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
-
-
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
-
-
+
+
+
+
);
diff --git a/src/plugins/wizard/.i18nrc.json b/src/plugins/wizard/.i18nrc.json
new file mode 100644
index 000000000000..2b511494a460
--- /dev/null
+++ b/src/plugins/wizard/.i18nrc.json
@@ -0,0 +1,7 @@
+{
+ "prefix": "wizard",
+ "paths": {
+ "wizard": "."
+ },
+ "translations": ["translations/ja-JP.json"]
+}
diff --git a/src/plugins/wizard/README.md b/src/plugins/wizard/README.md
new file mode 100755
index 000000000000..bcb362b374cb
--- /dev/null
+++ b/src/plugins/wizard/README.md
@@ -0,0 +1,11 @@
+# wizard
+
+A OpenSearch Dashboards plugin
+
+---
+
+## Development
+
+See the [OpenSearch Dashboards contributing
+guide](https://github.com/opensearch-project/OpenSearch-Dashboards/blob/master/CONTRIBUTING.md) for instructions
+setting up your development environment.
diff --git a/src/plugins/wizard/common/index.ts b/src/plugins/wizard/common/index.ts
new file mode 100644
index 000000000000..4b3522fec709
--- /dev/null
+++ b/src/plugins/wizard/common/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const PLUGIN_ID = 'wizard';
+export const PLUGIN_NAME = 'Wizard';
+
+export { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from './wizard_saved_object_attributes';
diff --git a/src/plugins/wizard/common/wizard_saved_object_attributes.ts b/src/plugins/wizard/common/wizard_saved_object_attributes.ts
new file mode 100644
index 000000000000..ff6c12417d24
--- /dev/null
+++ b/src/plugins/wizard/common/wizard_saved_object_attributes.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectAttributes } from 'opensearch-dashboards/public';
+
+export const WIZARD_SAVED_OBJECT = 'wizard';
+
+export interface WizardSavedObjectAttributes extends SavedObjectAttributes {
+ title: string;
+ description?: string;
+ state: string;
+}
diff --git a/src/plugins/wizard/opensearch_dashboards.json b/src/plugins/wizard/opensearch_dashboards.json
new file mode 100644
index 000000000000..8dd00aae3890
--- /dev/null
+++ b/src/plugins/wizard/opensearch_dashboards.json
@@ -0,0 +1,17 @@
+{
+ "id": "wizard",
+ "version": "1.0.0",
+ "opensearchDashboardsVersion": "opensearchDashboards",
+ "server": true,
+ "ui": true,
+ "requiredPlugins": [
+ "navigation",
+ "data",
+ "opensearchDashboardsReact",
+ "savedObjects",
+ "embeddable",
+ "dashboard",
+ "visualizations"
+ ],
+ "optionalPlugins": []
+}
diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss
new file mode 100644
index 000000000000..c1b3646e8e49
--- /dev/null
+++ b/src/plugins/wizard/public/application/_variables.scss
@@ -0,0 +1,3 @@
+@import '@elastic/eui/src/global_styling/variables/header';
+
+$osdHeaderOffset: $euiHeaderHeightCompensation * 2;
\ No newline at end of file
diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss
new file mode 100644
index 000000000000..2e1e93f44312
--- /dev/null
+++ b/src/plugins/wizard/public/application/app.scss
@@ -0,0 +1,13 @@
+@import "variables";
+
+.wizLayout {
+ padding: 0;
+ display: grid;
+ grid-template-rows: min-content 1fr;
+ grid-template-columns: 420px 1fr;
+ grid-template-areas:
+ "topNav topNav"
+ "sideNav workspace"
+ ;
+ height: calc(100vh - #{$osdHeaderOffset}); // TODO: update 190px to correct offset variable
+}
diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx
new file mode 100644
index 000000000000..7d578ee77cda
--- /dev/null
+++ b/src/plugins/wizard/public/application/app.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { I18nProvider } from '@osd/i18n/react';
+import { EuiPage } from '@elastic/eui';
+import { SideNav } from './components/side_nav';
+import { DragDropProvider } from './utils/drag_drop/drag_drop_context';
+import { Workspace } from './components/workspace';
+
+import './app.scss';
+import { TopNav } from './components/top_nav';
+
+export const WizardApp = () => {
+ // Render the application DOM.
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/plugins/wizard/public/application/components/_util.scss b/src/plugins/wizard/public/application/components/_util.scss
new file mode 100644
index 000000000000..9a444c1fe091
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/_util.scss
@@ -0,0 +1,8 @@
+@mixin scrollNavParent ($template-row: none) {
+ display: grid;
+ min-height: 0;
+
+ @if $template-row != 'none' {
+ grid-template-rows: $template-row;
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.scss b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss
new file mode 100644
index 000000000000..7477dcfca813
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.scss
@@ -0,0 +1,9 @@
+.wizConfigPanel {
+ background: #f0f1f3;
+ border-left: $euiBorderThin;
+ padding: $euiSizeS;
+}
+
+.wizConfigPanel__title {
+ margin-left: $euiSizeS;
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx
new file mode 100644
index 000000000000..ec910b7352de
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { EuiForm, EuiTitle } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@osd/i18n';
+import { ConfigSection } from './config_section';
+
+import './config_panel.scss';
+import { useTypedSelector } from '../../utils/state_management';
+
+export function ConfigPanel() {
+ const { configSections } = useTypedSelector((state) => state.config);
+
+ return (
+
+
+
+ {i18n.translate('wizard.nav.dataTab.configPanel.title', {
+ defaultMessage: 'Configuration',
+ })}
+
+
+ {Object.entries(configSections).map(([sectionId, sectionProps], index) => (
+
+ ))}
+
+ );
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/config_section.scss b/src/plugins/wizard/public/application/components/data_tab/config_section.scss
new file mode 100644
index 000000000000..79d0d3a913fd
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/config_section.scss
@@ -0,0 +1,23 @@
+.wizConfigSection {
+ margin-top: $euiSize;
+ border-bottom: $euiBorderThin;
+ padding-bottom: $euiSize;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ & .euiFormRow__labelWrapper {
+ margin-left: $euiSizeS;
+ }
+}
+
+.wizConfigSection__dropTarget {
+ @include euiSlightShadow;
+ background: $euiColorEmptyShade;
+ border: $euiBorderThin;
+ box-shadow: 0px 2px 2px rgba(152, 162, 179, 0.15);
+ border-radius: $euiBorderRadius;
+ padding: $euiSizeS $euiSizeM;
+ color: $euiColorDarkShade;
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/config_section.tsx b/src/plugins/wizard/public/application/components/data_tab/config_section.tsx
new file mode 100644
index 000000000000..64f74824d71a
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/config_section.tsx
@@ -0,0 +1,83 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { EuiButtonIcon, EuiPanel, EuiText, EuiTitle } from '@elastic/eui';
+import { i18n } from '@osd/i18n';
+import React, { useCallback } from 'react';
+import { IndexPatternField } from 'src/plugins/data/common';
+import { useDrop } from '../../utils/drag_drop';
+import { useTypedDispatch, useTypedSelector } from '../../utils/state_management';
+import {
+ addConfigSectionField,
+ removeConfigSectionField,
+} from '../../utils/state_management/config_slice';
+
+import './config_section.scss';
+
+interface ConfigSectionProps {
+ id: string;
+ title: string;
+}
+
+export const ConfigSection = ({ title, id }: ConfigSectionProps) => {
+ const dispatch = useTypedDispatch();
+ const { fields } = useTypedSelector((state) => state.config.configSections[id]);
+
+ const dropHandler = useCallback(
+ (field: IndexPatternField) => {
+ dispatch(
+ addConfigSectionField({
+ sectionId: id,
+ field,
+ })
+ );
+ },
+ [dispatch, id]
+ );
+ const [dropProps, { isValidDropTarget, dragData }] = useDrop('dataPlane', dropHandler);
+
+ const dropTargetString = dragData
+ ? dragData.type
+ : i18n.translate('wizard.nav.dataTab.configPanel.dropTarget.placeholder', {
+ defaultMessage: 'Click or drop to add',
+ });
+
+ return (
+
+
+ {title}
+
+ {fields.length ? (
+ fields.map((field, index) => (
+
+
+ {field.displayName}
+
+
+ dispatch(
+ removeConfigSectionField({
+ sectionId: id,
+ field,
+ })
+ )
+ }
+ />
+
+ ))
+ ) : (
+
+ {dropTargetString}
+
+ )}
+
+ );
+};
diff --git a/src/plugins/wizard/public/application/components/data_tab/field_search.tsx b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx
new file mode 100644
index 000000000000..2db8404c93c6
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/field_search.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { i18n } from '@osd/i18n';
+import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { setSearchField } from '../../utils/state_management/datasource_slice';
+import { useTypedDispatch } from '../../utils/state_management';
+
+export interface Props {
+ /**
+ * the input value of the user
+ */
+ value?: string;
+}
+
+/**
+ * Component is Wizard's side bar to search of available fields
+ * Additionally there's a button displayed that allows the user to show/hide more filter fields
+ */
+export function FieldSearch({ value }: Props) {
+ const searchPlaceholder = i18n.translate('wizard.fieldChooser.searchPlaceHolder', {
+ defaultMessage: 'Search field names',
+ });
+
+ const dispatch = useTypedDispatch();
+
+ return (
+
+
+
+ dispatch(setSearchField(event.currentTarget.value))}
+ placeholder={searchPlaceholder}
+ value={value}
+ />
+
+
+
+ );
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss
new file mode 100644
index 000000000000..c05f75457b0b
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.scss
@@ -0,0 +1,10 @@
+@import "../util";
+
+.wizFieldSelector {
+ @include scrollNavParent(auto 1fr);
+ padding: $euiSizeS;
+}
+
+.wizFieldSelector__fieldGroups {
+ overflow-y: auto;
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx
new file mode 100644
index 000000000000..1464f31aabd9
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx
@@ -0,0 +1,118 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useCallback, useState, useEffect } from 'react';
+import { EuiFlexItem, EuiAccordion, EuiSpacer, EuiNotificationBadge, EuiTitle } from '@elastic/eui';
+import { FieldSearch } from './field_search';
+
+import {
+ IndexPatternField,
+ OPENSEARCH_FIELD_TYPES,
+ OSD_FIELD_TYPES,
+} from '../../../../../data/public';
+import { FieldSelectorField } from './field_selector_field';
+
+import './field_selector.scss';
+import { useTypedSelector } from '../../utils/state_management';
+
+interface IFieldCategories {
+ categorical: IndexPatternField[];
+ numerical: IndexPatternField[];
+ meta: IndexPatternField[];
+}
+
+const META_FIELDS: string[] = [
+ OPENSEARCH_FIELD_TYPES._ID,
+ OPENSEARCH_FIELD_TYPES._INDEX,
+ OPENSEARCH_FIELD_TYPES._SOURCE,
+ OPENSEARCH_FIELD_TYPES._TYPE,
+];
+
+export const FieldSelector = () => {
+ const indexFields = useTypedSelector((state) => state.dataSource.visualizableFields);
+ const [filteredFields, setFilteredFields] = useState(indexFields);
+ const fieldSearchValue = useTypedSelector((state) => state.dataSource.searchField);
+
+ useEffect(() => {
+ const filteredSubset = indexFields.filter((field) =>
+ field.displayName.includes(fieldSearchValue)
+ );
+
+ setFilteredFields(filteredSubset);
+ return;
+ }, [indexFields, fieldSearchValue]);
+
+ const fields = filteredFields?.reduce(
+ (fieldGroups, currentField) => {
+ const category = getFieldCategory(currentField);
+ fieldGroups[category].push(currentField);
+
+ return fieldGroups;
+ },
+ {
+ categorical: [],
+ numerical: [],
+ meta: [],
+ }
+ );
+
+ return (
+
+ );
+};
+
+interface FieldGroupProps {
+ fields?: IndexPatternField[];
+ header: string;
+ id: string;
+}
+
+const FieldGroup = ({ fields, header, id }: FieldGroupProps) => (
+ <>
+
+ {header}
+
+ }
+ extraAction={
+
+ {fields?.length || 0}
+
+ }
+ initialIsOpen
+ >
+ {fields?.map((field, i) => (
+
+
+
+ ))}
+
+
+ >
+);
+
+function getFieldCategory(field: IndexPatternField): keyof IFieldCategories {
+ if (META_FIELDS.includes(field.name)) return 'meta';
+ if (field.type === OSD_FIELD_TYPES.NUMBER) return 'numerical';
+
+ return 'categorical';
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss
new file mode 100644
index 000000000000..0ace9a914b37
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.scss
@@ -0,0 +1,12 @@
+.wizFieldSelectorField {
+ @include euiBottomShadowSmall;
+ padding: $euiSizeXS;
+ background-color: $euiColorEmptyShade;
+ border: $euiBorderThin;
+ margin-top: $euiSizeS;
+
+ & > button {
+ align-items: center;
+ gap: 4px;
+ }
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx
new file mode 100644
index 000000000000..e545a6b33a63
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/field_selector_field.tsx
@@ -0,0 +1,83 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Any modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState } from 'react';
+import { IndexPatternField } from 'src/plugins/data/public';
+import { FieldButton, FieldIcon } from '../../../../../opensearch_dashboards_react/public';
+import { useDrag } from '../../utils/drag_drop/drag_drop_context';
+
+import './field_selector_field.scss';
+
+export interface FieldSelectorFieldProps {
+ field: IndexPatternField;
+}
+
+// TODO:
+// 1. Add field sections (Available fields, popular fields from src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx)
+// 2. Add popover for fields stats from discover as well
+export const FieldSelectorField = ({ field }: FieldSelectorFieldProps) => {
+ const [infoIsOpen, setOpen] = useState(false);
+ const [dragProps] = useDrag(field, `dataPlane`);
+
+ function togglePopover() {
+ setOpen(!infoIsOpen);
+ }
+
+ function wrapOnDot(str?: string) {
+ // u200B is a non-width white-space character, which allows
+ // the browser to efficiently word-wrap right after the dot
+ // without us having to draw a lot of extra DOM elements, etc
+ return str ? str.replace(/\./g, '.\u200B') : '';
+ }
+
+ const fieldName = (
+
+ {wrapOnDot(field.displayName)}
+
+ );
+
+ return (
+ }
+ // fieldAction={actionButton}
+ fieldName={fieldName}
+ {...dragProps}
+ />
+ );
+};
diff --git a/src/plugins/wizard/public/application/components/data_tab/index.scss b/src/plugins/wizard/public/application/components/data_tab/index.scss
new file mode 100644
index 000000000000..1ba02bcc9879
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/index.scss
@@ -0,0 +1,7 @@
+@import "../util";
+
+.wizDataTab {
+ @include scrollNavParent;
+ display: grid;
+ grid-template-columns: 50% 50%;
+}
diff --git a/src/plugins/wizard/public/application/components/data_tab/index.tsx b/src/plugins/wizard/public/application/components/data_tab/index.tsx
new file mode 100644
index 000000000000..dd062f3a787d
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_tab/index.tsx
@@ -0,0 +1,19 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { FieldSelector } from './field_selector';
+import { ConfigPanel } from './config_panel';
+
+import './index.scss';
+
+export const DataTab = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss
new file mode 100644
index 000000000000..88ff7ffb0e47
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/side_nav.scss
@@ -0,0 +1,18 @@
+@import "util";
+
+.wizSidenav {
+ @include scrollNavParent(auto 1fr);
+ grid-area: sideNav;
+ border-right: $euiBorderThin;
+}
+
+.wizDatasourceSelector {
+ padding: $euiSize $euiSize 0 $euiSize;
+}
+
+.wizSidenavTabs {
+ @include scrollNavParent(min-content 1fr);
+ &>[role="tabpanel"] {
+ @include scrollNavParent;
+ }
+}
diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx
new file mode 100644
index 000000000000..2f9eab83fad3
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/side_nav.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { i18n } from '@osd/i18n';
+
+import { EuiFormLabel, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
+
+import { DataTab } from './data_tab';
+import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
+import { WizardServices } from '../../types';
+import { StyleTab } from './style_tab';
+
+import './side_nav.scss';
+import { useTypedDispatch, useTypedSelector } from '../utils/state_management';
+import { setIndexPattern } from '../utils/state_management/datasource_slice';
+
+export const SideNav = () => {
+ const {
+ services: {
+ data,
+ savedObjects: { client: savedObjectsClient },
+ },
+ } = useOpenSearchDashboards();
+ const { IndexPatternSelect } = data.ui;
+ const { indexPattern } = useTypedSelector((state) => state.dataSource);
+ const dispatch = useTypedDispatch();
+
+ const tabs: EuiTabbedContentTab[] = [
+ {
+ id: 'data-tab',
+ name: i18n.translate('wizard.nav.dataTab.title', {
+ defaultMessage: 'Data',
+ }),
+ content: ,
+ },
+ {
+ id: 'style-tab',
+ name: i18n.translate('wizard.nav.styleTab.title', {
+ defaultMessage: 'Style',
+ }),
+ content: ,
+ },
+ ];
+
+ return (
+
+
+
+ {i18n.translate('wizard.nav.dataSource.selector.title', {
+ defaultMessage: 'Index Pattern',
+ })}
+
+ {
+ const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);
+ dispatch(setIndexPattern(newIndexPattern));
+ }}
+ isClearable={false}
+ />
+
+
+
+ );
+};
diff --git a/src/plugins/wizard/public/application/components/style_tab.tsx b/src/plugins/wizard/public/application/components/style_tab.tsx
new file mode 100644
index 000000000000..3d1eb0d98b35
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/style_tab.tsx
@@ -0,0 +1,10 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+
+export const StyleTab = () => {
+ return TODO: Layout styles come here.
;
+};
diff --git a/src/plugins/wizard/public/application/components/top_nav.scss b/src/plugins/wizard/public/application/components/top_nav.scss
new file mode 100644
index 000000000000..f8e1d1d6cfa4
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/top_nav.scss
@@ -0,0 +1,4 @@
+.wizTopNav {
+ grid-area: topNav;
+ border-bottom: $euiBorderThin;
+}
\ No newline at end of file
diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx
new file mode 100644
index 000000000000..5afa39f7bafd
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/top_nav.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useMemo } from 'react';
+import { PLUGIN_ID } from '../../../common';
+import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
+import { getTopNavconfig } from '../utils/get_top_nav_config';
+import { WizardServices } from '../../types';
+
+import './top_nav.scss';
+import { useTypedSelector } from '../utils/state_management';
+
+export const TopNav = () => {
+ const { services } = useOpenSearchDashboards();
+ const {
+ setHeaderActionMenu,
+ navigation: {
+ ui: { TopNavMenu },
+ },
+ } = services;
+
+ const config = useMemo(() => getTopNavconfig(services), [services]);
+ const { indexPattern } = useTypedSelector((state) => state.dataSource);
+
+ return (
+
+
+
+ );
+};
diff --git a/src/plugins/wizard/public/application/components/workspace.scss b/src/plugins/wizard/public/application/components/workspace.scss
new file mode 100644
index 000000000000..94a97e881bf6
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/workspace.scss
@@ -0,0 +1,12 @@
+.wizWorkspace {
+ display: grid;
+ grid-template-rows: auto 1fr;
+ grid-area: workspace;
+ grid-gap: $euiSizeM;
+ padding: $euiSizeM;
+ background-color: $euiColorEmptyShade;
+}
+
+.wizWorkspace__empty {
+ height: 100%;
+}
diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx
new file mode 100644
index 000000000000..a6550a58fb80
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/workspace.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ EuiButton,
+ EuiContextMenu,
+ EuiContextMenuPanelItemDescriptor,
+ EuiEmptyPrompt,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiPanel,
+ EuiPopover,
+} from '@elastic/eui';
+import React, { FC, useState, useMemo } from 'react';
+import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
+import { WizardServices } from '../../types';
+import { useTypedDispatch, useTypedSelector } from '../utils/state_management';
+import { setActiveVisualization } from '../utils/state_management/visualization_slice';
+
+import './workspace.scss';
+
+export const Workspace: FC = ({ children }) => {
+ return (
+
+
+
+
+
+
+
+ {children ? (
+ children
+ ) : (
+
+ Welcome to the wizard!}
+ body={Drag some fields onto the panel to visualize some data.
}
+ />
+
+ )}
+
+
+ );
+};
+
+const TypeSelectorPopover = () => {
+ const [isPopoverOpen, setPopover] = useState(false);
+ const { activeVisualization: activeVisualizationId } = useTypedSelector(
+ (state) => state.visualization
+ );
+ const {
+ services: { types },
+ } = useOpenSearchDashboards();
+ const dispatch = useTypedDispatch();
+
+ // TODO: Error if no active visualization
+ const activeVisualization = types.get(activeVisualizationId || '');
+ const visualizationTypes = types.all();
+
+ const onButtonClick = () => {
+ setPopover(!isPopoverOpen);
+ };
+
+ const closePopover = () => {
+ setPopover(false);
+ };
+
+ const panels = useMemo(
+ () => [
+ {
+ id: 0,
+ title: 'Chart types',
+ items: visualizationTypes.map(
+ ({ name, title, icon, description }): EuiContextMenuPanelItemDescriptor => ({
+ name: title,
+ icon: ,
+ onClick: () => {
+ closePopover();
+ dispatch(setActiveVisualization(name));
+ },
+ toolTipContent: description,
+ toolTipPosition: 'right',
+ })
+ ),
+ },
+ ],
+ [dispatch, visualizationTypes]
+ );
+
+ const button = (
+
+ {activeVisualization?.title}
+
+ );
+
+ return (
+
+ );
+};
diff --git a/src/plugins/wizard/public/application/index.tsx b/src/plugins/wizard/public/application/index.tsx
new file mode 100644
index 000000000000..c451d082b153
--- /dev/null
+++ b/src/plugins/wizard/public/application/index.tsx
@@ -0,0 +1,35 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
+import { Provider as ReduxProvider } from 'react-redux';
+import { Store } from 'redux';
+import { AppMountParameters } from '../../../../core/public';
+import { WizardServices } from '../types';
+import { WizardApp } from './app';
+import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public';
+
+export const renderApp = (
+ { appBasePath, element }: AppMountParameters,
+ services: WizardServices,
+ store: Store
+) => {
+ ReactDOM.render(
+
+
+
+
+
+
+
+
+ ,
+ element
+ );
+
+ return () => ReactDOM.unmountComponentAtNode(element);
+};
diff --git a/src/plugins/wizard/public/application/utils/async_search/index.ts b/src/plugins/wizard/public/application/utils/async_search/index.ts
new file mode 100644
index 000000000000..9746cde24e4c
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/async_search/index.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { CreateAggConfigParams } from 'src/plugins/data/common';
+import { DataPublicPluginStart, IndexPattern } from 'src/plugins/data/public';
+
+interface IDoAsyncSearch {
+ data: DataPublicPluginStart;
+ indexPattern: IndexPattern | null;
+ aggs?: CreateAggConfigParams[];
+}
+
+export const doAsyncSearch = async ({ data, indexPattern, aggs }: IDoAsyncSearch) => {
+ if (!indexPattern || !aggs || !aggs.length) return;
+
+ // Constuct the query portion of the search request
+ const query = data.query.getOpenSearchQuery(indexPattern);
+
+ // Constuct the aggregations portion of the search request by using the `data.search.aggs` service.
+ // const aggs = [{ type: 'avg', params: { field: field.name } }];
+ // const aggs = [
+ // { type: 'terms', params: { field: 'day_of_week' } },
+ // { type: 'avg', params: { field: field.name } },
+ // { type: 'terms', params: { field: 'customer_gender' } },
+ // ];
+ const aggConfigs = data.search.aggs.createAggConfigs(indexPattern, aggs);
+ const aggsDsl = aggConfigs.toDsl();
+
+ const request = {
+ params: {
+ index: indexPattern.title,
+ body: {
+ aggs: aggsDsl,
+ query,
+ },
+ },
+ };
+
+ // Submit the search request using the `data.search` service.
+ const { rawResponse } = await data.search.search(request).toPromise();
+
+ return {
+ rawResponse,
+ aggConfigs,
+ };
+};
diff --git a/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx
new file mode 100644
index 000000000000..a89226885d5d
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/drag_drop/drag_drop_context.tsx
@@ -0,0 +1,113 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { createContext, DragEvent, FC, ReactNode, useContext, useState } from 'react';
+
+interface DrapDataType {
+ namespace: string;
+ value: any;
+}
+
+// TODO: Replace any with corret type
+// TODO: Split into separate files
+interface IDragDropContext {
+ data?: DrapDataType;
+ setData?: any;
+ isDragging: boolean;
+ setIsDragging?: any;
+}
+
+const defaultContextProps = {
+ isDragging: false,
+};
+
+const DragDropContext = createContext(defaultContextProps);
+
+const DragDropProvider: FC = ({ children }) => {
+ const [isDragging, setIsDragging] = useState(false);
+ const [data, setData] = useState();
+ return (
+
+ {children}
+
+ );
+};
+
+const useDragDropContext = () => useContext(DragDropContext);
+
+const useDrag = (dragData: any, namespace: string) => {
+ const { setData, setIsDragging } = useDragDropContext();
+ const dragElementProps = {
+ draggable: true,
+ onDragStart: (event: DragEvent) => {
+ setIsDragging(true);
+ setData({
+ namespace,
+ value: dragData,
+ });
+ },
+ onDragEnd: (event: DragEvent) => {
+ setIsDragging(false);
+ setData(null);
+ },
+ };
+ return [dragElementProps];
+};
+
+interface IDropAttributes {
+ onDragOver: (event: DragEvent) => void;
+ onDrop: (event: DragEvent) => void;
+ onDragEnter: (event: DragEvent) => void;
+ onDragLeave: (event: DragEvent) => void;
+}
+
+interface IDropState {
+ isDragging: boolean;
+ canDrop: boolean;
+ isValidDropTarget: boolean;
+ dragData: any;
+}
+const useDrop = (namespace: string, onDropCallback: Function): [IDropAttributes, IDropState] => {
+ const { data, isDragging, setIsDragging, setData } = useDragDropContext();
+ const [canDrop, setCanDrop] = useState(false);
+
+ const dropAttributes: IDropAttributes = {
+ onDragOver: (event) => {
+ event.preventDefault();
+ },
+ onDrop: (event) => {
+ setIsDragging(false);
+ onDropCallback(data?.value);
+ setData(null);
+ },
+ onDragEnter: (event) => {
+ if (data?.namespace === namespace) {
+ setCanDrop(true);
+ }
+ },
+ onDragLeave: (event) => {
+ setCanDrop(false);
+ },
+ };
+ return [
+ dropAttributes,
+ {
+ isDragging,
+ canDrop,
+ isValidDropTarget: isDragging && data?.namespace === namespace,
+ dragData: data?.value,
+ },
+ ];
+};
+
+export { DragDropContext, DragDropProvider, useDragDropContext, useDrag, useDrop };
diff --git a/src/plugins/wizard/public/application/utils/drag_drop/index.ts b/src/plugins/wizard/public/application/utils/drag_drop/index.ts
new file mode 100644
index 000000000000..3799a2eb6052
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/drag_drop/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './drag_drop_context';
diff --git a/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx
new file mode 100644
index 000000000000..725f7f2baa92
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/get_top_nav_config.tsx
@@ -0,0 +1,127 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Any modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { i18n } from '@osd/i18n';
+import { TopNavMenuData } from '../../../../navigation/public';
+import {
+ OnSaveProps,
+ SavedObjectSaveModalOrigin,
+ showSaveModal,
+} from '../../../../saved_objects/public';
+import { WizardServices } from '../..';
+
+export const getTopNavconfig = ({
+ savedObjects: { client: savedObjectsClient },
+ toastNotifications,
+ i18n: { Context: I18nContext },
+}: WizardServices) => {
+ const topNavConfig: TopNavMenuData[] = [
+ {
+ id: 'save',
+ iconType: 'save',
+ emphasize: true,
+ label: 'Save',
+ testId: 'wizardSaveButton',
+ run: (anchorElement) => {
+ const onSave = async ({
+ // TODO: Figure out what the other props here do
+ newTitle,
+ newCopyOnSave,
+ isTitleDuplicateConfirmed,
+ onTitleDuplicate,
+ newDescription,
+ returnToOrigin,
+ }: OnSaveProps & { returnToOrigin: boolean }) => {
+ // TODO: Save the actual state of the wizard
+ const wizardSavedObject = await savedObjectsClient.create('wizard', {
+ title: newTitle,
+ description: newDescription,
+ state: JSON.stringify({}),
+ });
+
+ try {
+ const id = await wizardSavedObject.save();
+
+ if (id) {
+ toastNotifications.addSuccess({
+ title: i18n.translate(
+ 'wizard.topNavMenu.saveVisualization.successNotificationText',
+ {
+ defaultMessage: `Saved '{visTitle}'`,
+ values: {
+ visTitle: newTitle,
+ },
+ }
+ ),
+ 'data-test-subj': 'saveVisualizationSuccess',
+ });
+
+ return { id };
+ }
+
+ throw new Error('Saved but no id returned');
+ } catch (error: any) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+
+ toastNotifications.addDanger({
+ title: i18n.translate(
+ 'visualize.topNavMenu.saveVisualization.failureNotificationText',
+ {
+ defaultMessage: `Error on saving '{visTitle}'`,
+ values: {
+ visTitle: newTitle,
+ },
+ }
+ ),
+ text: error.message,
+ 'data-test-subj': 'saveVisualizationError',
+ });
+ return { error };
+ }
+ };
+
+ const saveModal = (
+ {}}
+ />
+ );
+
+ showSaveModal(saveModal, I18nContext);
+ },
+ },
+ ];
+
+ return topNavConfig;
+};
diff --git a/src/plugins/wizard/public/application/utils/state_management/config_slice.ts b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts
new file mode 100644
index 000000000000..5d8908596104
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { IndexPatternField } from '../../../../../data/public';
+
+interface ConfigSections {
+ [id: string]: {
+ title: string;
+ fields: IndexPatternField[];
+ };
+}
+interface ConfigState {
+ configSections: ConfigSections;
+}
+
+// TODO: Temp. Remove once visualizations can be refgistered and editor configs can be passed along
+// TODO: this is a placeholder while the config section is iorned out
+const initialState: ConfigState = {
+ configSections: {
+ x: {
+ title: 'X Axis',
+ fields: [],
+ },
+ y: {
+ title: 'Y Axis',
+ fields: [],
+ },
+ },
+};
+
+interface SectionField {
+ sectionId: string;
+ field: IndexPatternField;
+}
+
+export const slice = createSlice({
+ name: 'configuration',
+ initialState,
+ reducers: {
+ addConfigSectionField: (state, action: PayloadAction) => {
+ const { field, sectionId } = action.payload;
+ if (state.configSections[sectionId]) {
+ state.configSections[sectionId].fields.push(field);
+ }
+ },
+ removeConfigSectionField: (state, action: PayloadAction) => {
+ const { field, sectionId } = action.payload;
+ if (state.configSections[sectionId]) {
+ const fieldIndex = state.configSections[sectionId].fields.findIndex(
+ (configField) => configField === field
+ );
+ if (fieldIndex !== -1) state.configSections[sectionId].fields.splice(fieldIndex, 1);
+ }
+ },
+ },
+});
+
+export const { reducer } = slice;
+export const { addConfigSectionField, removeConfigSectionField } = slice.actions;
diff --git a/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts
new file mode 100644
index 000000000000..d51d463d68ee
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { IndexPattern } from 'src/plugins/data/common';
+import { WizardServices } from '../../../types';
+
+import { IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public';
+
+const ALLOWED_FIELDS: string[] = [OSD_FIELD_TYPES.STRING, OSD_FIELD_TYPES.NUMBER];
+
+interface DataSourceState {
+ indexPattern: IndexPattern | null;
+ visualizableFields: IndexPatternField[];
+ searchField: string;
+}
+
+const initialState: DataSourceState = {
+ indexPattern: null,
+ visualizableFields: [],
+ searchField: '',
+};
+
+export const getPreloadedState = async ({ data }: WizardServices): Promise => {
+ const preloadedState = { ...initialState };
+
+ const defaultIndexPattern = await data.indexPatterns.getDefault();
+ if (defaultIndexPattern) {
+ preloadedState.indexPattern = defaultIndexPattern;
+ preloadedState.visualizableFields = defaultIndexPattern.fields.filter(isVisualizable);
+ }
+
+ return preloadedState;
+};
+
+export const slice = createSlice({
+ name: 'dataSource',
+ initialState,
+ reducers: {
+ setIndexPattern: (state, action: PayloadAction) => {
+ state.indexPattern = action.payload;
+ state.visualizableFields = action.payload.fields.filter(isVisualizable);
+ },
+ setSearchField: (state, action: PayloadAction) => {
+ state.searchField = action.payload;
+ },
+ },
+});
+
+export const { reducer } = slice;
+export const { setIndexPattern, setSearchField } = slice.actions;
+
+// TODO: Temporary validate function
+// Need to identify how to get fieldCounts to use the standard filter and group functions
+function isVisualizable(field: IndexPatternField): boolean {
+ const isAggregatable = field.aggregatable === true;
+ const isNotScripted = !field.scripted;
+ const isAllowed = ALLOWED_FIELDS.includes(field.type);
+
+ return isAggregatable && isNotScripted && isAllowed;
+}
diff --git a/src/plugins/wizard/public/application/utils/state_management/hooks.ts b/src/plugins/wizard/public/application/utils/state_management/hooks.ts
new file mode 100644
index 000000000000..823c34528c90
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/state_management/hooks.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { RootState, AppDispatch } from './store';
+
+// Use throughout your app instead of plain `useDispatch` and `useSelector`
+export const useTypedDispatch = () => useDispatch();
+export const useTypedSelector: TypedUseSelectorHook = useSelector;
diff --git a/src/plugins/wizard/public/application/utils/state_management/index.ts b/src/plugins/wizard/public/application/utils/state_management/index.ts
new file mode 100644
index 000000000000..edb5c2a17184
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/state_management/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './store';
+export * from './hooks';
diff --git a/src/plugins/wizard/public/application/utils/state_management/preload.ts b/src/plugins/wizard/public/application/utils/state_management/preload.ts
new file mode 100644
index 000000000000..ad78b642c23e
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/state_management/preload.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { PreloadedState } from '@reduxjs/toolkit';
+import { WizardServices } from '../../..';
+import { getPreloadedState as getPreloadedDatasourceState } from './datasource_slice';
+import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice';
+import { RootState } from './store';
+
+export const getPreloadedState = async (
+ services: WizardServices
+): Promise> => {
+ const dataSourceState = await getPreloadedDatasourceState(services);
+ const visualizationState = await getPreloadedVisualizationState(services);
+
+ return {
+ dataSource: dataSourceState,
+ visualization: visualizationState,
+ };
+};
diff --git a/src/plugins/wizard/public/application/utils/state_management/store.ts b/src/plugins/wizard/public/application/utils/state_management/store.ts
new file mode 100644
index 000000000000..4fa56c1a7c97
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/state_management/store.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit';
+import { reducer as dataSourceReducer } from './datasource_slice';
+import { reducer as configReducer } from './config_slice';
+import { reducer as visualizationReducer } from './visualization_slice';
+import { WizardServices } from '../../..';
+import { getPreloadedState } from './preload';
+
+const rootReducer = combineReducers({
+ dataSource: dataSourceReducer,
+ config: configReducer,
+ visualization: visualizationReducer,
+});
+
+export const configurePreloadedStore = (preloadedState: PreloadedState) => {
+ return configureStore({
+ reducer: rootReducer,
+ preloadedState,
+ });
+};
+
+export const getPreloadedStore = async (services: WizardServices) => {
+ const preloadedState = await getPreloadedState(services);
+ return configurePreloadedStore(preloadedState);
+};
+
+// Infer the `RootState` and `AppDispatch` types from the store itself
+export type RootState = ReturnType;
+type Store = ReturnType;
+export type AppDispatch = Store['dispatch'];
diff --git a/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts
new file mode 100644
index 000000000000..692f9434c8de
--- /dev/null
+++ b/src/plugins/wizard/public/application/utils/state_management/visualization_slice.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { WizardServices } from '../../../types';
+
+interface VisualizationState {
+ activeVisualization: string | null;
+}
+
+const initialState: VisualizationState = {
+ activeVisualization: null,
+};
+
+export const getPreloadedState = async ({ types }: WizardServices): Promise => {
+ const preloadedState = { ...initialState };
+
+ const defaultVisualization = types.all()[0];
+ if (defaultVisualization) {
+ preloadedState.activeVisualization = defaultVisualization.name;
+ }
+
+ return preloadedState;
+};
+
+export const slice = createSlice({
+ name: 'visualization',
+ initialState,
+ reducers: {
+ setActiveVisualization: (state, action: PayloadAction) => {
+ state.activeVisualization = action.payload;
+ },
+ },
+});
+
+export const { reducer } = slice;
+export const { setActiveVisualization } = slice.actions;
diff --git a/src/plugins/wizard/public/index.ts b/src/plugins/wizard/public/index.ts
new file mode 100644
index 000000000000..97f9007549a0
--- /dev/null
+++ b/src/plugins/wizard/public/index.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { PluginInitializerContext } from '../../../core/public';
+import { WizardPlugin } from './plugin';
+
+// This exports static code and TypeScript types,
+// as well as, OpenSearch Dashboards Platform `plugin()` initializer.
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new WizardPlugin(initializerContext);
+}
+export { WizardServices, WizardPluginStartDependencies } from './types';
diff --git a/src/plugins/wizard/public/plugin.ts b/src/plugins/wizard/public/plugin.ts
new file mode 100644
index 000000000000..5b309080a872
--- /dev/null
+++ b/src/plugins/wizard/public/plugin.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { i18n } from '@osd/i18n';
+import {
+ AppMountParameters,
+ AppNavLinkStatus,
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ PluginInitializerContext,
+} from '../../../core/public';
+import {
+ WizardPluginSetupDependencies,
+ WizardPluginStartDependencies,
+ WizardServices,
+ WizardSetup,
+} from './types';
+import { PLUGIN_NAME } from '../common';
+import { TypeService } from './services/type_service';
+import { getPreloadedStore } from './application/utils/state_management';
+
+export class WizardPlugin
+ implements
+ Plugin {
+ private typeService = new TypeService();
+
+ constructor(public initializerContext: PluginInitializerContext) {}
+
+ public setup(
+ core: CoreSetup,
+ { visualizations }: WizardPluginSetupDependencies
+ ) {
+ const typeService = this.typeService;
+ // Register the plugin to core
+ core.application.register({
+ id: 'wizard',
+ title: PLUGIN_NAME,
+ navLinkStatus: AppNavLinkStatus.hidden,
+ async mount(params: AppMountParameters) {
+ // Load application bundle
+ const { renderApp } = await import('./application');
+ // Get start services as specified in opensearch_dashboards.json
+ const [coreStart, pluginsStart] = await core.getStartServices();
+ const { data, savedObjects, navigation } = pluginsStart;
+
+ const { registerDefaultTypes } = await import('./visualizations');
+ registerDefaultTypes(typeService.setup());
+
+ const services: WizardServices = {
+ ...coreStart,
+ toastNotifications: coreStart.notifications.toasts,
+ data,
+ savedObjectsPublic: savedObjects,
+ navigation,
+ setHeaderActionMenu: params.setHeaderActionMenu,
+ types: typeService.start(),
+ };
+
+ // make sure the index pattern list is up to date
+ data.indexPatterns.clearCache();
+ // make sure a default index pattern exists
+ // if not, the page will be redirected to management and visualize won't be rendered
+ // TODO: Add the redirect
+ await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern();
+
+ const store = await getPreloadedStore(services);
+
+ // Render the application
+ return renderApp(params, services, store);
+ },
+ });
+
+ // Register the plugin as an alias to create visualization
+ visualizations.registerAlias({
+ name: 'wizard',
+ title: 'Wizard',
+ description: i18n.translate('wizard.vizPicker.description', {
+ defaultMessage: 'TODO...',
+ }),
+ // TODO: Replace with actual icon once available
+ icon: 'vector',
+ stage: 'beta',
+ aliasApp: 'wizard',
+ aliasPath: '#/',
+ });
+
+ return {
+ ...typeService.setup(),
+ };
+ }
+
+ public start(core: CoreStart) {}
+
+ public stop() {}
+}
diff --git a/src/plugins/wizard/public/services/type_service/index.ts b/src/plugins/wizard/public/services/type_service/index.ts
new file mode 100644
index 000000000000..1fae953fb9b8
--- /dev/null
+++ b/src/plugins/wizard/public/services/type_service/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * from './type_service';
diff --git a/src/plugins/wizard/public/services/type_service/type_service.ts b/src/plugins/wizard/public/services/type_service/type_service.ts
new file mode 100644
index 000000000000..d43d779f75ea
--- /dev/null
+++ b/src/plugins/wizard/public/services/type_service/type_service.ts
@@ -0,0 +1,86 @@
+/*
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * The OpenSearch Contributors require contributions made to
+ * this file be licensed under the Apache-2.0 license or a
+ * compatible open source license.
+ *
+ * Any modifications Copyright OpenSearch Contributors. See
+ * GitHub history for details.
+ */
+
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { VisualizationType, VisualizationTypeOptions } from './visualization_type';
+
+/**
+ * Vis Types Service
+ *
+ * @internal
+ */
+export class TypeService {
+ private types: Record = {};
+
+ private registerVisualizationType(visDefinition: VisualizationType) {
+ if (this.types[visDefinition.name]) {
+ throw new Error('type already exists!');
+ }
+ this.types[visDefinition.name] = visDefinition;
+ }
+
+ public setup() {
+ return {
+ /**
+ * registers a visualization type
+ * @param config - visualization type definition
+ */
+ createVisualizationType: (config: VisualizationTypeOptions): void => {
+ const vis = new VisualizationType(config);
+ this.registerVisualizationType(vis);
+ },
+ };
+ }
+
+ public start() {
+ return {
+ /**
+ * returns specific visualization or undefined if not found
+ * @param {string} visualization - id of visualization to return
+ */
+ get: (visualization: string): VisualizationType | undefined => {
+ return this.types[visualization];
+ },
+ /**
+ * returns all registered visualization types
+ */
+ all: (): VisualizationType[] => {
+ return [...Object.values(this.types)];
+ },
+ };
+ }
+
+ public stop() {
+ // nothing to do here yet
+ }
+}
+
+/** @internal */
+export type TypeServiceSetup = ReturnType;
+export type TypeServiceStart = ReturnType;
diff --git a/src/plugins/wizard/public/services/type_service/visualization_type.ts b/src/plugins/wizard/public/services/type_service/visualization_type.ts
new file mode 100644
index 000000000000..cddb000f41db
--- /dev/null
+++ b/src/plugins/wizard/public/services/type_service/visualization_type.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { IconType } from '@elastic/eui';
+
+export interface VisualizationTypeOptions {
+ readonly name: string;
+ readonly title: string;
+ readonly description?: string;
+ readonly icon: IconType;
+ readonly stage?: 'beta' | 'production';
+ readonly contributions: {
+ containers?: {
+ // Define new or override existing view containers
+ name: string;
+ title: string;
+ location: 'panel' | 'toolbar';
+ // render: (schemas: ContainerSchema[]) => {}; // recieves an array of items to render within the container
+ };
+ items?: {
+ 'container-name': any[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency
+ // 'container-name': ContainerSchema[]; // schema that is used to render the container. Each container is responsible for deciding that for consistency
+ };
+ };
+ // pipeline: Expression;
+}
+
+export type IVisualizationType = Required;
+
+export class VisualizationType implements IVisualizationType {
+ public readonly name;
+ public readonly title;
+ public readonly description;
+ public readonly icon;
+ public readonly stage;
+ public readonly contributions;
+
+ constructor(options: VisualizationTypeOptions) {
+ this.name = options.name;
+ this.title = options.title;
+ this.description = options.description ?? '';
+ this.icon = options.icon;
+ this.stage = options.stage ?? 'production';
+ this.contributions = options.contributions;
+ }
+}
diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts
new file mode 100644
index 000000000000..07b1e5141c61
--- /dev/null
+++ b/src/plugins/wizard/public/types.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectsStart } from 'src/plugins/saved_objects/public';
+import { AppMountParameters, CoreStart, ToastsStart } from 'opensearch-dashboards/public';
+import { EmbeddableSetup } from 'src/plugins/embeddable/public';
+import { DashboardStart } from 'src/plugins/dashboard/public';
+import { VisualizationsSetup } from 'src/plugins/visualizations/public';
+import { NavigationPublicPluginStart } from '../../navigation/public';
+import { DataPublicPluginStart } from '../../data/public';
+import { TypeServiceSetup, TypeServiceStart } from './services/type_service';
+
+export type WizardSetup = TypeServiceSetup;
+
+export interface WizardPluginSetupDependencies {
+ embeddable: EmbeddableSetup;
+ visualizations: VisualizationsSetup;
+}
+export interface WizardPluginStartDependencies {
+ navigation: NavigationPublicPluginStart;
+ data: DataPublicPluginStart;
+ savedObjects: SavedObjectsStart;
+ dashboard: DashboardStart;
+}
+
+export interface WizardServices extends CoreStart {
+ setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
+ toastNotifications: ToastsStart;
+ savedObjectsPublic: SavedObjectsStart;
+ navigation: NavigationPublicPluginStart;
+ data: DataPublicPluginStart;
+ types: TypeServiceStart;
+}
diff --git a/src/plugins/wizard/public/visualizations/bar_chart/index.ts b/src/plugins/wizard/public/visualizations/bar_chart/index.ts
new file mode 100644
index 000000000000..cc05f790993f
--- /dev/null
+++ b/src/plugins/wizard/public/visualizations/bar_chart/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { VisualizationTypeOptions } from '../../services/type_service/visualization_type';
+
+export const createBarChartConfig = (): VisualizationTypeOptions => {
+ return {
+ name: 'bar_chart',
+ title: 'Bar Chart',
+ icon: 'visBarVertical',
+ description: 'This is a bar chart',
+ contributions: {},
+ };
+};
diff --git a/src/plugins/wizard/public/visualizations/index.ts b/src/plugins/wizard/public/visualizations/index.ts
new file mode 100644
index 000000000000..604de170c8ab
--- /dev/null
+++ b/src/plugins/wizard/public/visualizations/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import type { TypeServiceSetup } from '../services/type_service';
+import { createBarChartConfig } from './bar_chart';
+import { createPieChartConfig } from './pie_chart';
+
+export function registerDefaultTypes(typeServieSetup: TypeServiceSetup) {
+ const visualizationTypes = [createBarChartConfig, createPieChartConfig];
+
+ visualizationTypes.forEach((createTypeConfig) => {
+ typeServieSetup.createVisualizationType(createTypeConfig());
+ });
+}
diff --git a/src/plugins/wizard/public/visualizations/pie_chart/index.ts b/src/plugins/wizard/public/visualizations/pie_chart/index.ts
new file mode 100644
index 000000000000..b47965bc1905
--- /dev/null
+++ b/src/plugins/wizard/public/visualizations/pie_chart/index.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { VisualizationTypeOptions } from '../../services/type_service/visualization_type';
+
+export const createPieChartConfig = (): VisualizationTypeOptions => {
+ return {
+ name: 'pie_chart',
+ title: 'Pie Chart',
+ icon: 'visPie',
+ contributions: {},
+ };
+};
diff --git a/src/plugins/wizard/server/index.ts b/src/plugins/wizard/server/index.ts
new file mode 100644
index 000000000000..e995ea17b4a7
--- /dev/null
+++ b/src/plugins/wizard/server/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { PluginInitializerContext } from '../../../core/server';
+import { WizardPlugin } from './plugin';
+
+// This exports static code and TypeScript types,
+// as well as, OpenSearch Dashboards Platform `plugin()` initializer.
+
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new WizardPlugin(initializerContext);
+}
+
+export { WizardPluginSetup, WizardPluginStart } from './types';
diff --git a/src/plugins/wizard/server/plugin.ts b/src/plugins/wizard/server/plugin.ts
new file mode 100644
index 000000000000..d45e4081cce9
--- /dev/null
+++ b/src/plugins/wizard/server/plugin.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ Logger,
+} from '../../../core/server';
+
+import { WizardPluginSetup, WizardPluginStart } from './types';
+import { defineRoutes } from './routes';
+import { wizardApp } from './saved_objects';
+
+export class WizardPlugin implements Plugin {
+ private readonly logger: Logger;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this.logger = initializerContext.logger.get();
+ }
+
+ public setup({ http, savedObjects }: CoreSetup) {
+ this.logger.debug('wizard: Setup');
+ const router = http.createRouter();
+
+ // Register server side APIs
+ defineRoutes(router);
+
+ // Register saved object types
+ savedObjects.registerType(wizardApp);
+
+ return {};
+ }
+
+ public start(core: CoreStart) {
+ this.logger.debug('wizard: Started');
+ return {};
+ }
+
+ public stop() {}
+}
diff --git a/src/plugins/wizard/server/routes/index.ts b/src/plugins/wizard/server/routes/index.ts
new file mode 100644
index 000000000000..f6268695e838
--- /dev/null
+++ b/src/plugins/wizard/server/routes/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { IRouter } from '../../../../core/server';
+
+export function defineRoutes(router: IRouter) {
+ router.get(
+ {
+ path: '/api/wizard/example',
+ validate: false,
+ },
+ async (context, request, response) => {
+ return response.ok({
+ body: {
+ time: new Date().toISOString(),
+ },
+ });
+ }
+ );
+}
diff --git a/src/plugins/wizard/server/saved_objects/index.ts b/src/plugins/wizard/server/saved_objects/index.ts
new file mode 100644
index 000000000000..aa90fcea911b
--- /dev/null
+++ b/src/plugins/wizard/server/saved_objects/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { wizardApp } from './wizard_app';
diff --git a/src/plugins/wizard/server/saved_objects/wizard_app.ts b/src/plugins/wizard/server/saved_objects/wizard_app.ts
new file mode 100644
index 000000000000..138bea03b22a
--- /dev/null
+++ b/src/plugins/wizard/server/saved_objects/wizard_app.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectsType } from 'src/core/server';
+import { WIZARD_SAVED_OBJECT } from '../../common';
+
+export const wizardApp: SavedObjectsType = {
+ name: WIZARD_SAVED_OBJECT,
+ hidden: false,
+ namespaceType: 'single',
+ management: {
+ icon: 'visVisualBuilder', // TODO: Need a custom icon here
+ defaultSearchField: 'title',
+ importableAndExportable: true,
+ getTitle: (obj: { attributes: { title: string } }) => obj.attributes.title,
+ // getInAppUrl: TODO: Enable once editing is supported
+ },
+ migrations: {},
+ mappings: {
+ properties: {
+ title: {
+ type: 'text',
+ },
+ description: {
+ type: 'text',
+ },
+ // TODO: Determine what needs to be pulled out of state and added directly into the mapping
+ state: {
+ type: 'text',
+ index: false,
+ },
+ },
+ },
+};
diff --git a/src/plugins/wizard/server/types.ts b/src/plugins/wizard/server/types.ts
new file mode 100644
index 000000000000..5d26185a0374
--- /dev/null
+++ b/src/plugins/wizard/server/types.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface WizardPluginSetup {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface WizardPluginStart {}
diff --git a/yarn.lock b/yarn.lock
index 5804cdfa640c..ef19a747860b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2544,6 +2544,16 @@
colors "~1.2.1"
string-argv "~0.3.1"
+"@reduxjs/toolkit@^1.6.2":
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37"
+ integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA==
+ dependencies:
+ immer "^9.0.6"
+ redux "^4.1.0"
+ redux-thunk "^2.3.0"
+ reselect "^4.0.0"
+
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz#a21117b19ee9be70c379ec1877537ef2e1c63301"
@@ -14983,6 +14993,13 @@ redux@^4.0.0, redux@^4.0.4, redux@^4.0.5:
dependencies:
"@babel/runtime" "^7.9.2"
+redux@^4.1.0:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104"
+ integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+
reflect.ownkeys@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"