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