diff --git a/src/plugins/wizard/public/application/_variables.scss b/src/plugins/wizard/public/application/_variables.scss
index c1b3646e8e49..71d3b2c3b195 100644
--- a/src/plugins/wizard/public/application/_variables.scss
+++ b/src/plugins/wizard/public/application/_variables.scss
@@ -1,3 +1,7 @@
@import '@elastic/eui/src/global_styling/variables/header';
+@import '@elastic/eui/src/global_styling/variables/form';
-$osdHeaderOffset: $euiHeaderHeightCompensation * 2;
\ No newline at end of file
+$osdHeaderOffset: $euiHeaderHeightCompensation * 2;
+$osdDropdownNegativePadding: $euiFormControlPadding * -1;
+$osdDropdownPadding: $euiFormControlPadding;
+$wizSideNavWidth: 470px;
diff --git a/src/plugins/wizard/public/application/app.scss b/src/plugins/wizard/public/application/app.scss
index 4f05c6ecd7a2..3e2a28aca700 100644
--- a/src/plugins/wizard/public/application/app.scss
+++ b/src/plugins/wizard/public/application/app.scss
@@ -4,7 +4,7 @@
padding: 0;
display: grid;
grid-template-rows: min-content 1fr;
- grid-template-columns: 470px 1fr;
+ grid-template-columns: $wizSideNavWidth 1fr;
grid-template-areas:
"topNav topNav"
"sideNav workspace"
diff --git a/src/plugins/wizard/public/application/components/data_source_select.scss b/src/plugins/wizard/public/application/components/data_source_select.scss
new file mode 100644
index 000000000000..3e1bc7cf2775
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_source_select.scss
@@ -0,0 +1,5 @@
+@import "../variables";
+
+.wizDatasourceSelect {
+ max-width: 469px;
+}
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..313ecf55cfde
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/data_source_select.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 './data_source_select.scss';
+import indexPatternSvg from '../../assets/index_pattern.svg';
+import { useTypedDispatch } from '../utils/state_management';
+import { setIndexPattern } from '../utils/state_management/visualization_slice';
+import { IndexPattern } from '../../../../data/public';
+
+function indexPatternEquality(A?: SearchableDropdownOption, B?: SearchableDropdownOption): boolean {
+ return !A || !B ? false : A.id === B.id;
+}
+
+function toSearchableDropdownOption(indexPattern: IndexPattern): SearchableDropdownOption {
+ return {
+ id: indexPattern.id || '',
+ label: indexPattern.title,
+ searchableLabel: indexPattern.title,
+ prepend: ,
+ };
+}
+
+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/searchable_dropdown.scss b/src/plugins/wizard/public/application/components/searchable_dropdown.scss
new file mode 100644
index 000000000000..616741b3a863
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/searchable_dropdown.scss
@@ -0,0 +1,26 @@
+@import "../variables";
+
+.searchableDropdown {
+ overflow: "hidden";
+}
+
+.searchableDropdown .euiPopover,
+.searchableDropdown .euiPopover__anchor {
+ width: 100%;
+}
+
+.searchableDropdown--fixedWidthChild {
+ width: calc(#{$wizSideNavWidth} - #{$euiSizeXL} * 2) ;
+}
+
+.searchableDropdown--topDisplay {
+ padding-right: $euiSizeL;
+}
+
+
+.searchableDropdown--selectableWrapper .euiSelectableList {
+ // When clicking on the selectable content it will "highlight" itself with a box shadow
+ // This turns that off
+ box-shadow: none !important;
+ margin: $osdDropdownNegativePadding - 4;
+}
diff --git a/src/plugins/wizard/public/application/components/searchable_dropdown.tsx b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx
new file mode 100644
index 000000000000..da6795335088
--- /dev/null
+++ b/src/plugins/wizard/public/application/components/searchable_dropdown.tsx
@@ -0,0 +1,170 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useEffect } from 'react';
+import {
+ EuiLoadingSpinner,
+ EuiFormControlLayout,
+ EuiPopoverTitle,
+ EuiButtonEmpty,
+ EuiPopover,
+ EuiSelectable,
+ EuiTextColor,
+} from '@elastic/eui';
+import './searchable_dropdown.scss';
+
+export interface SearchableDropdownOption {
+ id: string;
+ label: string;
+ searchableLabel: string;
+ prepend: any;
+}
+
+interface SearchableDropdownProps {
+ selected?: SearchableDropdownOption;
+ onChange: (selection) => void;
+ options: SearchableDropdownOption[];
+ loading: boolean;
+ error?: Error;
+ prepend: string;
+ // not just the first time!
+ onOpen?: () => void;
+ equality: (A, B) => boolean;
+}
+
+type DisplayError = any;
+
+function displayError(error: DisplayError) {
+ return typeof error === 'object' ? error.toString() : <>{error}>;
+}
+
+export const SearchableDropdown = ({
+ onChange,
+ equality,
+ selected,
+ options,
+ error,
+ loading,
+ prepend,
+ onOpen,
+}: SearchableDropdownProps) => {
+ const [localOptions, setLocalOptions] = useState(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 ? (
+
+
+
+ ) : error !== undefined ? (
+ displayError(error)
+ ) : (
+ <>
+
+ {search}
+
+ {list}
+ >
+ );
+
+ const selectable = (
+
+
+ {listDisplay}
+
+
+ );
+
+ const selectedText =
+ selected === undefined ? (
+ {loading ? 'Loading' : 'Select an option'}
+ ) : (
+ <>
+ {selected.prepend} {selected.label}
+ >
+ );
+
+ const selectedView = (
+
+ {selectedText}
+
+ );
+
+ const formControl = {selectedView}
+
+ return (
+
+ );
+};
diff --git a/src/plugins/wizard/public/application/components/side_nav.scss b/src/plugins/wizard/public/application/components/side_nav.scss
index caa24e30b395..c279eb4df248 100644
--- a/src/plugins/wizard/public/application/components/side_nav.scss
+++ b/src/plugins/wizard/public/application/components/side_nav.scss
@@ -7,10 +7,6 @@
border-right: $euiBorderThin;
}
-.wizDatasourceSelector {
- padding: $euiSize $euiSize 0 $euiSize;
-}
-
.wizSidenavTabs {
.euiTab__content {
text-transform: capitalize;
@@ -22,3 +18,7 @@
@include scrollNavParent;
}
}
+
+.wizDatasourceSelect {
+ padding: $euiSize $euiSize 0 $euiSize;
+}
diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx
index c74837ceab54..c79f63d7b5c0 100644
--- a/src/plugins/wizard/public/application/components/side_nav.tsx
+++ b/src/plugins/wizard/public/application/components/side_nav.tsx
@@ -3,35 +3,22 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React from 'react';
-import { i18n } from '@osd/i18n';
-import { EuiFormLabel, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
-import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
-import { WizardServices } from '../../types';
+import React, { ReactElement } from 'react';
+import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
import './side_nav.scss';
-import { useTypedDispatch, useTypedSelector } from '../utils/state_management';
-import { setIndexPattern } from '../utils/state_management/visualization_slice';
import { useVisualizationType } from '../utils/use';
+import { DataSourceSelect } from './data_source_select';
import { DataTab } from '../contributions';
import { StyleTabConfig } from '../../services/type_service';
export const SideNav = () => {
- const {
- services: {
- data,
- savedObjects: { client: savedObjectsClient },
- },
- } = useOpenSearchDashboards();
- const { IndexPatternSelect } = data.ui;
- const { indexPattern: indexPatternId } = useTypedSelector((state) => state.visualization);
- const dispatch = useTypedDispatch();
const {
ui: { containerConfig },
} = useVisualizationType();
const tabs: EuiTabbedContentTab[] = Object.entries(containerConfig).map(
([containerName, config]) => {
- let content = null;
+ let content: null | ReactElement = null;
switch (containerName) {
case 'data':
content = ;
@@ -52,27 +39,8 @@ export const SideNav = () => {
return (
-
-
- {i18n.translate('wizard.nav.dataSource.selector.title', {
- defaultMessage: 'Index Pattern',
- })}
-
-
{
- const newIndexPattern = await data.indexPatterns.get(newIndexPatternId);
-
- if (newIndexPattern) {
- dispatch(setIndexPattern(newIndexPatternId));
- }
- }}
- isClearable={false}
- />
+
+
diff --git a/src/plugins/wizard/public/application/utils/use/index.ts b/src/plugins/wizard/public/application/utils/use/index.ts
index 2893ab0d11ff..542ae073cde5 100644
--- a/src/plugins/wizard/public/application/utils/use/index.ts
+++ b/src/plugins/wizard/public/application/utils/use/index.ts
@@ -4,4 +4,4 @@
*/
export { useVisualizationType } from './use_visualization_type';
-export { useIndexPattern } from './use_index_pattern';
+export { useIndexPattern, useIndexPatterns } from './use_index_pattern';
diff --git a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx
index e02f543d6c15..d5f255fcb10c 100644
--- a/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx
+++ b/src/plugins/wizard/public/application/utils/use/use_index_pattern.tsx
@@ -28,3 +28,47 @@ export const useIndexPattern = (): IndexPattern | undefined => {
return indexPattern;
};
+
+export const useIndexPatterns = () => {
+ const { indexPattern: indexId = '' } = useTypedSelector((state) => state.visualization);
+ const [indexPatterns, setIndexPatterns] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(undefined);
+ const {
+ services: { data },
+ } = useOpenSearchDashboards();
+
+ let foundSelected;
+ if (!loading && !error) {
+ foundSelected = indexPatterns.filter((p) => p.id === indexId)[0];
+ if (foundSelected === undefined) {
+ setError(
+ new Error("Attempted to select an index pattern that wasn't in the index pattern list")
+ );
+ }
+ }
+
+ useEffect(() => {
+ const handleUpdate = async () => {
+ try {
+ const ids = await data.indexPatterns.getIds(indexId);
+ const patterns = await Promise.all(ids.map((id) => data.indexPatterns.get(id)));
+ setIndexPatterns(patterns);
+ } catch (e) {
+ setError(e);
+ } finally {
+ setLoading(false);
+ }
+ };
+ handleUpdate();
+ // we want to run this hook exactly once, which you do by an empty dep array
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return {
+ indexPatterns,
+ error,
+ loading,
+ selected: foundSelected!,
+ };
+};
diff --git a/src/plugins/wizard/public/assets/index_pattern.svg b/src/plugins/wizard/public/assets/index_pattern.svg
new file mode 100644
index 000000000000..b1f140a38632
--- /dev/null
+++ b/src/plugins/wizard/public/assets/index_pattern.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/plugins/wizard/public/types.ts b/src/plugins/wizard/public/types.ts
index 14f13d56bdf3..529ebc58a60e 100644
--- a/src/plugins/wizard/public/types.ts
+++ b/src/plugins/wizard/public/types.ts
@@ -10,7 +10,7 @@ import { DashboardStart } from 'src/plugins/dashboard/public';
import { VisualizationsSetup } from 'src/plugins/visualizations/public';
import { ExpressionsStart } from 'src/plugins/expressions/public';
import { NavigationPublicPluginStart } from '../../navigation/public';
-import { DataPublicPluginStart } from '../../data/public';
+import { DataPublicPluginStart, IndexPatternField } from '../../data/public';
import { TypeServiceSetup, TypeServiceStart } from './services/type_service';
export type WizardSetup = TypeServiceSetup;
diff --git a/src/plugins/wizard/tsconfig.json b/src/plugins/wizard/tsconfig.json
new file mode 100644
index 000000000000..97ca7d718328
--- /dev/null
+++ b/src/plugins/wizard/tsconfig.json
@@ -0,0 +1,74 @@
+{
+ "compilerOptions": {
+ "baseUrl": "/home/ec2-user/osd",
+ "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" }
+ ]
+}