From 09de04817ec8e7d8ed1541a4d1d4f84f46b165af Mon Sep 17 00:00:00 2001 From: Jackie Han Date: Fri, 19 May 2023 15:03:55 -0700 Subject: [PATCH] Register AD as dashboard context menu option (#482) * Register AD as dashboard context menu option Signed-off-by: Jackie Han * addressing comments Signed-off-by: Jackie Han * add getActions props Signed-off-by: Jackie Han * add EmbeddableStart Signed-off-by: Jackie Han * remove spread operator Signed-off-by: Jackie Han * clenaup Signed-off-by: Jackie Han * add overlay getter and setter Signed-off-by: Jackie Han --------- Signed-off-by: Jackie Han --- opensearch_dashboards.json | 9 +- public/action/ad_dashboard_action.tsx | 78 +++++++++++++ .../AnywhereParentFlyout.tsx | 37 ++++++ .../AnywhereParentFlyout/index.tsx | 8 ++ .../containers/DocumentationTitle.tsx | 28 +++++ .../DocumentationTitle/index.tsx | 8 ++ public/plugin.ts | 109 ++++++++++-------- public/services.ts | 9 +- public/utils/constants.ts | 4 + public/utils/contextMenu/getActions.tsx | 84 ++++++++++++++ 10 files changed, 325 insertions(+), 49 deletions(-) create mode 100644 public/action/ad_dashboard_action.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx create mode 100644 public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx create mode 100644 public/utils/contextMenu/getActions.tsx diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 8fdba21c..21cd2fbb 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -7,7 +7,14 @@ "opensearchDashboardsUtils", "expressions", "data", - "visAugmenter" + "visAugmenter", + "uiActions", + "dashboard", + "embeddable", + "opensearchDashboardsReact", + "savedObjects", + "visAugmenter", + "opensearchDashboardsUtils" ], "server": true, "ui": true diff --git a/public/action/ad_dashboard_action.tsx b/public/action/ad_dashboard_action.tsx new file mode 100644 index 00000000..0be356ed --- /dev/null +++ b/public/action/ad_dashboard_action.tsx @@ -0,0 +1,78 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { IEmbeddable } from '../../../../src/plugins/dashboard/public/embeddable_plugin'; +import { + DASHBOARD_CONTAINER_TYPE, + DashboardContainer, +} from '../../../../src/plugins/dashboard/public'; +import { + IncompatibleActionError, + createAction, + Action, +} from '../../../../src/plugins/ui_actions/public'; +import { isReferenceOrValueEmbeddable } from '../../../../src/plugins/embeddable/public'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; + +export const ACTION_AD = 'ad'; + +function isDashboard( + embeddable: IEmbeddable +): embeddable is DashboardContainer { + return embeddable.type === DASHBOARD_CONTAINER_TYPE; +} + +export interface ActionContext { + embeddable: IEmbeddable; +} + +export interface CreateOptions { + grouping: Action['grouping']; + title: string; + icon: EuiIconType; + id: string; + order: number; + onClick: Function; +} + +export const createADAction = ({ + grouping, + title, + icon, + id, + order, + onClick, +}: CreateOptions) => + createAction({ + id, + order, + getDisplayName: ({ embeddable }: ActionContext) => { + if (!embeddable.parent || !isDashboard(embeddable.parent)) { + throw new IncompatibleActionError(); + } + return title; + }, + getIconType: () => icon, + type: ACTION_AD, + grouping, + isCompatible: async ({ embeddable }: ActionContext) => { + const paramsType = embeddable.vis?.params?.type; + const seriesParams = embeddable.vis?.params?.seriesParams || []; + const series = embeddable.vis?.params?.series || []; + const isLineGraph = + seriesParams.find((item) => item.type === 'line') || + series.find((item) => item.chart_type === 'line'); + const isValidVis = isLineGraph && paramsType !== 'table'; + return Boolean( + embeddable.parent && isDashboard(embeddable.parent) && isValidVis + ); + }, + execute: async ({ embeddable }: ActionContext) => { + if (!isReferenceOrValueEmbeddable(embeddable)) { + throw new IncompatibleActionError(); + } + + onClick({ embeddable }); + }, + }); \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx new file mode 100644 index 00000000..2a54a169 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/AnywhereParentFlyout.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import React, { useState } from 'react'; +import { get } from 'lodash'; +import AddAnomalyDetector from '../CreateAnomalyDetector'; +import { getEmbeddable } from '../../../../public/services'; + +const AnywhereParentFlyout = ({ startingFlyout, ...props }) => { + const embeddable = getEmbeddable().getEmbeddableFactory; + const indices: { label: string }[] = [ + { label: get(embeddable, 'vis.data.indexPattern.title', '') }, + ]; + + const [mode, setMode] = useState(startingFlyout); + const [selectedDetectorId, setSelectedDetectorId] = useState(); + + const AnywhereFlyout = { + create: AddAnomalyDetector, + }[mode]; + + return ( + + ); +}; + +export default AnywhereParentFlyout; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx new file mode 100644 index 00000000..cca0078b --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/AnywhereParentFlyout/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import AnywhereParentFlyout from './AnywhereParentFlyout'; + +export default AnywhereParentFlyout; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx new file mode 100644 index 00000000..22d2ac3c --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle.tsx @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiIcon, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +const DocumentationTitle = () => ( + + + + {i18n.translate( + 'dashboard.actions.adMenuItem.documentation.displayName', + { + defaultMessage: 'Documentation', + } + )} + + + + + + +); + +export default DocumentationTitle; \ No newline at end of file diff --git a/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx new file mode 100644 index 00000000..03b2fb80 --- /dev/null +++ b/public/components/FeatureAnywhereContextMenu/DocumentationTitle/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import DocumentationTitle from './containers/DocumentationTitle'; + +export default DocumentationTitle; \ No newline at end of file diff --git a/public/plugin.ts b/public/plugin.ts index 2f55a028..36e15cc3 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -14,58 +14,73 @@ import { CoreSetup, CoreStart, Plugin, - PluginInitializerContext, } from '../../../src/core/public'; -import { - AnomalyDetectionOpenSearchDashboardsPluginSetup, - AnomalyDetectionOpenSearchDashboardsPluginStart, -} from '.'; +import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { ACTION_AD } from './action/ad_dashboard_action'; +import { PLUGIN_NAME } from './utils/constants'; +import { getActions } from './utils/contextMenu/getActions'; import { overlayAnomaliesFunction } from './expressions/overlay_anomalies'; -import { setClient } from './services'; +import { setClient, setEmbeddable, setOverlays } from './services'; +import { AnomalyDetectionOpenSearchDashboardsPluginStart } from 'public'; +import { createStartServicesGetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; -export class AnomalyDetectionOpenSearchDashboardsPlugin - implements - Plugin< - AnomalyDetectionOpenSearchDashboardsPluginSetup, - AnomalyDetectionOpenSearchDashboardsPluginStart - > -{ - constructor(private readonly initializerContext: PluginInitializerContext) { - // can retrieve config from initializerContext +declare module '../../../src/plugins/ui_actions/public' { + export interface ActionContextMapping { + [ACTION_AD]: {}; } +} - public setup( - core: CoreSetup, - plugins - ): AnomalyDetectionOpenSearchDashboardsPluginSetup { - core.application.register({ - id: 'anomaly-detection-dashboards', - title: 'Anomaly Detection', - category: { - id: 'opensearch', - label: 'OpenSearch Plugins', - order: 2000, - }, - order: 5000, - mount: async (params: AppMountParameters) => { - const { renderApp } = await import('./anomaly_detection_app'); - const [coreStart, depsStart] = await core.getStartServices(); - return renderApp(coreStart, params); - }, - }); +export interface AnomalyDetectionSetupDeps { + embeddable: EmbeddableSetup; +} - // Set the HTTP client so it can be pulled into expression fns to make - // direct server-side calls - setClient(core.http); +export interface AnomalyDetectionStartDeps { + embeddable: EmbeddableStart; +} - // registers the expression function used to render anomalies on an Augmented Visualization - plugins.expressions.registerFunction(overlayAnomaliesFunction); - return {}; - } +export class AnomalyDetectionOpenSearchDashboardsPlugin implements + Plugin { + + public setup(core: CoreSetup, plugins: any) { + core.application.register({ + id: PLUGIN_NAME, + title: 'Anomaly Detection', + category: { + id: 'opensearch', + label: 'OpenSearch Plugins', + order: 2000, + }, + order: 5000, + mount: async (params: AppMountParameters) => { + const { renderApp } = await import('./anomaly_detection_app'); + const [coreStart] = await core.getStartServices(); + return renderApp(coreStart, params); + }, + }); - public start( - core: CoreStart - ): AnomalyDetectionOpenSearchDashboardsPluginStart { - return {}; - } -} + // Set the HTTP client so it can be pulled into expression fns to make + // direct server-side calls + setClient(core.http); + + // Create context menu actions. Pass core, to access service for flyouts. + const actions = getActions(); + + // Add actions to uiActions + actions.forEach((action) => { + plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); + }); + + // registers the expression function used to render anomalies on an Augmented Visualization + plugins.expressions.registerFunction(overlayAnomaliesFunction); + return {}; + } + + public start( + core: CoreStart, + {embeddable }: AnomalyDetectionStartDeps + ): AnomalyDetectionOpenSearchDashboardsPluginStart { + setEmbeddable(embeddable); + setOverlays(core.overlays); + return {}; + } +} \ No newline at end of file diff --git a/public/services.ts b/public/services.ts index d9161693..3857a95f 100644 --- a/public/services.ts +++ b/public/services.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CoreStart } from '../../../src/core/public'; +import { CoreStart, OverlayStart } from '../../../src/core/public'; +import { EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { createGetterSetter } from '../../../src/plugins/opensearch_dashboards_utils/public'; import { SavedObjectLoader } from '../../../src/plugins/saved_objects/public'; @@ -12,3 +13,9 @@ export const [getSavedFeatureAnywhereLoader, setSavedFeatureAnywhereLoader] = export const [getClient, setClient] = createGetterSetter('http'); + +export const [getEmbeddable, setEmbeddable] = + createGetterSetter('Embeddable'); + +export const [getOverlays, setOverlays] = + createGetterSetter('Overlays'); diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 23354742..099e6a7e 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -53,6 +53,8 @@ export const ANOMALY_RESULT_INDEX = '.opendistro-anomaly-results'; export const BASE_DOCS_LINK = 'https://opensearch.org/docs/monitoring-plugins'; +export const AD_DOCS_LINK = 'https://opensearch.org/docs/latest/observing-your-data/ad/index/'; + export const MAX_DETECTORS = 1000; export const MAX_ANOMALIES = 10000; @@ -87,3 +89,5 @@ export enum MISSING_FEATURE_DATA_SEVERITY { } export const SPACE_STR = ' '; + +export const APM_TRACE = 'apmTrace'; diff --git a/public/utils/contextMenu/getActions.tsx b/public/utils/contextMenu/getActions.tsx new file mode 100644 index 00000000..4dcb05f6 --- /dev/null +++ b/public/utils/contextMenu/getActions.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { i18n } from '@osd/i18n'; +import { EuiIconType } from '@elastic/eui'; +import { toMountPoint } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { Action } from '../../../../../src/plugins/ui_actions/public'; +import { createADAction } from '../../action/ad_dashboard_action'; +import AnywhereParentFlyout from '../../components/FeatureAnywhereContextMenu/AnywhereParentFlyout'; +import { Provider } from 'react-redux'; +import configureStore from '../../redux/configureStore'; +import DocumentationTitle from '../../components/FeatureAnywhereContextMenu/DocumentationTitle/containers/DocumentationTitle'; +import { AD_DOCS_LINK, APM_TRACE } from '../constants'; +import { getClient, getOverlays } from '../../../public/services'; + +// This is used to create all actions in the same context menu +const grouping: Action['grouping'] = [ + { + id: 'ad-dashboard-context-menu', + getDisplayName: () => 'Anomaly Detector', + getIconType: () => APM_TRACE, + }, +]; + +export const getActions = () => { + const getOnClick = + (startingFlyout) => + async ({ embeddable }) => { + const overlayService = getOverlays(); + const openFlyout = overlayService.openFlyout; + const store = configureStore(getClient()); + const overlay = openFlyout( + toMountPoint( + + overlay.close()} + /> + + ), + { size: 'm', className: 'context-menu__flyout' } + ); + }; + + return [ + { + grouping, + id: 'createAnomalyDetector', + title: i18n.translate( + 'dashboard.actions.adMenuItem.createAnomalyDetector.displayName', + { + defaultMessage: 'Create anomaly detector', + } + ), + icon: 'plusInCircle' as EuiIconType, + order: 100, + onClick: getOnClick('create'), + }, + { + grouping, + id: 'associatedAnomalyDetector', + title: i18n.translate( + 'dashboard.actions.adMenuItem.associatedAnomalyDetector.displayName', + { + defaultMessage: 'Associated anomaly detector', + } + ), + icon: 'gear' as EuiIconType, + order: 99, + onClick: getOnClick('associated'), + }, + { + id: 'documentationAnomalyDetector', + title: , + icon: 'documentation' as EuiIconType, + order: 98, + onClick: () => { + window.open( + AD_DOCS_LINK, + '_blank' + ); + }, + }, + ].map((options) => createADAction({ ...options, grouping })); +}; \ No newline at end of file