diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx index 84302c54f51f..7d578ee77cda 100644 --- a/src/plugins/wizard/public/application/app.tsx +++ b/src/plugins/wizard/public/application/app.tsx @@ -3,28 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect } from 'react'; +import React from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { EuiPage } from '@elastic/eui'; -import { DataPublicPluginStart } from '../../../data/public'; import { SideNav } from './components/side_nav'; import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; -import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { WizardServices } from '../types'; import { Workspace } from './components/workspace'; import './app.scss'; import { TopNav } from './components/top_nav'; -import { useTypedDispatch } from './utils/state_management'; -import { setIndexPattern } from './utils/state_management/datasource_slice'; export const WizardApp = () => { - const { - services: { data }, - } = useOpenSearchDashboards(); - - useIndexPattern(data); - // Render the application DOM. return ( @@ -38,18 +27,3 @@ export const WizardApp = () => { ); }; - -// TODO: Temporary. Need to update it fetch the index pattern cohesively -function useIndexPattern(data: DataPublicPluginStart) { - const dispatch = useTypedDispatch(); - - useEffect(() => { - const fetchIndexPattern = async () => { - const defaultIndexPattern = await data.indexPatterns.getDefault(); - if (defaultIndexPattern) { - dispatch(setIndexPattern(defaultIndexPattern)); - } - }; - fetchIndexPattern(); - }, [data.indexPatterns, dispatch]); -} diff --git a/src/plugins/wizard/public/application/components/workspace.tsx b/src/plugins/wizard/public/application/components/workspace.tsx index a83201be4ccd..a6550a58fb80 100644 --- a/src/plugins/wizard/public/application/components/workspace.tsx +++ b/src/plugins/wizard/public/application/components/workspace.tsx @@ -3,8 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import React, { FC } from 'react'; +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'; @@ -13,10 +27,7 @@ export const Workspace: FC = ({ children }) => {
- {/* TODO: This is the temporary view of the selected chard, should be replaced by dropdown */} - - Bar - + @@ -35,3 +46,68 @@ export const Workspace: FC = ({ children }) => {
); }; + +const TypeSelectorPopover = () => { + const [isPopoverOpen, setPopover] = useState(false); + const { activeVisualization: activeVisualizationId } = useTypedSelector( + (state) => state.visualization + ); + const { + services: { types }, + } = useOpenSearchDashboards(); + const dispatch = useTypedDispatch(); + + // TODO: Error if no active visualization + const activeVisualization = types.get(activeVisualizationId || ''); + const visualizationTypes = types.all(); + + const 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 index b48044e8b2e8..c451d082b153 100644 --- a/src/plugins/wizard/public/application/index.tsx +++ b/src/plugins/wizard/public/application/index.tsx @@ -7,15 +7,16 @@ 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'; -import { store } from './utils/state_management'; export const renderApp = ( { appBasePath, element }: AppMountParameters, - services: WizardServices + services: WizardServices, + store: Store ) => { ReactDOM.render( 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 index a84f1a73ca04..d51d463d68ee 100644 --- a/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts +++ b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts @@ -5,6 +5,7 @@ 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'; @@ -22,6 +23,18 @@ const initialState: DataSourceState = { 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, 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 index c3c94fa673fe..4fa56c1a7c97 100644 --- a/src/plugins/wizard/public/application/utils/state_management/store.ts +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -3,17 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { configureStore } from '@reduxjs/toolkit'; +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'; -export const store = configureStore({ - reducer: { - dataSource: dataSourceReducer, - config: configReducer, - }, +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; -export type AppDispatch = typeof store.dispatch; +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/plugin.ts b/src/plugins/wizard/public/plugin.ts index 139c40708586..5b309080a872 100644 --- a/src/plugins/wizard/public/plugin.ts +++ b/src/plugins/wizard/public/plugin.ts @@ -16,17 +16,24 @@ 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 { + 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', @@ -39,6 +46,9 @@ export class WizardPlugin 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, @@ -46,16 +56,20 @@ export class WizardPlugin 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); + return renderApp(params, services, store); }, }); @@ -72,6 +86,10 @@ export class WizardPlugin aliasApp: 'wizard', aliasPath: '#/', }); + + return { + ...typeService.setup(), + }; } public start(core: CoreStart) {} 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..bdc2a421c0ad --- /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. + */ + +/* + * 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. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +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 index 6c8c08547bbb..07b1e5141c61 100644 --- a/src/plugins/wizard/public/types.ts +++ b/src/plugins/wizard/public/types.ts @@ -10,6 +10,9 @@ 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; @@ -28,4 +31,5 @@ export interface WizardServices extends CoreStart { 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/public/visualizations/xy_chart.tsx b/src/plugins/wizard/public/visualizations/xy_chart.tsx deleted file mode 100644 index 0215b31b2b42..000000000000 --- a/src/plugins/wizard/public/visualizations/xy_chart.tsx +++ /dev/null @@ -1,6 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// TODO: This is where we register the visualizations using a registration service provided by the plugin