From 6adf60c03b4869c8e8aeb733ec12489c5f337385 Mon Sep 17 00:00:00 2001 From: Joshua Li Date: Mon, 17 Apr 2023 11:41:21 -0700 Subject: [PATCH] [2.x] Refactor Saved objects and add visualization embeddable (#341) (#352) Signed-off-by: Joshua Li Co-authored-by: Eric Wei Co-authored-by: opensearch-trigger-bot[bot] <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Co-authored-by: Derek Ho (cherry picked from commit 6047bff9e3575edc75398640e845cab73bd6c32d) --- auto_sync_commit_metadata.json | 4 +- common/constants/explorer.ts | 12 +- common/types/explorer.ts | 29 +- .../observability_saved_object_attributes.ts | 19 + common/utils/core_services.ts | 41 + common/utils/index.ts | 3 +- common/utils/query_utils.ts | 11 +- common/utils/settings_service.ts | 25 - opensearch_dashboards.json | 10 +- package.json | 7 +- .../__snapshots__/log_config.test.tsx.snap | 104 +- .../service_config.test.tsx.snap | 96 +- .../__snapshots__/trace_config.test.tsx.snap | 96 +- .../application_analytics/helpers/utils.tsx | 42 +- .../live_tail_button.test.tsx.snap | 12 +- public/components/common/search/search.scss | 8 +- .../custom_panel_table.test.tsx.snap | 8 +- .../custom_panel_view.test.tsx.snap | 1042 +++++++++++++-- .../__snapshots__/utils.test.tsx.snap | 36 +- .../helpers/__tests__/utils.test.tsx | 1 + .../custom_panels/helpers/utils.tsx | 63 +- .../__snapshots__/empty_panel.test.tsx.snap | 16 +- .../__snapshots__/panel_grid.test.tsx.snap | 197 ++- .../visualization_container.test.tsx.snap | 4 +- .../visualization_flyout.test.tsx.snap | 16 +- .../visualization_flyout.tsx | 31 +- .../__snapshots__/no_results.test.tsx.snap | 4 +- .../event_analytics/explorer/explorer.tsx | 1115 ++++++----------- .../explorer/log_explorer.scss | 6 +- .../event_analytics/explorer/log_explorer.tsx | 46 +- .../patterns_header.test.tsx.snap | 4 +- .../__snapshots__/save_panel.test.tsx.snap | 4 +- .../__snapshots__/field.test.tsx.snap | 4 +- .../__snapshots__/sidebar.test.tsx.snap | 112 +- .../explorer/sidebar/sidebar.scss | 12 - .../__snapshots__/config_panel.test.tsx.snap | 76 +- .../count_distribution.test.tsx.snap | 6 +- .../shared_components.test.tsx.snap | 12 +- .../components/event_analytics/home/home.tsx | 86 +- .../home/saved_objects_table.tsx | 43 +- .../event_analytics/hooks/use_fetch_events.ts | 19 +- public/components/event_analytics/index.tsx | 46 +- .../event_analytics/utils/utils.tsx | 42 +- .../metrics/redux/slices/metrics_slice.ts | 20 +- .../__snapshots__/searchbar.test.tsx.snap | 8 +- .../__snapshots__/sidebar.test.tsx.snap | 16 +- .../metrics_export_panel.test.tsx.snap | 4 +- .../__snapshots__/top_menu.test.tsx.snap | 24 +- .../__snapshots__/empty_view.test.tsx.snap | 4 +- .../__snapshots__/metrics_grid.test.tsx.snap | 209 ++- .../paragraph_components/paragraphs.tsx | 66 +- .../__snapshots__/dashboard.test.tsx.snap | 264 ++++ .../__snapshots__/services.test.tsx.snap | 264 ++++ .../__snapshots__/traces.test.tsx.snap | 264 ++++ .../__snapshots__/assets.test.tsx.snap | 12 +- .../__tests__/__snapshots__/bar.test.tsx.snap | 4 +- .../__snapshots__/gauge.test.tsx.snap | 8 +- .../__snapshots__/heatmap.test.tsx.snap | 4 +- .../__snapshots__/histogram.test.tsx.snap | 4 +- .../horizontal_bar.test.tsx.snap | 4 +- .../__snapshots__/line.test.tsx.snap | 4 +- .../__snapshots__/metrics.test.tsx.snap | 8 +- .../__tests__/__snapshots__/pie.test.tsx.snap | 4 +- .../__snapshots__/text.test.tsx.snap | 4 +- .../__snapshots__/treemap.test.tsx.snap | 4 +- .../saved_object_visualization.tsx | 103 ++ public/embeddable/filters/filter_parser.ts | 53 + .../embeddable/observability_embeddable.tsx | 95 ++ .../observability_embeddable_component.tsx | 41 + .../observability_embeddable_factory.tsx | 144 +++ public/plugin.ts | 92 +- .../services/data_fetchers/fetch_interface.ts | 8 + public/services/data_fetchers/fetcher_base.ts | 11 + .../data_fetchers/ppl/ppl_data_fetcher.ts | 208 +++ .../event_analytics/saved_objects.ts | 64 +- .../saved_object_client/client_base.ts | 17 + .../saved_object_client/client_factory.ts | 37 + .../saved_object_client/client_interface.ts | 16 + .../osd_saved_object_client.ts | 130 ++ .../osd_saved_objects/saved_visualization.ts | 188 +++ .../osd_saved_objects/types.ts | 20 + .../saved_object_client/ppl/index.ts | 9 + .../saved_object_client/ppl/panels.ts | 41 + .../saved_object_client/ppl/ppl_client.ts | 139 ++ .../saved_object_client/ppl/saved_query.ts | 73 ++ .../ppl/saved_visualization.ts | 92 ++ .../saved_objects_actions.ts | 124 ++ .../saved_object_client/types.ts | 57 + .../saved_object_loaders/loader_base.ts | 11 + .../saved_object_loaders/loader_interface.ts | 8 + .../saved_object_loaders/ppl/index.ts | 4 + .../saved_object_loaders/ppl/ppl_loader.ts | 242 ++++ .../saved_object_savers/index.ts | 12 + .../ppl/save_as_current_vis.ts | 67 + .../ppl/save_as_new_query.ts | 66 + .../ppl/save_as_new_vis.ts | 97 ++ .../ppl/save_current_query.ts | 42 + .../ppl/saved_query_saver.ts | 13 + .../saved_object_savers/saver_base.ts | 23 + .../saved_object_savers/saver_interface.ts | 8 + public/types.ts | 16 + public/variables.scss | 9 + server/plugin.ts | 13 +- .../event_analytics/event_analytics_router.ts | 27 +- .../observability_saved_object.ts | 43 + server/services/facets/saved_objects.ts | 11 +- test/__mocks__/coreMocks.ts | 22 +- test/jest.config.js | 7 +- test/setup.jest.ts | 22 +- yarn.lock | 146 ++- 110 files changed, 5976 insertions(+), 1468 deletions(-) create mode 100644 common/types/observability_saved_object_attributes.ts create mode 100644 common/utils/core_services.ts delete mode 100644 common/utils/settings_service.ts create mode 100644 public/components/visualizations/saved_object_visualization.tsx create mode 100644 public/embeddable/filters/filter_parser.ts create mode 100644 public/embeddable/observability_embeddable.tsx create mode 100644 public/embeddable/observability_embeddable_component.tsx create mode 100644 public/embeddable/observability_embeddable_factory.tsx create mode 100644 public/services/data_fetchers/fetch_interface.ts create mode 100644 public/services/data_fetchers/fetcher_base.ts create mode 100644 public/services/data_fetchers/ppl/ppl_data_fetcher.ts create mode 100644 public/services/saved_objects/saved_object_client/client_base.ts create mode 100644 public/services/saved_objects/saved_object_client/client_factory.ts create mode 100644 public/services/saved_objects/saved_object_client/client_interface.ts create mode 100644 public/services/saved_objects/saved_object_client/osd_saved_objects/osd_saved_object_client.ts create mode 100644 public/services/saved_objects/saved_object_client/osd_saved_objects/saved_visualization.ts create mode 100644 public/services/saved_objects/saved_object_client/osd_saved_objects/types.ts create mode 100644 public/services/saved_objects/saved_object_client/ppl/index.ts create mode 100644 public/services/saved_objects/saved_object_client/ppl/panels.ts create mode 100644 public/services/saved_objects/saved_object_client/ppl/ppl_client.ts create mode 100644 public/services/saved_objects/saved_object_client/ppl/saved_query.ts create mode 100644 public/services/saved_objects/saved_object_client/ppl/saved_visualization.ts create mode 100644 public/services/saved_objects/saved_object_client/saved_objects_actions.ts create mode 100644 public/services/saved_objects/saved_object_client/types.ts create mode 100644 public/services/saved_objects/saved_object_loaders/loader_base.ts create mode 100644 public/services/saved_objects/saved_object_loaders/loader_interface.ts create mode 100644 public/services/saved_objects/saved_object_loaders/ppl/index.ts create mode 100644 public/services/saved_objects/saved_object_loaders/ppl/ppl_loader.ts create mode 100644 public/services/saved_objects/saved_object_savers/index.ts create mode 100644 public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts create mode 100644 public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts create mode 100644 public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts create mode 100644 public/services/saved_objects/saved_object_savers/ppl/save_current_query.ts create mode 100644 public/services/saved_objects/saved_object_savers/ppl/saved_query_saver.ts create mode 100644 public/services/saved_objects/saved_object_savers/saver_base.ts create mode 100644 public/services/saved_objects/saved_object_savers/saver_interface.ts create mode 100644 server/saved_objects/observability_saved_object.ts diff --git a/auto_sync_commit_metadata.json b/auto_sync_commit_metadata.json index 051450e71..2aeed56b4 100644 --- a/auto_sync_commit_metadata.json +++ b/auto_sync_commit_metadata.json @@ -1,4 +1,4 @@ { - "last_github_commit": "4ddf822d3450cb5ce1d9fd3671670c84a0acd6cc", - "last_gitfarm_commit": "b095641c9270cae3d835a525f593e5cb6b21608b" + "last_github_commit": "6047bff9e3575edc75398640e845cab73bd6c32d", + "last_gitfarm_commit": "3dba7edb78f37032252e40aa1e005587ffc8bf96" } \ No newline at end of file diff --git a/common/constants/explorer.ts b/common/constants/explorer.ts index a26677c89..f586f5fca 100644 --- a/common/constants/explorer.ts +++ b/common/constants/explorer.ts @@ -4,8 +4,8 @@ */ import { htmlIdGenerator } from '@elastic/eui'; -import { VIS_CHART_TYPES } from './shared'; import { ThresholdUnitType } from '../../public/components/event_analytics/explorer/visualizations/config_panel/config_panes/config_controls/config_thresholds'; +import { VIS_CHART_TYPES } from './shared'; export const EVENT_ANALYTICS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugin/event-analytics/'; @@ -31,6 +31,11 @@ export const TAB_EVENT_ID_TXT_PFX = 'main-content-events-'; export const TAB_CHART_ID_TXT_PFX = 'main-content-vis-'; export const TAB_EVENT_ID = 'main-content-events'; export const TAB_CHART_ID = 'main-content-vis'; +export const CREATE_TAB_PARAM_KEY = 'create'; +export const CREATE_TAB_PARAM = { + [TAB_EVENT_ID]: 'events', + [TAB_CHART_ID]: 'visualizations', +} as const; export const HAS_SAVED_TIMESTAMP = 'hasSavedTimestamp'; export const FILTER_OPTIONS = ['Visualization', 'Query', 'Metric']; export const SAVED_QUERY = 'savedQuery'; @@ -309,3 +314,8 @@ export const sampleLogPatternData = { "Mozilla/5.0 (X11; Linux x86_64; rv:6.0a1) Gecko/20110421 Firefox/6.0a1"', anomalyCount: 0, }; + +export const TYPE_TAB_MAPPING = { + [SAVED_QUERY]: TAB_EVENT_ID, + [SAVED_VISUALIZATION]: TAB_CHART_ID, +}; diff --git a/common/types/explorer.ts b/common/types/explorer.ts index d32fe5c3a..53dfe2ca2 100644 --- a/common/types/explorer.ts +++ b/common/types/explorer.ts @@ -33,14 +33,18 @@ import SavedObjects from '../../public/services/saved_objects/event_analytics/sa import TimestampUtils from '../../public/services/timestamp/timestamp'; import PPLService from '../../public/services/requests/ppl'; import DSLService from '../../public/services/requests/dsl'; -import { SavedObjectsStart } from '../../../../src/core/public/saved_objects'; +import { + SavedObjectAttributes, + SavedObjectsStart, +} from '../../../../src/core/public/saved_objects'; + export interface IQueryTab { id: string; name: React.ReactNode | string; content: React.ReactNode; } -export interface IField { +export interface IField extends SavedObjectAttributes { name: string; type: string; label?: string; @@ -151,7 +155,7 @@ export interface SavedQuery { selected_timestamp: IField; } -export interface SavedVisualization { +export interface SavedVisualization extends SavedObjectAttributes { description: string; name: string; query: string; @@ -159,25 +163,12 @@ export interface SavedVisualization { selected_fields: { text: string; tokens: [] }; selected_timestamp: IField; type: string; + sub_type?: 'metric' | 'visualization'; // exists if sub type is metric + user_configs?: string; + units_of_measure?: string; application_id?: string; } -export interface SavedQueryRes { - createdTimeMs: number; - lastUpdatedTimeMs: number; - objectId: string; - savedQuery: SavedQuery; - tenant: string; -} - -export interface SavedVizRes { - createdTimeMs: number; - lastUpdatedTimeMs: number; - objectId: string; - savedVisualization: SavedVisualization; - tenant: string; -} - export interface ExplorerDataType { jsonData: object[]; jsonDataAll: object[]; diff --git a/common/types/observability_saved_object_attributes.ts b/common/types/observability_saved_object_attributes.ts new file mode 100644 index 000000000..520f922bc --- /dev/null +++ b/common/types/observability_saved_object_attributes.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from '../../../../src/core/types'; +import { SavedVisualization } from './explorer'; + +export const VISUALIZATION_SAVED_OBJECT = 'observability-visualization'; +export const OBSERVABILTY_SAVED_OBJECTS = [VISUALIZATION_SAVED_OBJECT] as const; +export const SAVED_OBJECT_VERSION = 1; + +export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes { + title: string; + description: string; + version: number; + createdTimeMs: number; + savedVisualization: SavedVisualization; +} diff --git a/common/utils/core_services.ts b/common/utils/core_services.ts new file mode 100644 index 000000000..3b772d225 --- /dev/null +++ b/common/utils/core_services.ts @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + HttpStart, + IUiSettingsClient, + NotificationsStart, + SavedObjectsClientContract, + ToastInput, +} from '../../../../src/core/public'; +import { createGetterSetter } from '../../../../src/plugins/opensearch_dashboards_utils/common'; +import PPLService from '../../public/services/requests/ppl'; +import { QueryManager } from '../query_manager'; + +let uiSettings: IUiSettingsClient; +let notifications: NotificationsStart; + +export const uiSettingsService = { + init: (client: IUiSettingsClient, notificationsStart: NotificationsStart) => { + uiSettings = client; + notifications = notificationsStart; + }, + get: (key: string, defaultOverride?: any) => { + return uiSettings?.get(key, defaultOverride) || ''; + }, + set: (key: string, value: any) => { + return uiSettings?.set(key, value) || Promise.reject('uiSettings client not initialized.'); + }, + addToast: (toast: ToastInput) => { + return notifications.toasts.add(toast); + }, +}; + +export const [getPPLService, setPPLService] = createGetterSetter('PPLService'); +export const [getOSDHttp, setOSDHttp] = createGetterSetter('http'); +export const [getOSDSavedObjectsClient, setOSDSavedObjectsClient] = createGetterSetter< + SavedObjectsClientContract +>('SavedObjectsClient'); +export const [getQueryManager, setQueryManager] = createGetterSetter('QueryManager'); diff --git a/common/utils/index.ts b/common/utils/index.ts index 8ec98b14f..91caccf22 100644 --- a/common/utils/index.ts +++ b/common/utils/index.ts @@ -10,4 +10,5 @@ export { composeFinalQuery, removeBacktick, } from './query_utils'; -export { uiSettingsService } from './settings_service'; + +export * from './core_services'; diff --git a/common/utils/query_utils.ts b/common/utils/query_utils.ts index eec15aa2d..81853112c 100644 --- a/common/utils/query_utils.ts +++ b/common/utils/query_utils.ts @@ -13,7 +13,6 @@ import { PPL_INDEX_INSERT_POINT_REGEX, PPL_INDEX_REGEX, PPL_NEWLINE_REGEX, - PPL_STATS_REGEX, } from '../../common/constants/shared'; /** @@ -42,6 +41,7 @@ export const preprocessQuery = ({ selectedPatternField, patternRegex, filteredPattern, + whereClause, }: { rawQuery: string; startTime: string; @@ -51,6 +51,7 @@ export const preprocessQuery = ({ selectedPatternField?: string; patternRegex?: string; filteredPattern?: string; + whereClause?: string; }) => { let finalQuery = ''; @@ -65,7 +66,13 @@ export const preprocessQuery = ({ finalQuery = `${tokens![1]}=${ tokens![2] - } | where ${timeField} >= '${start}' and ${timeField} <= '${end}'${tokens![3]}`; + } | where ${timeField} >= '${start}' and ${timeField} <= '${end}'`; + + if (whereClause) { + finalQuery += ` AND ${whereClause}`; + } + + finalQuery += tokens![3]; if (isLiveQuery) { finalQuery = finalQuery + ` | sort - ${timeField}`; diff --git a/common/utils/settings_service.ts b/common/utils/settings_service.ts deleted file mode 100644 index f22912127..000000000 --- a/common/utils/settings_service.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { IUiSettingsClient, NotificationsStart, ToastInput } from '../../../../src/core/public'; - -let uiSettings: IUiSettingsClient; -let notifications: NotificationsStart; - -export const uiSettingsService = { - init: (client: IUiSettingsClient, notificationsStart: NotificationsStart) => { - uiSettings = client; - notifications = notificationsStart; - }, - get: (key: string, defaultOverride?: any) => { - return uiSettings?.get(key, defaultOverride) || ''; - }, - set: (key: string, value: any) => { - return uiSettings?.set(key, value) || Promise.reject("uiSettings client not initialized."); - }, - addToast: (toast: ToastInput) => { - return notifications.toasts.add(toast); - } -}; \ No newline at end of file diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 01c0df7b5..3e115a18b 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -6,14 +6,16 @@ "ui": true, "requiredPlugins": [ "charts", + "dashboard", "data", "embeddable", "inspector", - "urlForwarding", "navigation", + "opensearchDashboardsReact", + "opensearchDashboardsUtils", + "savedObjects", "uiActions", - "dashboard", - "visualizations", - "opensearchDashboardsReact" + "urlForwarding", + "visualizations" ] } diff --git a/package.json b/package.json index 28e43c7a0..96c689079 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,13 @@ "ag-grid-react": "^27.3.0", "antlr4": "4.8.0", "antlr4ts": "^0.5.0-alpha.4", + "performance-now": "^2.1.0", "plotly.js-dist": "^2.2.0", "postinstall": "^0.7.4", "react-graph-vis": "^1.0.5", "react-paginate": "^8.1.3", "react-plotly.js": "^2.5.1", - "redux-persist": "^6.0.0", - "performance-now": "^2.1.0" + "redux-persist": "^6.0.0" }, "devDependencies": { "@cypress/skip-test": "^2.6.1", @@ -37,7 +37,8 @@ "antlr4ts-cli": "^0.5.0-alpha.4", "cypress": "^6.0.0", "eslint": "^6.8.0", - "jest-dom": "^4.0.0" + "jest-dom": "^4.0.0", + "ts-jest": "^29.1.0" }, "resolutions": { "react-syntax-highlighter": "^15.4.3", diff --git a/public/components/application_analytics/__tests__/__snapshots__/log_config.test.tsx.snap b/public/components/application_analytics/__tests__/__snapshots__/log_config.test.tsx.snap index 18c3c8847..22fb59295 100644 --- a/public/components/application_analytics/__tests__/__snapshots__/log_config.test.tsx.snap +++ b/public/components/application_analytics/__tests__/__snapshots__/log_config.test.tsx.snap @@ -1,12 +1,56 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Log Config component renders empty log config 1`] = ` - - - + - + `; exports[`Log Config component renders with query 1`] = ` - - - + - + `; diff --git a/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap b/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap index 8b128989e..f6bc62f39 100644 --- a/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap +++ b/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap @@ -1,12 +1,56 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Service Config component renders empty service config 1`] = ` - - + `; exports[`Service Config component renders with one service selected 1`] = ` - - + `; diff --git a/public/components/application_analytics/__tests__/__snapshots__/trace_config.test.tsx.snap b/public/components/application_analytics/__tests__/__snapshots__/trace_config.test.tsx.snap index ef6b391ee..b0856bbf7 100644 --- a/public/components/application_analytics/__tests__/__snapshots__/trace_config.test.tsx.snap +++ b/public/components/application_analytics/__tests__/__snapshots__/trace_config.test.tsx.snap @@ -1,11 +1,55 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Trace Config component renders empty trace config 1`] = ` - - + `; exports[`Trace Config component renders with one trace selected 1`] = ` - - + `; diff --git a/public/components/application_analytics/helpers/utils.tsx b/public/components/application_analytics/helpers/utils.tsx index a9df996b1..5bdbcf706 100644 --- a/public/components/application_analytics/helpers/utils.tsx +++ b/public/components/application_analytics/helpers/utils.tsx @@ -2,45 +2,44 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ import { EuiDescriptionList, EuiSelectOption, EuiSpacer, EuiText } from '@elastic/eui'; import { ApplicationType, AvailabilityType } from 'common/types/application_analytics'; import { FilterType } from 'public/components/trace_analytics/components/common/filters/filters'; +import PPLService from 'public/services/requests/ppl'; import React, { Dispatch, ReactChild } from 'react'; import { batch } from 'react-redux'; -import PPLService from 'public/services/requests/ppl'; +import { HttpSetup } from '../../../../../../src/core/public'; +import { APP_ANALYTICS_API_PREFIX } from '../../../../common/constants/application_analytics'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; +import { NEW_SELECTED_QUERY_TAB, TAB_CREATED_TYPE } from '../../../../common/constants/explorer'; +import { SPAN_REGEX } from '../../../../common/constants/shared'; +import { VisualizationType } from '../../../../common/types/custom_panels'; import { IField } from '../../../../common/types/explorer'; import { preprocessQuery } from '../../../../common/utils/query_utils'; -import { SPAN_REGEX } from '../../../../common/constants/shared'; import { fetchVisualizationById } from '../../../components/custom_panels/helpers/utils'; -import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; -import { VisualizationType } from '../../../../common/types/custom_panels'; -import { NEW_SELECTED_QUERY_TAB, TAB_CREATED_TYPE } from '../../../../common/constants/explorer'; -import { APP_ANALYTICS_API_PREFIX } from '../../../../common/constants/application_analytics'; -import { HttpSetup } from '../../../../../../src/core/public'; import { init as initFields, remove as removefields, } from '../../event_analytics/redux/slices/field_slice'; import { - init as initVisualizationConfig, - reset as resetVisualizationConfig, -} from '../../event_analytics/redux/slices/viualization_config_slice'; -import { - init as initQuery, - remove as removeQuery, - changeQuery, -} from '../../event_analytics/redux/slices/query_slice'; + init as initPatterns, + remove as removePatterns, +} from '../../event_analytics/redux/slices/patterns_slice'; import { init as initQueryResult, remove as removeQueryResult, } from '../../event_analytics/redux/slices/query_result_slice'; +import { + changeQuery, + init as initQuery, + remove as removeQuery, +} from '../../event_analytics/redux/slices/query_slice'; import { addTab, removeTab } from '../../event_analytics/redux/slices/query_tab_slice'; import { - init as initPatterns, - remove as removePatterns, -} from '../../event_analytics/redux/slices/patterns_slice'; + init as initVisualizationConfig, + reset as resetVisualizationConfig, +} from '../../event_analytics/redux/slices/viualization_config_slice'; // Name validation export const isNameValid = (name: string, existingNames: string[]) => { @@ -219,13 +218,14 @@ export const calculateAvailability = async ( const visData = await fetchVisualizationById(http, visualizationId, (value: string) => console.error(value) ); + const userConfigs = visData.user_configs ? JSON.parse(visData.user_configs) : {}; // If there are levels, we get the current value - if (visData.user_configs.availabilityConfig?.hasOwnProperty('level')) { + if (userConfigs.availabilityConfig?.hasOwnProperty('level')) { // For every saved visualization with availability levels we push it to visWithAvailability // This is used to populate the options in configuration visWithAvailability.push({ value: visualizationId, text: visData.name }); - const levels = visData.user_configs.availabilityConfig.level.reverse(); + const levels = userConfigs.availabilityConfig.level.reverse(); let currValue = Number.MIN_VALUE; const finalQuery = preprocessQuery({ rawQuery: visData.query, diff --git a/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap b/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap index 78104e652..11e8fae85 100644 --- a/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap +++ b/public/components/common/live_tail/__tests__/__snapshots__/live_tail_button.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Live tail button change live tail to 10s interval 1`] = ` - - + `; exports[`Live tail button starts live tail with 5s interval 1`] = ` - - + `; exports[`Live tail off button stop live tail 1`] = ` - @@ -271,5 +271,5 @@ exports[`Live tail off button stop live tail 1`] = ` - + `; diff --git a/public/components/common/search/search.scss b/public/components/common/search/search.scss index 1b0666a6d..681a7f0b9 100644 --- a/public/components/common/search/search.scss +++ b/public/components/common/search/search.scss @@ -4,14 +4,12 @@ */ .globalQueryBar { - margin: .5rem 0; + margin: 0; padding: .5rem; } .aa-Autocomplete { width: 100%; position: relative; - margin: 4px; - margin-left: 8px; --aa-search-input-height: 38px; --aa-panel-border-color-rgb: rgba(227,230,238,255); --aa-input-background-color-rbg: rgba(250,251,253,255); @@ -72,8 +70,6 @@ .base-query-popover { border: unset; font-size: 17px; - position: absolute; - top: 20px; - left: 8px; + margin-top: 14px; background-color: transparent; } diff --git a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap index 334a93774..337b01306 100644 --- a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap +++ b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_table.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Panels Table Component renders empty panel table container 1`] = ` - - + `; exports[`Panels Table Component renders panel table container 1`] = ` - - + `; diff --git a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap index ab362302d..7b2eda6f2 100644 --- a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap +++ b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap @@ -1,47 +1,319 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Panels View Component renders panel view container with visualizations 1`] = ` - - @@ -501,7 +777,7 @@ exports[`Panels View Component renders panel view container with visualizations - + @@ -524,7 +800,7 @@ exports[`Panels View Component renders panel view container with visualizations
-
- + - @@ -1304,7 +1580,7 @@ exports[`Panels View Component renders panel view container with visualizations
- @@ -1423,7 +1699,7 @@ exports[`Panels View Component renders panel view container with visualizations
- +
@@ -1436,48 +1712,320 @@ exports[`Panels View Component renders panel view container with visualizations /> - - + - + @@ -1639,51 +2191,212 @@ exports[`Panels View Component renders panel view container with visualizations -
+ `; exports[`Panels View Component renders panel view container without visualizations 1`] = ` - - @@ -2129,7 +2846,7 @@ exports[`Panels View Component renders panel view container without visualizatio - + @@ -2152,7 +2869,7 @@ exports[`Panels View Component renders panel view container without visualizatio
-
- + - @@ -2918,7 +3635,7 @@ exports[`Panels View Component renders panel view container without visualizatio
- @@ -3032,7 +3749,7 @@ exports[`Panels View Component renders panel view container without visualizatio
- +
@@ -3045,48 +3762,209 @@ exports[`Panels View Component renders panel view container without visualizatio /> - - + - + @@ -3248,5 +4130,5 @@ exports[`Panels View Component renders panel view container without visualizatio -
+ `; diff --git a/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap b/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap index b8735f33b..2201cdec0 100644 --- a/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap +++ b/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap @@ -2,7 +2,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = `
- - - - - - + + +
`; exports[`Utils helper functions renders displayVisualization function 2`] = `
- - - - - - + + +
`; exports[`Utils helper functions renders displayVisualization function 3`] = `
- - - - - - + + +
`; diff --git a/public/components/custom_panels/helpers/__tests__/utils.test.tsx b/public/components/custom_panels/helpers/__tests__/utils.test.tsx index 55e6349e0..ba4c6b27b 100644 --- a/public/components/custom_panels/helpers/__tests__/utils.test.tsx +++ b/public/components/custom_panels/helpers/__tests__/utils.test.tsx @@ -104,6 +104,7 @@ describe('Utils helper functions', () => { const setToast = jest.fn(); expect(isPPLFilterValid(sampleSavedVisualization.visualization.query, setToast)).toBe(false); expect(isPPLFilterValid("where Carrier = 'OpenSearch-Air'", setToast)).toBe(true); + expect(isPPLFilterValid('', setToast)).toBe(true); }); it('renders displayVisualization function', () => { diff --git a/public/components/custom_panels/helpers/utils.tsx b/public/components/custom_panels/helpers/utils.tsx index e066622b7..4f0c44b4c 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -2,7 +2,6 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ import dateMath from '@elastic/datemath'; import { ShortDate } from '@elastic/eui'; @@ -11,24 +10,26 @@ import _ from 'lodash'; import { Moment } from 'moment-timezone'; import React from 'react'; import { Layout } from 'react-grid-layout'; +import { CoreStart } from '../../../../../../src/core/public'; import { PPL_DATE_FORMAT, PPL_INDEX_REGEX, PPL_WHERE_CLAUSE_REGEX, } from '../../../../common/constants/shared'; -import PPLService from '../../../services/requests/ppl'; -import { CoreStart } from '../../../../../../src/core/public'; -import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; +import { QueryManager } from '../../../../common/query_manager'; import { - VisualizationType, SavedVisualizationType, + VisualizationType, VizContainerError, } from '../../../../common/types/custom_panels'; -import { Visualization } from '../../visualizations/visualization'; +import { SavedVisualization } from '../../../../common/types/explorer'; +import { removeBacktick } from '../../../../common/utils'; import { getVizContainerProps } from '../../../components/visualizations/charts/helpers'; -import { QueryManager } from '../../../../common/query_manager'; +import PPLService from '../../../services/requests/ppl'; +import { SavedObjectsActions } from '../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { ObservabilitySavedVisualization } from '../../../services/saved_objects/saved_object_client/types'; import { getDefaultVisConfig } from '../../event_analytics/utils'; -import { removeBacktick } from '../../../../common/utils'; +import { Visualization } from '../../visualizations/visualization'; /* * "Utils" This file contains different reused functions in operational panels @@ -170,11 +171,17 @@ export const fetchVisualizationById = async ( savedVisualizationId: string, setIsError: (error: VizContainerError) => void ) => { - let savedVisualization = {} as SavedVisualizationType; - await http - .get(`${CUSTOM_PANELS_API_PREFIX}/visualizations/${savedVisualizationId}`) + let savedVisualization = {} as SavedVisualization; + + await SavedObjectsActions.get({ objectId: savedVisualizationId }) .then((res) => { - savedVisualization = res.visualization; + const visualization = (res.observabilityObjectList[0] as ObservabilitySavedVisualization) + .savedVisualization; + savedVisualization = { + ...visualization, + id: res.observabilityObjectList[0].objectId, + timeField: visualization.selected_timestamp.name, + }; }) .catch((err) => { setIsError({ @@ -382,6 +389,36 @@ export const onTimeChange = ( setRecentlyUsedRanges(recentlyUsedRangeObject.slice(0, 9)); }; +/** + * Convert an ObservabilitySavedVisualization into SavedVisualizationType, + * which is used in panels. + */ +export const parseSavedVisualizations = ( + visualization: ObservabilitySavedVisualization +): SavedVisualizationType => { + return { + id: visualization.objectId, + name: visualization.savedVisualization.name, + query: visualization.savedVisualization.query, + type: visualization.savedVisualization.type, + timeField: visualization.savedVisualization.selected_timestamp.name, + selected_date_range: visualization.savedVisualization.selected_date_range, + selected_fields: visualization.savedVisualization.selected_fields, + user_configs: visualization.savedVisualization.user_configs + ? JSON.parse(visualization.savedVisualization.user_configs) + : {}, + sub_type: visualization.savedVisualization.hasOwnProperty('sub_type') + ? visualization.savedVisualization.sub_type + : '', + units_of_measure: visualization.savedVisualization.hasOwnProperty('units_of_measure') + ? visualization.savedVisualization.units_of_measure + : '', + ...(visualization.savedVisualization.application_id + ? { application_id: visualization.savedVisualization.application_id } + : {}), + }; +}; + // Function to check date validity export const isDateValid = ( start: string | Moment | undefined, @@ -425,7 +462,7 @@ export const isPPLFilterValid = ( setToast('Please remove index from PPL Filter', 'danger', undefined); return false; } - if (!checkWhereClauseExists(query)) { + if (query && !checkWhereClauseExists(query)) { setToast('PPL filters should start with a where clause', 'danger', undefined); return false; } diff --git a/public/components/custom_panels/panel_modules/__tests__/__snapshots__/empty_panel.test.tsx.snap b/public/components/custom_panels/panel_modules/__tests__/__snapshots__/empty_panel.test.tsx.snap index 399245903..b9c1ad590 100644 --- a/public/components/custom_panels/panel_modules/__tests__/__snapshots__/empty_panel.test.tsx.snap +++ b/public/components/custom_panels/panel_modules/__tests__/__snapshots__/empty_panel.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Empty panel view component renders empty panel view with disabled popover 1`] = ` - @@ -79,7 +79,7 @@ exports[`Empty panel view component renders empty panel view with disabled popov
- @@ -193,7 +193,7 @@ exports[`Empty panel view component renders empty panel view with disabled popov
- + @@ -206,11 +206,11 @@ exports[`Empty panel view component renders empty panel view with disabled popov /> -
+ `; exports[`Empty panel view component renders empty panel view with enabled popover 1`] = ` - @@ -288,7 +288,7 @@ exports[`Empty panel view component renders empty panel view with enabled popove
- @@ -407,7 +407,7 @@ exports[`Empty panel view component renders empty panel view with enabled popove
- + @@ -420,5 +420,5 @@ exports[`Empty panel view component renders empty panel view with enabled popove /> -
+ `; diff --git a/public/components/custom_panels/panel_modules/panel_grid/__tests__/__snapshots__/panel_grid.test.tsx.snap b/public/components/custom_panels/panel_modules/panel_grid/__tests__/__snapshots__/panel_grid.test.tsx.snap index b192dff2d..04f722aaa 100644 --- a/public/components/custom_panels/panel_modules/panel_grid/__tests__/__snapshots__/panel_grid.test.tsx.snap +++ b/public/components/custom_panels/panel_modules/panel_grid/__tests__/__snapshots__/panel_grid.test.tsx.snap @@ -1,48 +1,213 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Panel Grid Component renders panel grid component with empty visualizations 1`] = ` - - + `; diff --git a/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap b/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap index 778d6e167..4ba3e3ec3 100644 --- a/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap +++ b/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Visualization Container Component renders add visualization container 1`] = ` - - + `; diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/__tests__/__snapshots__/visualization_flyout.test.tsx.snap b/public/components/custom_panels/panel_modules/visualization_flyout/__tests__/__snapshots__/visualization_flyout.test.tsx.snap index 6b9d3e801..7049259cb 100644 --- a/public/components/custom_panels/panel_modules/visualization_flyout/__tests__/__snapshots__/visualization_flyout.test.tsx.snap +++ b/public/components/custom_panels/panel_modules/visualization_flyout/__tests__/__snapshots__/visualization_flyout.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Visualization Flyout Component renders add visualization Flyout 1`] = ` - - - - + + `; exports[`Visualization Flyout Component renders replace visualization Flyout 1`] = ` - - - - + + `; diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx index db31f0d71..971c4821d 100644 --- a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx @@ -2,7 +2,6 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ /* eslint-disable react-hooks/exhaustive-deps */ import { @@ -33,22 +32,30 @@ import { EuiToolTip, ShortDate, } from '@elastic/eui'; -import _, { isError } from 'lodash'; +import _ from 'lodash'; import React, { useEffect, useState } from 'react'; -import { FlyoutContainers } from '../../../common/flyout_containers'; -import { displayVisualization, getQueryResponse, isDateValid } from '../../helpers/utils'; -import { convertDateTime } from '../../helpers/utils'; -import PPLService from '../../../../services/requests/ppl'; import { CoreStart } from '../../../../../../../src/core/public'; import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; +import { SAVED_VISUALIZATION } from '../../../../../common/constants/explorer'; import { pplResponse, SavedVisualizationType, VisualizationType, VizContainerError, } from '../../../../../common/types/custom_panels'; -import './visualization_flyout.scss'; import { uiSettingsService } from '../../../../../common/utils'; +import PPLService from '../../../../services/requests/ppl'; +import { SavedObjectsActions } from '../../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { ObservabilitySavedVisualization } from '../../../../services/saved_objects/saved_object_client/types'; +import { FlyoutContainers } from '../../../common/flyout_containers'; +import { + convertDateTime, + displayVisualization, + getQueryResponse, + isDateValid, + parseSavedVisualizations, +} from '../../helpers/utils'; +import './visualization_flyout.scss'; /* * VisaulizationFlyout - This module create a flyout to add visualization @@ -334,8 +341,14 @@ export const VisaulizationFlyout = ({ // Fetch all saved visualizations const fetchSavedVisualizations = async () => { - return http - .get(`${CUSTOM_PANELS_API_PREFIX}/visualizations`) + return SavedObjectsActions.getBulk({ + objectType: [SAVED_VISUALIZATION], + sortOrder: 'desc', + fromIndex: 0, + }) + .then((response) => ({ + visualizations: response.observabilityObjectList.map(parseSavedVisualizations), + })) .then((res) => { if (res.visualizations.length > 0) { setSavedVisualizations(res.visualizations); diff --git a/public/components/event_analytics/__tests__/__snapshots__/no_results.test.tsx.snap b/public/components/event_analytics/__tests__/__snapshots__/no_results.test.tsx.snap index a128191e4..50c496076 100644 --- a/public/components/event_analytics/__tests__/__snapshots__/no_results.test.tsx.snap +++ b/public/components/event_analytics/__tests__/__snapshots__/no_results.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`No result component Renders No result component 1`] = ` - + - + `; diff --git a/public/components/event_analytics/explorer/explorer.tsx b/public/components/event_analytics/explorer/explorer.tsx index 379663cf2..32423d6ff 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -20,18 +20,27 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import classNames from 'classnames'; -import { has, isEmpty, isEqual, reduce } from 'lodash'; -import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { isEmpty, isEqual, reduce } from 'lodash'; +import React, { + ReactElement, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { batch, useDispatch, useSelector } from 'react-redux'; +import { LogExplorerRouterContext } from '..'; import { + CREATE_TAB_PARAM, + CREATE_TAB_PARAM_KEY, DATE_PICKER_FORMAT, DEFAULT_AVAILABILITY_QUERY, EVENT_ANALYTICS_DOCUMENTATION_URL, - FILTERED_PATTERN, NEW_TAB, PATTERNS_EXTRACTOR_REGEX, PATTERNS_REGEX, - PATTERN_REGEX, RAW_QUERY, SAVED_OBJECT_ID, SAVED_OBJECT_TYPE, @@ -54,18 +63,34 @@ import { PPL_NEWLINE_REGEX, PPL_STATS_REGEX, } from '../../../../common/constants/shared'; +import { QueryManager } from '../../../../common/query_manager'; import { - IDefaultTimestampState, + IExplorerFields, IExplorerProps, IField, + IQuery, IQueryTab, IVisualizationContainerProps, } from '../../../../common/types/explorer'; import { buildQuery, - composeFinalQuery, getIndexPatternFromRawQuery, + uiSettingsService, } from '../../../../common/utils'; +import { PPLDataFetcher } from '../../../services/data_fetchers/ppl/ppl_data_fetcher'; +import { getSavedObjectsClient } from '../../../services/saved_objects/saved_object_client/client_factory'; +import { OSDSavedVisualizationClient } from '../../../services/saved_objects/saved_object_client/osd_saved_objects/saved_visualization'; +import { + PanelSavedObjectClient, + PPLSavedQueryClient, +} from '../../../services/saved_objects/saved_object_client/ppl'; +import { PPLSavedObjectLoader } from '../../../services/saved_objects/saved_object_loaders/ppl/ppl_loader'; +import { + SaveAsCurrentQuery, + SaveAsCurrentVisualization, + SaveAsNewVisualization, +} from '../../../services/saved_objects/saved_object_savers'; +import { SaveAsNewQuery } from '../../../services/saved_objects/saved_object_savers/ppl/save_as_new_query'; import { sleep } from '../../common/live_tail/live_tail_button'; import { onItemSelect, parseGetSuggestions } from '../../common/search/autocomplete_logic'; import { Search } from '../../common/search/search'; @@ -84,21 +109,15 @@ import { selectVisualizationConfig, } from '../redux/slices/viualization_config_slice'; import { formatError, getDefaultVisConfig } from '../utils'; +import { getContentTabTitle, getDateRange } from '../utils/utils'; import { DataGrid } from './events_views/data_grid'; import { HitsCounter } from './hits_counter/hits_counter'; +import { LogPatterns } from './log_patterns/log_patterns'; import { NoResults } from './no_results'; import { Sidebar } from './sidebar'; import { TimechartHeader } from './timechart_header'; import { ExplorerVisualizations } from './visualizations'; import { CountDistribution } from './visualizations/count_distribution'; -import { QueryManager } from '../../../../common/query_manager'; -import { uiSettingsService } from '../../../../common/utils'; -import { LogPatterns } from './log_patterns/log_patterns'; - -const TYPE_TAB_MAPPING = { - [SAVED_QUERY]: TAB_EVENT_ID, - [SAVED_VISUALIZATION]: TAB_CHART_ID, -}; export const Explorer = ({ pplService, @@ -124,9 +143,10 @@ export const Explorer = ({ callbackInApp, queryManager = new QueryManager(), }: IExplorerProps) => { + const routerContext = useContext(LogExplorerRouterContext); const dispatch = useDispatch(); const requestParams = { tabId }; - const { getLiveTail, getEvents } = useFetchEvents({ + const { getLiveTail, getEvents, getAvailableFields } = useFetchEvents({ pplService, requestParams, }); @@ -153,7 +173,6 @@ export const Explorer = ({ const [selectedCustomPanelOptions, setSelectedCustomPanelOptions] = useState([]); const [selectedPanelName, setSelectedPanelName] = useState(''); const [curVisId, setCurVisId] = useState('bar'); - const [prevIndex, setPrevIndex] = useState(''); const [isPanelTextFieldInvalid, setIsPanelTextFieldInvalid] = useState(false); const [isSidebarClosed, setIsSidebarClosed] = useState(false); const [timeIntervalOptions, setTimeIntervalOptions] = useState(TIME_INTERVAL_OPTIONS); @@ -220,14 +239,15 @@ export const Explorer = ({ }; useEffect(() => { - document.addEventListener('visibilitychange', function () { - if (document.hidden) { - setBrowserTabFocus(false); - } else { - setBrowserTabFocus(true); - } - }); - }); + const handleSetBrowserTabFocus = () => { + if (document.hidden) setBrowserTabFocus(false); + else setBrowserTabFocus(true); + }; + document.addEventListener('visibilitychange', handleSetBrowserTabFocus); + return () => { + document.removeEventListener('visibilitychange', handleSetBrowserTabFocus); + }; + }, []); const getErrorHandler = (title: string) => { return (error: any) => { @@ -238,227 +258,75 @@ export const Explorer = ({ }; }; - const getSavedDataById = async (objectId: string) => { - // load saved query/visualization if object id exists - await savedObjects - .fetchSavedObjects({ - objectId, - }) - .then(async (res) => { - const savedData = res.observabilityObjectList[0]; - const isSavedQuery = has(savedData, SAVED_QUERY); - const savedType = isSavedQuery ? SAVED_QUERY : SAVED_VISUALIZATION; - const objectData = isSavedQuery ? savedData.savedQuery : savedData.savedVisualization; - const isSavedVisualization = savedData.savedVisualization; - const currQuery = objectData?.query || ''; - - if (appLogEvents) { - if (objectData?.selected_date_range?.start && objectData?.selected_date_range?.end) { - setStartTime(objectData.selected_date_range.start); - setEndTime(objectData.selected_date_range.end); - } - } - - // update redux - batch(async () => { - await dispatch( - changeQuery({ - tabId, - query: { - [RAW_QUERY]: currQuery, - [SELECTED_TIMESTAMP]: objectData?.selected_timestamp?.name || 'timestamp', - [SAVED_OBJECT_ID]: objectId, - [SAVED_OBJECT_TYPE]: savedType, - [SELECTED_DATE_RANGE]: - objectData?.selected_date_range?.start && objectData?.selected_date_range?.end - ? [objectData.selected_date_range.start, objectData.selected_date_range.end] - : ['now-15m', 'now'], - }, - }) - ); - await dispatch( - updateFields({ - tabId, - data: { - [SELECTED_FIELDS]: [...objectData?.selected_fields?.tokens], - }, - }) - ); - await dispatch( - updateTabName({ - tabId, - tabName: objectData.name, - }) - ); - // fill saved user configs - if (objectData?.type) { - let visConfig = {}; - const customConfig = objectData.user_configs ? JSON.parse(objectData.user_configs) : {}; - if (!isEmpty(customConfig.dataConfig) && !isEmpty(customConfig.dataConfig?.series)) { - visConfig = { ...customConfig }; - } else { - const statsTokens = queryManager.queryParser().parse(objectData.query).getStats(); - visConfig = { dataConfig: { ...getDefaultVisConfig(statsTokens) } }; - } - await dispatch( - updateVizConfig({ - tabId, - vizId: objectData?.type, - data: visConfig, - }) - ); - } - }); - - // update UI state with saved data - setSelectedPanelName(objectData?.name || ''); - setCurVisId(objectData?.type || 'bar'); - setTempQuery((staleTempQuery: string) => { - return objectData?.query || staleTempQuery; - }); - if (isSavedVisualization?.sub_type) { - if (isSavedVisualization?.sub_type === 'metric') { - setMetricChecked(true); - setMetricMeasure(isSavedVisualization?.units_of_measure); - } - setSubType(isSavedVisualization?.sub_type); - } - const tabToBeFocused = isSavedQuery - ? TYPE_TAB_MAPPING[SAVED_QUERY] - : TYPE_TAB_MAPPING[SAVED_VISUALIZATION]; - setSelectedContentTab(tabToBeFocused); - await fetchData(); - }) - .catch((error) => { - notifications.toasts.addError(error, { - title: `Cannot get saved data for object id: ${objectId}`, - }); - }); - }; - - const getDefaultTimestampByIndexPattern = async ( - indexPattern: string - ): Promise => await timestampUtils.getTimestamp(indexPattern); - const fetchData = async (startingTime?: string, endingTime?: string) => { - const curQuery = queryRef.current; + const curQuery: IQuery = queryRef.current!; const rawQueryStr = (curQuery![RAW_QUERY] as string).includes(appBaseQuery) ? curQuery![RAW_QUERY] : buildQuery(appBasedRef.current, curQuery![RAW_QUERY]); - const curIndex = getIndexPatternFromRawQuery(rawQueryStr); - - if (isEmpty(rawQueryStr)) return; - - if (isEmpty(curIndex)) { - setToast('Query does not include valid index.', 'danger'); - return; - } - - let curTimestamp: string = curQuery![SELECTED_TIMESTAMP]; - if (isEmpty(curTimestamp)) { - const defaultTimestamp = await getDefaultTimestampByIndexPattern(curIndex); - if (isEmpty(defaultTimestamp.default_timestamp)) { - setToast(defaultTimestamp.message, 'danger'); - return; - } - curTimestamp = defaultTimestamp.default_timestamp; - if (defaultTimestamp.hasSchemaConflict) { - setToast(defaultTimestamp.message, 'danger'); - } - } - - let curPattern: string = curQuery![SELECTED_PATTERN_FIELD]; - - if (isEmpty(curPattern)) { - const patternErrorHandler = getErrorHandler('Error fetching default pattern field'); - await setDefaultPatternsField(curIndex, '', patternErrorHandler); - const newQuery = queryRef.current; - curPattern = newQuery![SELECTED_PATTERN_FIELD]; - if (isEmpty(curPattern)) { - setToast('Index does not contain a valid pattern field.', 'danger'); - return; - } - } - - if (isEqual(typeof startingTime, 'undefined') && isEqual(typeof endingTime, 'undefined')) { - startingTime = curQuery![SELECTED_DATE_RANGE][0]; - endingTime = curQuery![SELECTED_DATE_RANGE][1]; - } - - // compose final query - const finalQuery = composeFinalQuery( - curQuery![RAW_QUERY], - startingTime!, - endingTime!, - curTimestamp, - isLiveTailOnRef.current, - appBasedRef.current, - curQuery![SELECTED_PATTERN_FIELD], - curQuery![PATTERN_REGEX], - curQuery![FILTERED_PATTERN] - ); - - batch(() => { - dispatch( - changeQuery({ - tabId, - query: { - finalQuery, - [RAW_QUERY]: rawQueryStr, - [SELECTED_TIMESTAMP]: curTimestamp, - }, - }) - ); - if (selectedContentTabId === TAB_CHART_ID) { - // parse stats section on every search - const statsTokens = queryManager.queryParser().parse(rawQueryStr).getStats(); - const updatedDataConfig = getDefaultVisConfig(statsTokens); - dispatch( - changeVizConfig({ - tabId, - vizId: curVisId, - data: { dataConfig: { ...updatedDataConfig } }, - }) - ); - } - }); - - if (!selectedIntervalRef.current || selectedIntervalRef.current.text === 'Auto') { - findAutoInterval(startingTime, endingTime); - } - if (isLiveTailOnRef.current) { - getLiveTail(undefined, getErrorHandler('Error fetching events')); - } else { - getEvents(undefined, getErrorHandler('Error fetching events')); - } - getCountVisualizations(selectedIntervalRef.current!.value.replace(/^auto_/, '')); - - // to fetch patterns data on current query - if (!finalQuery.match(PATTERNS_REGEX)) { - getPatterns(selectedIntervalRef.current!.value.replace(/^auto_/, '')); - } - - // for comparing usage if for the same tab, user changed index from one to another - if (!isLiveTailOnRef.current) { - setPrevIndex(curTimestamp); - if (!queryRef.current!.isLoaded) { - dispatch( - changeQuery({ - tabId, - query: { - isLoaded: true, - }, - }) - ); - } - } + new PPLDataFetcher( + { ...curQuery }, + { batch, dispatch, changeQuery, changeVizConfig }, + { + tabId, + findAutoInterval, + getCountVisualizations, + getLiveTail, + getEvents, + getErrorHandler, + getPatterns, + setDefaultPatternsField, + timestampUtils, + curVisId, + selectedContentTabId, + queryManager, + getDefaultVisConfig, + getAvailableFields, + }, + { + appBaseQuery, + query: rawQueryStr, + startingTime, + endingTime, + isLiveTailOn: isLiveTailOnRef.current, + selectedInterval: selectedIntervalRef, + }, + notifications + ).search(); }; const isIndexPatternChanged = (currentQuery: string, prevTabQuery: string) => !isEqual(getIndexPatternFromRawQuery(currentQuery), getIndexPatternFromRawQuery(prevTabQuery)); const updateTabData = async (objectId: string) => { - await getSavedDataById(objectId); + await new PPLSavedObjectLoader( + getSavedObjectsClient({ objectId, objectType: 'savedQuery' }), + notifications, + { + batch, + dispatch, + changeQuery, + updateFields, + updateTabName, + updateVizConfig, + }, + { objectId }, + { + tabId, + appLogEvents, + setStartTime, + setEndTime, + queryManager, + getDefaultVisConfig, + setSelectedPanelName, + setCurVisId, + setTempQuery, + setMetricChecked, + setMetricMeasure, + setSubType, + setSelectedContentTab, + fetchData, + } + ).load(); }; const prepareAvailability = async () => { @@ -493,6 +361,12 @@ export const Explorer = ({ } else { fetchData(); } + if ( + routerContext && + routerContext.searchParams.get(CREATE_TAB_PARAM_KEY) === CREATE_TAB_PARAM[TAB_CHART_ID] + ) { + setSelectedContentTab(TAB_CHART_ID); + } }, []); useEffect(() => { @@ -560,16 +434,7 @@ export const Explorer = ({ }); const handleOverrideTimestamp = async (timestamp: IField) => { - const curQuery = queryRef.current; - const rawQueryStr = buildQuery(appBaseQuery, curQuery![RAW_QUERY]); - const curIndex = getIndexPatternFromRawQuery(rawQueryStr); - if (isEmpty(rawQueryStr) || isEmpty(curIndex)) { - setToast('Cannot override timestamp because there was no valid index found.', 'danger'); - return; - } - setIsOverridingTimestamp(true); - await dispatch( changeQuery({ tabId, @@ -578,7 +443,6 @@ export const Explorer = ({ }, }) ); - setIsOverridingTimestamp(false); handleQuerySearch(); }; @@ -612,212 +476,177 @@ export const Explorer = ({ return 0; }, [countDistribution?.data]); - const onPatternSelection = async (pattern: string) => { - if (queryRef.current![FILTERED_PATTERN] === pattern) { - return; - } - dispatch( - changeQuery({ - tabId, - query: { - [FILTERED_PATTERN]: pattern, - }, - }) - ); - // workaround to refresh callback and trigger fetch data - await setTempQuery(queryRef.current![RAW_QUERY]); - await handleTimeRangePickerRefresh(true); - }; - - const getMainContent = () => { + const mainContent = useMemo(() => { return ( -
-
-
- {!isSidebarClosed && ( -
- -
- )} - { - setIsSidebarClosed((staleState) => { - return !staleState; - }); - }} - data-test-subj="collapseSideBarButton" - aria-controls="discover-sidebar" - aria-expanded={isSidebarClosed ? 'false' : 'true'} - aria-label="Toggle sidebar" - className="dscCollapsibleSidebar__collapseButton" - /> -
-
- {explorerData && !isEmpty(explorerData.jsonData) ? ( -
-
- {countDistribution?.data && !isLiveTailOnRef.current && ( - <> - - - +
+ {!isSidebarClosed && ( +
+ +
+ )} + { + setIsSidebarClosed((staleState) => { + return !staleState; + }); + }} + data-test-subj="collapseSideBarButton" + aria-controls="discover-sidebar" + aria-expanded={isSidebarClosed ? 'false' : 'true'} + aria-label="Toggle sidebar" + className="dscCollapsibleSidebar__collapseButton" + /> +
+
+ {explorerData && !isEmpty(explorerData.jsonData) ? ( +
+
+ {countDistribution?.data && !isLiveTailOnRef.current && ( + <> + + + { + return sum + n; + }, + 0 + )} + showResetButton={false} + onResetQuery={() => {}} + /> + + + { + const intervalOptionsIndex = timeIntervalOptions.findIndex( + (item) => item.value === selectedIntrv + ); + const intrv = selectedIntrv.replace(/^auto_/, ''); + getCountVisualizations(intrv); + selectedIntervalRef.current = timeIntervalOptions[intervalOptionsIndex]; + getPatterns(intrv, getErrorHandler('Error fetching patterns')); + }} + stateInterval={selectedIntervalRef.current?.value} + /> + + + + + + + )} + +
+

+ +

+
+ {isLiveTailOnRef.current && ( + <> + + + + +   Live streaming + + + {}} + /> + + since {liveTimestamp} + + + + )} + {countDistribution?.data && ( + +

+ Events + + {' '} + ( + {reduce( countDistribution.data['count()'], (sum, n) => { return sum + n; }, 0 )} - showResetButton={false} - onResetQuery={() => {}} - /> - - - { - const intervalOptionsIndex = timeIntervalOptions.findIndex( - (item) => item.value === selectedIntrv - ); - const intrv = selectedIntrv.replace(/^auto_/, ''); - getCountVisualizations(intrv); - selectedIntervalRef.current = - timeIntervalOptions[intervalOptionsIndex]; - getPatterns(intrv, getErrorHandler('Error fetching patterns')); - }} - stateInterval={selectedIntervalRef.current?.value} - /> - - - - - - - )} - -
-

- -

-
- {isLiveTailOnRef.current && ( - <> - - - - -   Live streaming - - - {}} - /> - - since {liveTimestamp} - - - - )} - {countDistribution?.data && ( - -

- Events - - {' '} - ( - {reduce( - countDistribution.data['count()'], - (sum, n) => { - return sum + n; - }, - 0 - )} - ) - -

-
- )} - - - - ​ - -
-
-

+ ) + + + + )} + + + + ​ + +
+
- ) : ( - - )} -
+
+ ) : ( + + )}
-
+ ); - }; - - function getMainContentTab({ - tabID, - tabTitle, - getContent, - }: { - tabID: string; - tabTitle: string; - getContent: () => JSX.Element; - }) { - return { - id: tabID, - name: ( - <> - - {tabTitle} - - - ), - content: <>{getContent()}, - }; - } + }, [ + isPanelTextFieldInvalid, + explorerData, + explorerFields, + isSidebarClosed, + countDistribution, + explorerVisualizations, + isOverridingTimestamp, + query, + isLiveTailOnRef.current, + ]); const visualizations: IVisualizationContainerProps = useMemo(() => { return getVizContainerProps({ @@ -842,7 +671,7 @@ export const Explorer = ({ } }; - const getExplorerVis = () => { + const explorerVis = useMemo(() => { return ( ); - }; + }, [query, curVisId, explorerFields, explorerVisualizations, explorerData, visualizations]); - const getMainContentTabs = () => { - return [ - getMainContentTab({ - tabID: TAB_EVENT_ID, - tabTitle: TAB_EVENT_TITLE, - getContent: () => getMainContent(), - }), - getMainContentTab({ - tabID: TAB_CHART_ID, - tabTitle: TAB_CHART_TITLE, - getContent: () => getExplorerVis(), - }), - ]; - }; + const contentTabs = [ + { + id: TAB_EVENT_ID, + name: getContentTabTitle(TAB_EVENT_ID, TAB_EVENT_TITLE), + content: mainContent, + }, + { + id: TAB_CHART_ID, + name: getContentTabTitle(TAB_CHART_ID, TAB_CHART_TITLE), + content: explorerVis, + }, + ]; - const memorizedMainContentTabs = useMemo(() => { - return getMainContentTabs(); - }, [ - curVisId, - isPanelTextFieldInvalid, - explorerData, - explorerFields, - isSidebarClosed, - countDistribution, - explorerVisualizations, - selectedContentTabId, - isOverridingTimestamp, - visualizations, - query, - isLiveTailOnRef.current, - userVizConfigs, - ]); const handleContentTabClick = (selectedTab: IQueryTab) => setSelectedContentTab(selectedTab.id); const updateQueryInStore = async (updateQuery: string) => { @@ -904,26 +714,18 @@ export const Explorer = ({ ); }; - const updateCurrentTimeStamp = async (timestamp: string) => { - await dispatch( - changeQuery({ - tabId, - query: { - [SELECTED_TIMESTAMP]: timestamp, - }, - }) - ); - }; - const handleQuerySearch = useCallback( async (availability?: boolean) => { // clear previous selected timestamp when index pattern changes - if ( - !isEmpty(tempQuery) && - !isEmpty(query[RAW_QUERY]) && - isIndexPatternChanged(tempQuery, query[RAW_QUERY]) - ) { - await updateCurrentTimeStamp(''); + if (isIndexPatternChanged(tempQuery, query[RAW_QUERY])) { + await dispatch( + changeQuery({ + tabId, + query: { + [SELECTED_TIMESTAMP]: '', + }, + }) + ); await setDefaultPatternsField('', ''); } if (availability !== true) { @@ -936,218 +738,102 @@ export const Explorer = ({ const handleQueryChange = async (newQuery: string) => setTempQuery(newQuery); - const handleSavingObject = async () => { - const currQuery = queryRef.current; - const currFields = explorerFieldsRef.current; - if (isEmpty(currQuery![RAW_QUERY]) && isEmpty(appBaseQuery)) { - setToast('No query to save.', 'danger'); - return; - } - if (isEmpty(selectedPanelNameRef.current)) { - setIsPanelTextFieldInvalid(true); - setToast('Name field cannot be empty.', 'danger'); - return; - } - setIsPanelTextFieldInvalid(false); - if (isEqual(selectedContentTabId, TAB_EVENT_ID)) { - const isTabMatchingSavedType = isEqual(currQuery![SAVED_OBJECT_TYPE], SAVED_QUERY); - if (!isEmpty(currQuery![SAVED_OBJECT_ID]) && isTabMatchingSavedType) { - await savedObjects - .updateSavedQueryById({ - query: currQuery![RAW_QUERY], - fields: currFields![SELECTED_FIELDS], - dateRange: currQuery![SELECTED_DATE_RANGE], - name: selectedPanelNameRef.current, - timestamp: currQuery![SELECTED_TIMESTAMP], - objectId: currQuery![SAVED_OBJECT_ID], - type: '', - }) - .then((res: any) => { - setToast( - `Query '${selectedPanelNameRef.current}' has been successfully updated.`, - 'success' - ); - dispatch( - updateTabName({ - tabId, - tabName: selectedPanelNameRef.current, - }) - ); - return res; - }) - .catch((error: any) => { - notifications.toasts.addError(error, { - title: `Cannot update query '${selectedPanelNameRef.current}'`, - }); - }); + const getSavingCommonParams = ( + queryState: IQuery, + fields: IExplorerFields, + savingTitle: string + ) => { + return { + query: queryState[RAW_QUERY], + fields: fields[SELECTED_FIELDS], + dateRange: queryState[SELECTED_DATE_RANGE], + name: savingTitle, + timestamp: queryState[SELECTED_TIMESTAMP], + }; + }; + + const handleSavingObject = useCallback(() => { + const isOnEventPage = isEqual(selectedContentTabId, TAB_EVENT_ID); + const isObjTypeMatchQuery = isEqual(query[SAVED_OBJECT_TYPE], SAVED_QUERY); + const isObjTypeMatchVis = isEqual(query[SAVED_OBJECT_TYPE], SAVED_VISUALIZATION); + const isTabHasObjID = !isEmpty(query[SAVED_OBJECT_ID]); + const commonParams = getSavingCommonParams(query, explorerFields, selectedPanelNameRef.current); + + let soClient; + if (isOnEventPage) { + if (isTabHasObjID && isObjTypeMatchQuery) { + soClient = new SaveAsCurrentQuery( + { tabId, notifications }, + { dispatch, updateTabName }, + PPLSavedQueryClient.getInstance(), + { + ...commonParams, + objectId: query[SAVED_OBJECT_ID], + } + ); } else { - // create new saved query - savedObjects - .createSavedQuery({ - query: currQuery![RAW_QUERY], - fields: currFields![SELECTED_FIELDS], - dateRange: currQuery![SELECTED_DATE_RANGE], - name: selectedPanelNameRef.current, - timestamp: currQuery![SELECTED_TIMESTAMP], - objectId: '', - type: '', - }) - .then((res: any) => { - history.replace(`/event_analytics/explorer/${res.objectId}`); - setToast( - `New query '${selectedPanelNameRef.current}' has been successfully saved.`, - 'success' - ); - batch(() => { - dispatch( - changeQuery({ - tabId, - query: { - [SAVED_OBJECT_ID]: res.objectId, - [SAVED_OBJECT_TYPE]: SAVED_QUERY, - }, - }) - ); - dispatch( - updateTabName({ - tabId, - tabName: selectedPanelNameRef.current, - }) - ); - }); - history.replace(`/event_analytics/explorer/${res.objectId}`); - return res; - }) - .catch((error: any) => { - if (error?.body?.statusCode === 403) { - showPermissionErrorToast(); - } else { - notifications.toasts.addError(error, { - title: `Cannot save query '${selectedPanelNameRef.current}'`, - }); - } - }); - } - // to-dos - update selected custom panel - if (!isEmpty(selectedCustomPanelOptions)) { - // update custom panel - query - } - } else if (isEqual(selectedContentTabId, TAB_CHART_ID)) { - if (isEmpty(currQuery![RAW_QUERY]) && isEmpty(appBaseQuery)) { - setToast(`There is no query or(and) visualization to save`, 'danger'); - return; + soClient = new SaveAsNewQuery( + { tabId, history, notifications, showPermissionErrorToast }, + { batch, dispatch, changeQuery, updateTabName }, + new PPLSavedQueryClient(http), + { ...commonParams } + ); } - let savingVisRes; - const vizDescription = userVizConfigs[curVisId]?.dataConfig?.panelOptions?.description || ''; - const isTabMatchingSavedType = isEqual(currQuery![SAVED_OBJECT_TYPE], SAVED_VISUALIZATION); - if (!isEmpty(currQuery![SAVED_OBJECT_ID]) && isTabMatchingSavedType) { - savingVisRes = await savedObjects - .updateSavedVisualizationById({ - query: buildQuery('', currQuery![RAW_QUERY]), - fields: currFields![SELECTED_FIELDS], - dateRange: currQuery![SELECTED_DATE_RANGE], - name: selectedPanelNameRef.current, - timestamp: currQuery![SELECTED_TIMESTAMP], - objectId: currQuery![SAVED_OBJECT_ID], + } else { + if (isTabHasObjID && isObjTypeMatchVis) { + soClient = new SaveAsCurrentVisualization( + { tabId, history, notifications, showPermissionErrorToast }, + { batch, dispatch, changeQuery, updateTabName }, + getSavedObjectsClient({ + objectId: query[SAVED_OBJECT_ID], + objectType: 'savedVisualization', + }), + new PanelSavedObjectClient(http), + { + ...commonParams, + objectId: query[SAVED_OBJECT_ID], type: curVisId, - userConfigs: userVizConfigs.hasOwnProperty(curVisId) - ? JSON.stringify(userVizConfigs[curVisId]) - : JSON.stringify({}), - description: vizDescription, + userConfigs: JSON.stringify(userVizConfigs[curVisId]), + description: userVizConfigs[curVisId]?.dataConfig?.panelOptions?.description || '', subType, - }) - .then((res: any) => { - setToast( - `Visualization '${selectedPanelNameRef.current}' has been successfully updated.`, - 'success' - ); - dispatch( - updateTabName({ - tabId, - tabName: selectedPanelNameRef.current, - }) - ); - return res; - }) - .catch((error: any) => { - notifications.toasts.addError(error, { - title: `Cannot update Visualization '${selectedPanelNameRef.current}'`, - }); - }); + selectedPanels: selectedCustomPanelOptions, + } + ); } else { - // create new saved visualization - savingVisRes = await savedObjects - .createSavedVisualization({ - query: buildQuery('', currQuery![RAW_QUERY]), - fields: currFields![SELECTED_FIELDS], - dateRange: currQuery![SELECTED_DATE_RANGE], + soClient = new SaveAsNewVisualization( + { + tabId, + history, + notifications, + showPermissionErrorToast, + appLogEvents, + addVisualizationToPanel, + }, + { batch, dispatch, changeQuery, updateTabName }, + OSDSavedVisualizationClient.getInstance(), + new PanelSavedObjectClient(http), + { + ...commonParams, type: curVisId, - name: selectedPanelNameRef.current, - timestamp: currQuery![SELECTED_TIMESTAMP], applicationId: appId, - userConfigs: userVizConfigs.hasOwnProperty(curVisId) - ? JSON.stringify(userVizConfigs[curVisId]) - : JSON.stringify({}), - description: vizDescription, + userConfigs: JSON.stringify(userVizConfigs[curVisId]), + description: userVizConfigs[curVisId]?.dataConfig?.panelOptions?.description || '', subType, - }) - .then((res: any) => { - batch(() => { - dispatch( - changeQuery({ - tabId, - query: { - [SAVED_OBJECT_ID]: res.objectId, - [SAVED_OBJECT_TYPE]: SAVED_VISUALIZATION, - }, - }) - ); - dispatch( - updateTabName({ - tabId, - tabName: selectedPanelNameRef.current, - }) - ); - }); - if (appLogEvents) { - addVisualizationToPanel(res.objectId, selectedPanelNameRef.current); - } else { - history.replace(`/event_analytics/explorer/${res.objectId}`); - } - setToast( - `New visualization '${selectedPanelNameRef.current}' has been successfully saved.`, - 'success' - ); - return res; - }) - .catch((error: any) => { - notifications.toasts.addError(error, { - title: `Cannot save Visualization '${selectedPanelNameRef.current}'`, - }); - }); - } - if (!has(savingVisRes, 'objectId')) return; - // update custom panel - visualization - if (!isEmpty(selectedCustomPanelOptions)) { - savedObjects - .bulkUpdateCustomPanel({ - selectedCustomPanels: selectedCustomPanelOptions, - savedVisualizationId: savingVisRes.objectId, - }) - .then((res: any) => { - setToast( - `Visualization '${selectedPanelNameRef.current}' has been successfully saved to operation panels.`, - 'success' - ); - }) - .catch((error: any) => { - notifications.toasts.addError(error, { - title: `Cannot add Visualization '${selectedPanelNameRef.current}' to operation panels`, - }); - }); + selectedPanels: selectedCustomPanelOptions, + } + ); } } - }; + soClient.save(); + }, [ + query, + curVisId, + userVizConfigs, + selectedContentTabId, + explorerFields, + subType, + selectedCustomPanelOptions, + ]); const liveTailLoop = async ( name: string, @@ -1216,12 +902,7 @@ export const Explorer = ({ ); }); - const dateRange = - isEmpty(startTime) || isEmpty(endTime) - ? isEmpty(query.selectedDateRange) - ? ['now-15m', 'now'] - : [query.selectedDateRange[0], query.selectedDateRange[1]] - : [startTime, endTime]; + const dateRange = getDateRange(startTime, endTime, query); const handleLiveTailSearch = useCallback( async (startingTime: string, endingTime: string) => { @@ -1231,15 +912,12 @@ export const Explorer = ({ [tempQuery] ); - const generateViewQuery = (queryString: string) => { - if (queryString.includes(appBaseQuery)) { - if (queryString.includes('|')) { - // Some scenarios have ' | ' after base query and some have '| ' - return queryString.replace(' | ', '| ').replace(appBaseQuery + '| ', ''); - } - return ''; - } - return queryString; + const processAppAnalyticsQuery = (queryString: string) => { + if (!queryString.includes(appBaseQuery)) return queryString; + if (queryString.includes(appBaseQuery) && queryString.includes('|')) + // Some scenarios have ' | ' after base query and some have '| ' + return queryString.replace(' | ', '| ').replace(appBaseQuery + '| ', ''); + return ''; }; return ( @@ -1247,20 +925,12 @@ export const Explorer = ({ value={{ tabId, curVisId, - dispatch, changeVisualizationConfig, - explorerVisualizations, setToast, pplService, - handleQuerySearch, - handleQueryChange, - setTempQuery, - fetchData, - explorerFields, - explorerData, - http, - query, notifications, + dispatch, + handleQueryChange, }} >
tab.id === selectedContentTabId)} + initialSelectedTab={contentTabs[0]} + selectedTab={contentTabs.find((tab) => tab.id === selectedContentTabId)} onTabClick={(selectedTab: EuiTabbedContentTab) => handleContentTabClick(selectedTab)} - tabs={memorizedMainContentTabs} + tabs={contentTabs} + size="s" />
diff --git a/public/components/event_analytics/explorer/log_explorer.scss b/public/components/event_analytics/explorer/log_explorer.scss index 772a5b873..f8c25dac7 100644 --- a/public/components/event_analytics/explorer/log_explorer.scss +++ b/public/components/event_analytics/explorer/log_explorer.scss @@ -14,12 +14,12 @@ svg { vertical-align: inherit; } - .linkNewTag{ + .linkNewTag { display: inline-block; text-align: center; font-size: 0.875rem; - line-height: 3.5; - padding: 0.5rem; + line-height: $tab-new-line-height; + padding: $tab-new-padding; // align with content tab with small size min-width: 6rem; } } diff --git a/public/components/event_analytics/explorer/log_explorer.tsx b/public/components/event_analytics/explorer/log_explorer.tsx index b9e20a37a..ac9aa69bd 100644 --- a/public/components/event_analytics/explorer/log_explorer.tsx +++ b/public/components/event_analytics/explorer/log_explorer.tsx @@ -4,33 +4,35 @@ */ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { map, isEmpty } from 'lodash'; -import $ from 'jquery'; import { EuiIcon, - EuiText, - EuiTabbedContentTab, EuiTabbedContent, + EuiTabbedContentTab, + EuiText, htmlIdGenerator, } from '@elastic/eui'; -import { Explorer } from './explorer'; -import { ILogExplorerProps } from '../../../../common/types/explorer'; +import $ from 'jquery'; +import { isEmpty, map } from 'lodash'; +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { LogExplorerRouterContext } from '..'; import { - TAB_TITLE, - TAB_ID_TXT_PFX, - SAVED_OBJECT_ID, + APP_ANALYTICS_TAB_ID_REGEX, + CREATE_TAB_PARAM_KEY, NEW_TAB, REDIRECT_TAB, - TAB_EVENT_ID, + SAVED_OBJECT_ID, TAB_CHART_ID, - APP_ANALYTICS_TAB_ID_REGEX, + TAB_EVENT_ID, + TAB_ID_TXT_PFX, + TAB_TITLE, } from '../../../../common/constants/explorer'; -import { selectQueryTabs, setSelectedQueryTab } from '../redux/slices/query_tab_slice'; -import { selectQueries } from '../redux/slices/query_slice'; -import { selectQueryResult } from '../redux/slices/query_result_slice'; +import { ILogExplorerProps } from '../../../../common/types/explorer'; import { initializeTabData, removeTabData } from '../../application_analytics/helpers/utils'; +import { selectQueryResult } from '../redux/slices/query_result_slice'; +import { selectQueries } from '../redux/slices/query_slice'; +import { selectQueryTabs, setSelectedQueryTab } from '../redux/slices/query_tab_slice'; +import { Explorer } from './explorer'; const searchBarConfigs = { [TAB_EVENT_ID]: { @@ -56,6 +58,7 @@ export const LogExplorer = ({ http, queryManager, }: ILogExplorerProps) => { + const routerContext = useContext(LogExplorerRouterContext); const dispatch = useDispatch(); const tabIds = useSelector(selectQueryTabs).queryTabIds.filter( (tabid: string) => !tabid.match(APP_ANALYTICS_TAB_ID_REGEX) @@ -145,6 +148,16 @@ export const LogExplorer = ({ if (!isEmpty(savedObjectId)) { dispatchSavedObjectId(); } + if (routerContext && routerContext.searchParams.has(CREATE_TAB_PARAM_KEY)) { + // need to wait for current redux event loop to finish + setImmediate(() => { + addNewTab(NEW_TAB); + routerContext.searchParams.delete(CREATE_TAB_PARAM_KEY); + routerContext.routerProps.history.replace({ + search: routerContext.searchParams.toString(), + }); + }); + } }, []); function getQueryTab({ @@ -218,6 +231,7 @@ export const LogExplorer = ({ selectedTab={memorizedTabs.find((tab) => tab.id === curSelectedTabId)} onTabClick={(selectedTab: EuiTabbedContentTab) => handleTabClick(selectedTab)} data-test-subj="eventExplorer__topLevelTabbing" + size="s" /> ); diff --git a/public/components/event_analytics/explorer/log_patterns/__tests__/__snapshots__/patterns_header.test.tsx.snap b/public/components/event_analytics/explorer/log_patterns/__tests__/__snapshots__/patterns_header.test.tsx.snap index 877740513..2ce4cbd87 100644 --- a/public/components/event_analytics/explorer/log_patterns/__tests__/__snapshots__/patterns_header.test.tsx.snap +++ b/public/components/event_analytics/explorer/log_patterns/__tests__/__snapshots__/patterns_header.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Patterns header component Renders header of log patterns 1`] = ` - - + `; diff --git a/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap b/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap index dce6acd73..acc9e820e 100644 --- a/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap +++ b/public/components/event_analytics/explorer/save_panel/__tests__/__snapshots__/save_panel.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Saved query table component Renders saved query table 1`] = ` - - + `; diff --git a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap index 35c8c3a91..8ec133b20 100644 --- a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap +++ b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/field.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Field component Renders a sidebar field 1`] = ` - - + `; diff --git a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap index 1d5a67a00..cffedeeb5 100644 --- a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap +++ b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap @@ -12,7 +12,7 @@ exports[`Siderbar component Renders empty sidebar component 1`] = ` } } > - - + `; @@ -233,7 +233,7 @@ exports[`Siderbar component Renders sidebar component 1`] = ` } } > - - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • @@ -2909,7 +2909,7 @@ exports[`Siderbar component Renders sidebar component 1`] = ` data-attr-field="agent" key="fieldagent" > - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • - - +
  • @@ -10060,6 +10060,6 @@ exports[`Siderbar component Renders sidebar component 1`] = ` -
    + `; diff --git a/public/components/event_analytics/explorer/sidebar/sidebar.scss b/public/components/event_analytics/explorer/sidebar/sidebar.scss index ac5b4326c..cc9557c25 100644 --- a/public/components/event_analytics/explorer/sidebar/sidebar.scss +++ b/public/components/event_analytics/explorer/sidebar/sidebar.scss @@ -3,14 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -.dscSidebar__container { - padding-left: 0 !important; - padding-right: 0 !important; - background-color: transparent; - border-right-color: transparent; - border-bottom-color: transparent; -} - .dscIndexPattern__container { display: flex; align-items: center; @@ -97,10 +89,6 @@ vertical-align: middle; } -.explorerFieldSelector { - padding: $euiSizeS; -} - #vis__mainContent { .explorer__insights { .explorerFieldSelector, .explorer__vizDataConfig { diff --git a/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap b/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap index f6c29b52f..a712df4bb 100644 --- a/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap +++ b/public/components/event_analytics/explorer/visualizations/config_panel/__tests__/__snapshots__/config_panel.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Config panel component Renders config panel with visualization data 1`] = ` - - - - + @@ -8183,7 +8183,7 @@ exports[`Config panel component Renders config panel with visualization data 1`]
    - -
    - + @@ -9256,7 +9256,7 @@ exports[`Config panel component Renders config panel with visualization data 1`]
    -
    - + @@ -9632,7 +9632,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] - + @@ -9651,7 +9651,7 @@ exports[`Config panel component Renders config panel with visualization data 1`]
    - -
    - + @@ -10719,7 +10719,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -10993,7 +10993,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -11071,7 +11071,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] - + @@ -11090,7 +11090,7 @@ exports[`Config panel component Renders config panel with visualization data 1`]
    - -
    - + @@ -12211,7 +12211,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -12282,7 +12282,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -13267,7 +13267,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -13508,7 +13508,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -13749,7 +13749,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -13990,7 +13990,7 @@ exports[`Config panel component Renders config panel with visualization data 1`] className="euiSpacer euiSpacer--s" /> - - + @@ -14237,18 +14237,18 @@ exports[`Config panel component Renders config panel with visualization data 1`] - + -
    + -
    + `; diff --git a/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap b/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap index caaac78ac..03e9b3fb4 100644 --- a/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap +++ b/public/components/event_analytics/explorer/visualizations/count_distribution/__tests__/__snapshots__/count_distribution.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Count distribution component Renders count distribution component with data 1`] = ` - - + `; -exports[`Count distribution component Renders empty count distribution component 1`] = ``; +exports[`Count distribution component Renders empty count distribution component 1`] = ``; diff --git a/public/components/event_analytics/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap b/public/components/event_analytics/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap index f409c4a8d..adb37ebb1 100644 --- a/public/components/event_analytics/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap +++ b/public/components/event_analytics/explorer/visualizations/shared_components/__tests__/__snapshots__/shared_components.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Shared components Renders empty placeholder component 1`] = ` - - - + - + `; exports[`Shared components Renders tool bar button component 1`] = ` - @@ -203,5 +203,5 @@ exports[`Shared components Renders tool bar button component 1`] = ` - + `; diff --git a/public/components/event_analytics/home/home.tsx b/public/components/event_analytics/home/home.tsx index 9e58215fb..f69489dfc 100644 --- a/public/components/event_analytics/home/home.tsx +++ b/public/components/event_analytics/home/home.tsx @@ -3,67 +3,68 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, ReactElement, useRef, useEffect } from 'react'; -import { useDispatch, batch, connect } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageHeaderSection, - EuiTitle, - EuiPageContent, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, EuiButton, - EuiPopover, - EuiContextMenuPanel, EuiContextMenuItem, - EuiOverlayMask, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, EuiLink, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPopover, + EuiSpacer, EuiText, - EuiHorizontalRule, + EuiTitle, htmlIdGenerator, } from '@elastic/eui'; -import { DeleteModal } from '../../common/helpers/delete_modal'; -import { Search } from '../../common/search/search'; +import React, { ReactElement, useEffect, useRef, useState } from 'react'; +import { batch, connect, useDispatch } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { HttpStart } from '../../../../../../src/core/public'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; import { - RAW_QUERY, - TAB_ID_TXT_PFX, - SELECTED_DATE_RANGE, EVENT_ANALYTICS_DOCUMENTATION_URL, - TAB_CREATED_TYPE, NEW_TAB, + RAW_QUERY, REDIRECT_TAB, + SELECTED_DATE_RANGE, + TAB_CREATED_TYPE, + TAB_ID_TXT_PFX, } from '../../../../common/constants/explorer'; import { - OBSERVABILITY_BASE, EVENT_ANALYTICS, + OBSERVABILITY_BASE, SAVED_OBJECTS, } from '../../../../common/constants/shared'; import { EmptyTabParams, ExplorerData as IExplorerData, IQuery, - SavedQueryRes, - SavedVizRes, } from '../../../../common/types/explorer'; -import { HttpStart } from '../../../../../../src/core/public'; +import { getOSDSavedObjectsClient } from '../../../../common/utils'; import SavedObjects from '../../../services/saved_objects/event_analytics/saved_objects'; -import { addTab, selectQueryTabs } from '../redux/slices/query_tab_slice'; +import { OSDSavedVisualizationClient } from '../../../services/saved_objects/saved_object_client/osd_saved_objects/saved_visualization'; +import { PPLSavedQueryClient } from '../../../services/saved_objects/saved_object_client/ppl'; +import { SavedObjectsActions } from '../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { ObservabilitySavedObject } from '../../../services/saved_objects/saved_object_client/types'; +import { getSampleDataModal } from '../../common/helpers/add_sample_modal'; +import { DeleteModal } from '../../common/helpers/delete_modal'; +import { onItemSelect, parseGetSuggestions } from '../../common/search/autocomplete_logic'; +import { Search } from '../../common/search/search'; import { init as initFields } from '../redux/slices/field_slice'; -import { init as initQuery, changeQuery } from '../redux/slices/query_slice'; -import { init as initQueryResult, selectQueryResult } from '../redux/slices/query_result_slice'; import { init as initPatterns } from '../redux/slices/patterns_slice'; +import { init as initQueryResult, selectQueryResult } from '../redux/slices/query_result_slice'; +import { changeQuery, init as initQuery, selectQueries } from '../redux/slices/query_slice'; +import { addTab, selectQueryTabs, setSelectedQueryTab } from '../redux/slices/query_tab_slice'; import { SavedQueryTable } from './saved_objects_table'; -import { selectQueries } from '../redux/slices/query_slice'; -import { setSelectedQueryTab } from '../redux/slices/query_tab_slice'; -import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; -import { getSampleDataModal } from '../../common/helpers/add_sample_modal'; -import { parseGetSuggestions, onItemSelect } from '../../common/search/autocomplete_logic'; interface IHomeProps { pplService: any; @@ -116,13 +117,13 @@ const EventAnalyticsHome = (props: IHomeProps) => { }; const fetchHistories = async () => { - const res = await savedObjects.fetchSavedObjects({ + const observabilityObjects = await SavedObjectsActions.getBulk({ objectType: ['savedQuery', 'savedVisualization'], sortOrder: 'desc', fromIndex: 0, }); - const nonAppObjects = res.observabilityObjectList.filter( - (object: SavedQueryRes | SavedVizRes) => + const nonAppObjects = observabilityObjects.observabilityObjectList.filter( + (object: ObservabilitySavedObject) => (object.savedVisualization && !object.savedVisualization.application_id) || object.savedQuery ); @@ -131,9 +132,8 @@ const EventAnalyticsHome = (props: IHomeProps) => { const deleteHistoryList = async () => { const objectIdsToDelete = selectedHistories.map((hstry) => hstry.data.objectId); - await savedObjects - .deleteSavedObjectsList({ objectIdList: objectIdsToDelete }) - .then(async (res) => { + await SavedObjectsActions.deleteBulk({ objectIdList: objectIdsToDelete }) + .then(async () => { setSavedHistories((staleHistories) => { return staleHistories.filter((his) => { return !objectIdsToDelete.includes(his.objectId); @@ -274,8 +274,10 @@ const EventAnalyticsHome = (props: IHomeProps) => { }), }); - const res = await savedObjects.fetchSavedObjects({ - objectIdList: [...resp?.savedVizIds, ...resp?.savedQueryIds] || [], + // wait for sample data to flush to index + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const res = await SavedObjectsActions.getBulk({ objectType: ['savedQuery', 'savedVisualization'], sortOrder: 'desc', fromIndex: 0, diff --git a/public/components/event_analytics/home/saved_objects_table.tsx b/public/components/event_analytics/home/saved_objects_table.tsx index afe00992f..9aa396264 100644 --- a/public/components/event_analytics/home/saved_objects_table.tsx +++ b/public/components/event_analytics/home/saved_objects_table.tsx @@ -3,17 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useRef } from 'react'; -import { EuiLink, EuiInMemoryTable, EuiIcon } from '@elastic/eui'; +import { Criteria, EuiIcon, EuiInMemoryTable, EuiLink } from '@elastic/eui'; +import React, { useRef, useState } from 'react'; import { FILTER_OPTIONS } from '../../../../common/constants/explorer'; -import { isEmpty } from 'lodash'; -interface savedQueryTableProps { - savedHistories: Array; +interface SavedQueryTableProps { + savedHistories: any[]; handleHistoryClick: (objectId: string) => void; - handleSelectHistory: (selectedHistories: Array) => void; + handleSelectHistory: (selectedHistories: any[]) => void; isTableLoading: boolean; - selectedHistories: Array; + selectedHistories: History[]; } export function SavedQueryTable({ @@ -21,7 +20,7 @@ export function SavedQueryTable({ handleHistoryClick, handleSelectHistory, isTableLoading, -}: savedQueryTableProps) { +}: SavedQueryTableProps) { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const pageIndexRef = useRef(); @@ -29,11 +28,12 @@ export function SavedQueryTable({ const pageSizeRef = useRef(); pageSizeRef.current = pageSize; - const onTableChange = ({ page = {} }) => { - const { index: pageIndex, size: pageSize } = page; - - setPageIndex(pageIndex); - setPageSize(pageSize); + const onTableChange = (criteria: Criteria) => { + if (criteria.page) { + const { index, size } = criteria.page; + setPageIndex(index); + setPageSize(size); + } }; const columns = [ @@ -43,7 +43,7 @@ export function SavedQueryTable({ sortable: true, width: '40px', render: (item: any) => { - if (item == 'Visualization') { + if (item === 'Visualization') { return (
    @@ -87,14 +87,11 @@ export function SavedQueryTable({ const isSavedVisualization = h.hasOwnProperty('savedVisualization'); const savedObject = isSavedVisualization ? h.savedVisualization : h.savedQuery; const curType = isSavedVisualization ? 'savedVisualization' : 'savedQuery'; - var subType = ''; - if (isSavedVisualization) { - if (savedObject?.sub_type === 'metric') { - subType = 'Metric' - } else { - subType = savedObject?.sub_type; - } - } + const displayType = !isSavedVisualization + ? 'Query' + : savedObject?.sub_type === 'metric' + ? 'Metric' + : 'Visualization'; const record = { objectId: h.objectId, objectType: curType, @@ -109,7 +106,7 @@ export function SavedQueryTable({ id: h.objectId, data: record, name: savedObject.name, - type: isSavedVisualization ? (!isEmpty(subType)) ? subType: 'Visualization' : 'Query', + type: displayType, }; }); diff --git a/public/components/event_analytics/hooks/use_fetch_events.ts b/public/components/event_analytics/hooks/use_fetch_events.ts index bbcbb4b78..cfd229eb0 100644 --- a/public/components/event_analytics/hooks/use_fetch_events.ts +++ b/public/components/event_analytics/hooks/use_fetch_events.ts @@ -21,6 +21,7 @@ import { selectQueries } from '../redux/slices/query_slice'; import { reset as visualizationReset } from '../redux/slices/visualization_slice'; import { updateFields, sortFields, selectFields } from '../redux/slices/field_slice'; import PPLService from '../../../services/requests/ppl'; +import { PPL_STATS_REGEX } from '../../../../common/constants/shared'; interface IFetchEventsParams { pplService: PPLService; @@ -57,7 +58,7 @@ export const useFetchEvents = ({ pplService, requestParams }: IFetchEventsParams .finally(() => setIsEventsLoading(false)); }; - const dispatchOnGettingHis = (res: any) => { + const dispatchOnGettingHis = (res: any, query: string) => { const selectedFields: string[] = fieldsRef.current![requestParams.tabId][SELECTED_FIELDS].map( (field: IField) => field.name ); @@ -81,8 +82,8 @@ export const useFetchEvents = ({ pplService, requestParams }: IFetchEventsParams tabId: requestParams.tabId, data: { [UNSELECTED_FIELDS]: res?.schema ? [...res.schema] : [], - [QUERIED_FIELDS]: [], - [AVAILABLE_FIELDS]: res?.schema || [], + [QUERIED_FIELDS]: query.match(PPL_STATS_REGEX) ? [...res.schema] : [], // when query contains stats, need populate this + [AVAILABLE_FIELDS]: res?.schema ? [...res.schema] : [], [SELECTED_FIELDS]: [], }, }) @@ -153,7 +154,7 @@ export const useFetchEvents = ({ pplService, requestParams }: IFetchEventsParams res.total = res.total + responseRef.current.total; res.size = res.size + responseRef.current.size; } - dispatchOnGettingHis(res); + dispatchOnGettingHis(res, searchQuery); } if (isEmpty(res.jsonData) && isEmpty(responseRef.current)) { dispatchOnNoHis(res); @@ -171,7 +172,7 @@ export const useFetchEvents = ({ pplService, requestParams }: IFetchEventsParams 'jdbc', (res: any) => { if (!isEmpty(res.jsonData)) { - return dispatchOnGettingHis(res); + return dispatchOnGettingHis(res, searchQuery); } // when no hits and needs to get available fields to override default timestamp dispatchOnNoHis(res); @@ -183,14 +184,6 @@ export const useFetchEvents = ({ pplService, requestParams }: IFetchEventsParams const getAvailableFields = (query: string) => { fetchEvents({ query }, 'jdbc', (res: any) => { batch(() => { - dispatch( - fetchSuccess({ - tabId: requestParams.tabId, - data: { - jsonDataAll: res.jsonData, - }, - }) - ); dispatch( updateFields({ tabId: requestParams.tabId, diff --git a/public/components/event_analytics/index.tsx b/public/components/event_analytics/index.tsx index ff4c22513..711b891d1 100644 --- a/public/components/event_analytics/index.tsx +++ b/public/components/event_analytics/index.tsx @@ -7,12 +7,17 @@ import { EuiGlobalToastList } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; import { EmptyTabParams, EventAnalyticsProps } from 'common/types/explorer'; import { isEmpty } from 'lodash'; -import React, { ReactChild, useState } from 'react'; -import { HashRouter, Route, Switch, useHistory } from 'react-router-dom'; +import React, { createContext, ReactChild, useState } from 'react'; +import { HashRouter, Route, RouteComponentProps, Switch, useHistory } from 'react-router-dom'; import { RAW_QUERY } from '../../../common/constants/explorer'; import { ObservabilitySideBar } from '../common/side_nav'; -import { Home as EventExplorerHome } from './home/home'; import { LogExplorer } from './explorer/log_explorer'; +import { Home as EventExplorerHome } from './home/home'; + +export const LogExplorerRouterContext = createContext<{ + routerProps: RouteComponentProps; + searchParams: URLSearchParams; +} | null>(null); export const EventAnalytics = ({ chrome, @@ -64,7 +69,7 @@ export const EventAnalytics = ({ { + render={(routerProps) => { chrome.setBreadcrumbs([ ...parentBreadcrumbs, eventAnalyticsBreadcrumb, @@ -74,19 +79,26 @@ export const EventAnalytics = ({ }, ]); return ( - + + + ); }} /> diff --git a/public/components/event_analytics/utils/utils.tsx b/public/components/event_analytics/utils/utils.tsx index 494d1e06a..cc786f0d8 100644 --- a/public/components/event_analytics/utils/utils.tsx +++ b/public/components/event_analytics/utils/utils.tsx @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import dateMath from '@elastic/datemath'; import { uniqueId, isEmpty } from 'lodash'; import moment from 'moment'; import React from 'react'; +import { EuiText } from '@elastic/eui'; import { HttpStart } from '../../../../../../src/core/public'; import { CUSTOM_LABEL, @@ -22,6 +22,7 @@ import { GetTooltipHoverInfoType, IExplorerFields, IField, + IQuery, } from '../../../../common/types/explorer'; import PPLService from '../../../services/requests/ppl'; import { DocViewRow, IDocType } from '../explorer/events_views'; @@ -39,7 +40,7 @@ export const getTrs = ( explorerFields: IField[], limit: number, setLimit: React.Dispatch>, - PAGE_SIZE: number, + pageSize: number, timeStampField: any, explorerFieldsFull: IExplorerFields, pplService: PPLService, @@ -65,10 +66,10 @@ export const getTrs = ( if (prevTrs.length >= docs.length) return prevTrs; // reset limit if no previous table rows - if (prevTrs.length === 0 && limit !== PAGE_SIZE) setLimit(PAGE_SIZE); + if (prevTrs.length === 0 && limit !== pageSize) setLimit(pageSize); const trs = prevTrs.slice(); - const upperLimit = Math.min(trs.length === 0 ? PAGE_SIZE : limit, docs.length); + const upperLimit = Math.min(trs.length === 0 ? pageSize : limit, docs.length); const tempRefs = rowRefs; for (let i = trs.length; i < upperLimit; i++) { const docId = uniqueId('doc_view'); @@ -228,7 +229,7 @@ export const fetchSurroundingData = async ( await pplService .fetch({ query: finalQuery, format: 'jdbc' }) .then((res) => { - const resuleData = typeOfDocs == 'new' ? res.jsonData.reverse() : res.jsonData; + const resuleData = typeOfDocs === 'new' ? res.jsonData.reverse() : res.jsonData; resultCount = resuleData.length; setEventsData(createTds(resuleData, selectedCols, getTds)); }) @@ -407,7 +408,7 @@ export const getDefaultVisConfig = (statsToken: statsChunk) => { const getSpanValue = (groupByToken: GroupByChunk) => { const timeUnitValue = TIME_INTERVAL_OPTIONS.find( - (time_unit) => time_unit.value === groupByToken?.span?.span_expression.time_unit + (timeUnit) => timeUnit.value === groupByToken?.span?.span_expression.time_unit )?.text; return !isEmpty(groupByToken?.span) ? { @@ -429,3 +430,32 @@ const getSpanValue = (groupByToken: GroupByChunk) => { } : undefined; }; + +/** + * Use startTime and endTime as date range if both exists, else use selectDatarange + * in query state, if this is also empty then use default 15mins as default date range + * @param startTime + * @param endTime + * @param queryState + * @returns [startTime, endTime] + */ +export const getDateRange = ( + startTime: string | undefined, + endTime: string | undefined, + queryState: IQuery +) => { + if (startTime && endTime) return [startTime, endTime]; + const { selectedDateRange } = queryState; + if (!isEmpty(selectedDateRange)) return [selectedDateRange[0], selectedDateRange[1]]; + return ['now-15m', 'now']; +}; + +export const getContentTabTitle = (tabID: string, tabTitle: string) => { + return ( + <> + + {tabTitle} + + + ); +}; diff --git a/public/components/metrics/redux/slices/metrics_slice.ts b/public/components/metrics/redux/slices/metrics_slice.ts index a443a806c..3054b25b9 100644 --- a/public/components/metrics/redux/slices/metrics_slice.ts +++ b/public/components/metrics/redux/slices/metrics_slice.ts @@ -3,19 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { PPL_DATASOURCES_REQUEST, REDUX_SLICE_METRICS, + SAVED_VISUALIZATION, } from '../../../../../common/constants/metrics'; -import { - pplServiceRequestor, - getVisualizations, - getNewVizDimensions, - sortMetricLayout, -} from '../../helpers/utils'; -import PPLService from '../../../../services/requests/ppl'; import { MetricType } from '../../../../../common/types/metrics'; +import PPLService from '../../../../services/requests/ppl'; +import { SavedObjectsActions } from '../../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { ObservabilitySavedVisualization } from '../../../../services/saved_objects/saved_object_client/types'; +import { getNewVizDimensions, pplServiceRequestor, sortMetricLayout } from '../../helpers/utils'; const initialState = { pplService: PPLService, @@ -34,9 +32,11 @@ export const loadMetrics = createAsyncThunk('metrics/loadData', async (services: }); const fetchCustomMetrics = async (http: any) => { - const dataSet = await getVisualizations(http); + const dataSet = await SavedObjectsActions.getBulk({ + objectType: [SAVED_VISUALIZATION], + }); const savedMetrics = dataSet.observabilityObjectList.filter( - (obj: any) => obj.savedVisualization.sub_type === 'metric' + (obj) => obj.savedVisualization.sub_type === 'metric' ); const normalizedData = savedMetrics.map((obj: any) => ({ id: obj.objectId, diff --git a/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap b/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap index 1a271b63c..c4fcb663b 100644 --- a/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap +++ b/public/components/metrics/sidebar/__tests__/__snapshots__/searchbar.test.tsx.snap @@ -12,7 +12,7 @@ exports[`Search Bar Component Search Side Bar Component with available metrics 1 } } > -
    @@ -145,7 +145,7 @@ exports[`Search Bar Component Search Side Bar Component with available metrics 1
    -
    + `; @@ -161,7 +161,7 @@ exports[`Search Bar Component Search Side Bar Component with no available metric } } > -
    @@ -290,6 +290,6 @@ exports[`Search Bar Component Search Side Bar Component with no available metric
    -
    + `; diff --git a/public/components/metrics/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap b/public/components/metrics/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap index b4ba2e2b7..7ce02dc76 100644 --- a/public/components/metrics/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap +++ b/public/components/metrics/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap @@ -12,7 +12,7 @@ exports[`Side Bar Component renders Side Bar Component 1`] = ` } } > - -
    - + @@ -222,7 +222,7 @@ exports[`Side Bar Component renders Side Bar Component 1`] = ` className="euiSpacer euiSpacer--s" /> - - + @@ -333,7 +333,7 @@ exports[`Side Bar Component renders Side Bar Component 1`] = ` className="euiSpacer euiSpacer--s" /> - - + - + `; diff --git a/public/components/metrics/top_menu/__tests__/__snapshots__/metrics_export_panel.test.tsx.snap b/public/components/metrics/top_menu/__tests__/__snapshots__/metrics_export_panel.test.tsx.snap index f9101b0ea..2801c6f67 100644 --- a/public/components/metrics/top_menu/__tests__/__snapshots__/metrics_export_panel.test.tsx.snap +++ b/public/components/metrics/top_menu/__tests__/__snapshots__/metrics_export_panel.test.tsx.snap @@ -12,7 +12,7 @@ exports[`Export Metrics Panel Component renders Export Metrics Panel Component 1 } } > - - + `; diff --git a/public/components/metrics/top_menu/__tests__/__snapshots__/top_menu.test.tsx.snap b/public/components/metrics/top_menu/__tests__/__snapshots__/top_menu.test.tsx.snap index edf4f4490..1e637705f 100644 --- a/public/components/metrics/top_menu/__tests__/__snapshots__/top_menu.test.tsx.snap +++ b/public/components/metrics/top_menu/__tests__/__snapshots__/top_menu.test.tsx.snap @@ -12,7 +12,7 @@ exports[`Metrics Top Menu Component renders Top Menu Component when disabled in } } > - - +
    -
    + @@ -1338,7 +1338,7 @@ exports[`Metrics Top Menu Component renders Top Menu Component when disabled in -
    + `; @@ -1354,7 +1354,7 @@ exports[`Metrics Top Menu Component renders Top Menu Component when disabled wit } } > - - +
    -
    + @@ -2570,7 +2570,7 @@ exports[`Metrics Top Menu Component renders Top Menu Component when disabled wit -
    + `; @@ -2586,7 +2586,7 @@ exports[`Metrics Top Menu Component renders Top Menu Component when enabled 1`] } } > - - +
    -
    + @@ -3794,6 +3794,6 @@ exports[`Metrics Top Menu Component renders Top Menu Component when enabled 1`] -
    + `; diff --git a/public/components/metrics/view/__tests__/__snapshots__/empty_view.test.tsx.snap b/public/components/metrics/view/__tests__/__snapshots__/empty_view.test.tsx.snap index c9427636e..b8a327d4b 100644 --- a/public/components/metrics/view/__tests__/__snapshots__/empty_view.test.tsx.snap +++ b/public/components/metrics/view/__tests__/__snapshots__/empty_view.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Empty View Component renders empty view container without metrics 1`] = ` - +
    -
    + `; diff --git a/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap b/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap index 381df4aae..b8e25d8b3 100644 --- a/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap +++ b/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap @@ -12,48 +12,213 @@ exports[`Metrics Grid Component renders Metrics Grid Component 1`] = ` } } > - - - + - - + - - + - + `; diff --git a/public/components/notebooks/components/paragraph_components/paragraphs.tsx b/public/components/notebooks/components/paragraph_components/paragraphs.tsx index 7926ef263..333cb90de 100644 --- a/public/components/notebooks/components/paragraph_components/paragraphs.tsx +++ b/public/components/notebooks/components/paragraph_components/paragraphs.tsx @@ -21,6 +21,7 @@ import { EuiText, htmlIdGenerator, } from '@elastic/eui'; +import _ from 'lodash'; import moment from 'moment'; import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; @@ -37,11 +38,11 @@ import { } from '../../../../../common/constants/shared'; import { ParaType } from '../../../../../common/types/notebooks'; import { uiSettingsService } from '../../../../../common/utils'; +import PPLService from '../../../../services/requests/ppl'; +import { SavedObjectsActions } from '../../../../services/saved_objects/saved_object_client/saved_objects_actions'; +import { ObservabilitySavedVisualization } from '../../../../services/saved_objects/saved_object_client/types'; import { ParaInput } from './para_input'; import { ParaOutput } from './para_output'; -import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; -import PPLService from '../../../../services/requests/ppl'; -import _ from 'lodash'; /* * "Paragraphs" component is used to render cells of the notebook open and "add para div" between paragraphs @@ -67,7 +68,7 @@ import _ from 'lodash'; * Cell component of nteract used as a container for paragraphs in notebook UI. * https://components.nteract.io/#cell */ -type ParagraphProps = { +interface ParagraphProps { pplService: PPLService; para: ParaType; setPara: (para: ParaType) => void; @@ -89,7 +90,7 @@ type ParagraphProps = { movePara: (index: number, targetIndex: number) => void; showQueryParagraphError: boolean; queryParagraphErrorMessage: string; -}; +} export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { const { @@ -137,17 +138,17 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { }) .catch((err) => console.error('Fetching dashboard visualization issue', err.body.message)); - await http - .get(`${CUSTOM_PANELS_API_PREFIX}/visualizations`) + await SavedObjectsActions.getBulk({ + objectType: ['savedVisualization'], + }) .then((res) => { - const noAppVisualizations = res.visualizations.filter((vis) => { - return !!!vis.application_id; - }); - opt2 = noAppVisualizations.map((vizObject) => ({ - label: vizObject.name, - key: vizObject.id, - className: 'OBSERVABILITY_VISUALIZATION', - })); + opt2 = res.observabilityObjectList + .filter((visualization) => !visualization.savedVisualization.application_id) + .map((visualization) => ({ + label: visualization.savedVisualization.name, + key: visualization.objectId, + className: 'OBSERVABILITY_VISUALIZATION', + })); }) .catch((err) => console.error('Fetching observability visualization issue', err.body.message) @@ -225,7 +226,7 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { setRunParaError(true); return; } - let newVisObjectInput = undefined; + let newVisObjectInput; if (para.isVizualisation) { const inputTemp = createDashboardVizObject(selectedVisOption[0].key); setVisInput(inputTemp); @@ -270,7 +271,7 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { return paraOutput; } - const renderParaHeader = (type: string, index: number) => { + const renderParaHeader = (type: string, idx: number) => { const panels: EuiContextMenuPanelDescriptor[] = [ { id: 0, @@ -293,48 +294,48 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { }, { name: 'Move up', - disabled: index === 0, + disabled: idx === 0, onClick: () => { setIsPopoverOpen(false); - props.movePara(index, index - 1); + props.movePara(idx, idx - 1); }, }, { name: 'Move to top', - disabled: index === 0, + disabled: idx === 0, onClick: () => { setIsPopoverOpen(false); - props.movePara(index, 0); + props.movePara(idx, 0); }, }, { name: 'Move down', - disabled: index === props.paraCount - 1, + disabled: idx === props.paraCount - 1, onClick: () => { setIsPopoverOpen(false); - props.movePara(index, index + 1); + props.movePara(idx, idx + 1); }, }, { name: 'Move to bottom', - disabled: index === props.paraCount - 1, + disabled: idx === props.paraCount - 1, onClick: () => { setIsPopoverOpen(false); - props.movePara(index, props.paraCount - 1); + props.movePara(idx, props.paraCount - 1); }, }, { name: 'Duplicate', onClick: () => { setIsPopoverOpen(false); - props.clonePara(para, index + 1); + props.clonePara(para, idx + 1); }, }, { name: 'Delete', onClick: () => { setIsPopoverOpen(false); - props.deletePara(para, index); + props.deletePara(para, idx); }, }, ], @@ -347,14 +348,14 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { name: 'Code block', onClick: () => { setIsPopoverOpen(false); - props.addPara(index, '', 'CODE'); + props.addPara(idx, '', 'CODE'); }, }, { name: 'Visualization', onClick: () => { setIsPopoverOpen(false); - props.addPara(index, '', 'VISUALIZATION'); + props.addPara(idx, '', 'VISUALIZATION'); }, }, ], @@ -367,14 +368,14 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { name: 'Code block', onClick: () => { setIsPopoverOpen(false); - props.addPara(index + 1, '', 'CODE'); + props.addPara(idx + 1, '', 'CODE'); }, }, { name: 'Visualization', onClick: () => { setIsPopoverOpen(false); - props.addPara(index + 1, '', 'VISUALIZATION'); + props.addPara(idx + 1, '', 'VISUALIZATION'); }, }, ], @@ -386,7 +387,7 @@ export const Paragraphs = forwardRef((props: ParagraphProps, ref) => { - {`[${index + 1}] ${type} `} + {`[${idx + 1}] ${type} `} { <> {renderParaHeader(!para.isVizualisation ? 'Code block' : 'Visualization', index)} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
    paragraphSelector(index)}> {para.isInputExpanded && ( <> diff --git a/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap b/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap index 452952c19..f7e00be9b 100644 --- a/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap +++ b/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap @@ -17,7 +17,47 @@ exports[`Dashboard component renders dashboard 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -44,6 +84,10 @@ exports[`Dashboard component renders dashboard 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -245,7 +289,47 @@ exports[`Dashboard component renders dashboard 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -272,6 +356,10 @@ exports[`Dashboard component renders dashboard 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -2293,7 +2381,47 @@ exports[`Dashboard component renders empty dashboard 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -2320,6 +2448,10 @@ exports[`Dashboard component renders empty dashboard 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -2516,7 +2648,47 @@ exports[`Dashboard component renders empty dashboard 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -2543,6 +2715,10 @@ exports[`Dashboard component renders empty dashboard 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -4537,7 +4713,47 @@ exports[`Dashboard component renders empty jaeger dashboard 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -4564,6 +4780,10 @@ exports[`Dashboard component renders empty jaeger dashboard 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={false} @@ -4766,7 +4986,47 @@ exports[`Dashboard component renders empty jaeger dashboard 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -4793,6 +5053,10 @@ exports[`Dashboard component renders empty jaeger dashboard 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={false} diff --git a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap b/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap index 42291d3fa..25deb6fbc 100644 --- a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap +++ b/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap @@ -17,7 +17,47 @@ exports[`Services component renders empty services page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -44,6 +84,10 @@ exports[`Services component renders empty services page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -242,7 +286,47 @@ exports[`Services component renders empty services page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -269,6 +353,10 @@ exports[`Services component renders empty services page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -1824,7 +1912,47 @@ exports[`Services component renders jaeger services page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -1851,6 +1979,10 @@ exports[`Services component renders jaeger services page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={false} @@ -2055,7 +2187,47 @@ exports[`Services component renders jaeger services page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -2082,6 +2254,10 @@ exports[`Services component renders jaeger services page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={false} @@ -3140,7 +3316,47 @@ exports[`Services component renders services page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -3167,6 +3383,10 @@ exports[`Services component renders services page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -3370,7 +3590,47 @@ exports[`Services component renders services page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -3397,6 +3657,10 @@ exports[`Services component renders services page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap index 1aeeab3f1..e0acd2e97 100644 --- a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap +++ b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap @@ -17,7 +17,47 @@ exports[`Traces component renders empty traces page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -44,6 +84,10 @@ exports[`Traces component renders empty traces page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -241,7 +285,47 @@ exports[`Traces component renders empty traces page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -268,6 +352,10 @@ exports[`Traces component renders empty traces page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -1290,7 +1378,47 @@ exports[`Traces component renders jaeger traces page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -1317,6 +1445,10 @@ exports[`Traces component renders jaeger traces page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={false} @@ -1520,7 +1652,47 @@ exports[`Traces component renders jaeger traces page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -1547,6 +1719,10 @@ exports[`Traces component renders jaeger traces page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={false} @@ -2594,7 +2770,47 @@ exports[`Traces component renders traces page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -2621,6 +2837,10 @@ exports[`Traces component renders traces page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} @@ -2823,7 +3043,47 @@ exports[`Traces component renders traces page 1`] = ` } chrome={ Object { + "addApplicationClass": [MockFunction], + "docTitle": Object { + "change": [MockFunction], + "reset": [MockFunction], + }, + "getApplicationClasses$": [MockFunction], + "getBadge$": [MockFunction], + "getBrand$": [MockFunction], + "getBreadcrumbs$": [MockFunction], + "getCustomNavLink$": [MockFunction], + "getHeaderComponent": [MockFunction], + "getHelpExtension$": [MockFunction], "getIsNavDrawerLocked$": [MockFunction], + "getIsVisible$": [MockFunction], + "navControls": Object { + "getCenter$": [MockFunction], + "getLeft$": [MockFunction], + "getRight$": [MockFunction], + "registerCenter": [MockFunction], + "registerLeft": [MockFunction], + "registerRight": [MockFunction], + }, + "navLinks": Object { + "enableForcedAppSwitcherNavigation": [MockFunction], + "get": [MockFunction], + "getAll": [MockFunction], + "getForceAppSwitcherNavigation$": [MockFunction], + "getNavLinks$": [MockFunction], + "has": [MockFunction], + "showOnly": [MockFunction], + "update": [MockFunction], + }, + "recentlyAccessed": Object { + "add": [MockFunction], + "get": [MockFunction], + "get$": [MockFunction], + }, + "removeApplicationClass": [MockFunction], + "setAppTitle": [MockFunction], + "setBadge": [MockFunction], + "setBrand": [MockFunction], "setBreadcrumbs": [MockFunction] { "calls": Array [ Array [ @@ -2850,6 +3110,10 @@ exports[`Traces component renders traces page 1`] = ` }, ], }, + "setCustomNavLink": [MockFunction], + "setHelpExtension": [MockFunction], + "setHelpSupportUrl": [MockFunction], + "setIsVisible": [MockFunction], } } dataPrepperIndicesExist={true} diff --git a/public/components/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap b/public/components/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap index 3b213ff7d..4e61b4176 100644 --- a/public/components/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap +++ b/public/components/visualizations/assets/__tests__/__snapshots__/assets.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Assets components Renders lens icon of bar component 1`] = ` - + - + `; exports[`Assets components Renders lens icon of horizontal bar component 1`] = ` - + - + `; exports[`Assets components Renders lens icon of line component 1`] = ` - + - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap index 93f6a7ccc..4b8f4303a 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/bar.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Veritcal Bar component Renders veritcal bar component 1`] = ` - - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap index 26727a324..893fbd762 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/gauge.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Gauge component Renders gauge component 1`] = ` - -
    - - + + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap index 7095291d5..59917f21b 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/heatmap.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Heatmap component Renders heatmap component 1`] = ` - - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap index a215f4213..3b55f2c74 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/histogram.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Histogram component Renders histogram component 1`] = ` - - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap index 26af055f7..113f9b68b 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/horizontal_bar.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Horizontal bar component Renders horizontal bar component 1`] = ` - - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap index f692a6f0f..f5cd8f9cb 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/line.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Line component Renders line component 1`] = ` - - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap index 551cc8879..87fa48258 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/metrics.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Metrics component Renders Metrics component 1`] = ` - - - - + + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap index 4c99f9044..4465f321e 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/pie.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Pie component Renders pie component 1`] = ` - - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/text.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/text.test.tsx.snap index b95d9a886..6c40f4dfe 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/text.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/text.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Text component Renders text component 1`] = ` - - + `; diff --git a/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap b/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap index 787f386a5..4081a0419 100644 --- a/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap +++ b/public/components/visualizations/charts/__tests__/__snapshots__/treemap.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Treemap component Renders treemap component 1`] = ` - - + `; diff --git a/public/components/visualizations/saved_object_visualization.tsx b/public/components/visualizations/saved_object_visualization.tsx new file mode 100644 index 000000000..1b30e9d76 --- /dev/null +++ b/public/components/visualizations/saved_object_visualization.tsx @@ -0,0 +1,103 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import _ from 'lodash'; +import React, { useEffect, useState } from 'react'; +import { SizeMe } from 'react-sizeme'; +import { Filter, Query, TimeRange } from '../../../../../src/plugins/data/common'; +import { QueryManager } from '../../../common/query_manager'; +import { IVisualizationContainerProps, SavedVisualization } from '../../../common/types/explorer'; +import { getPPLService, preprocessQuery, removeBacktick } from '../../../common/utils'; +import { getDefaultVisConfig } from '../event_analytics/utils'; +import { getVizContainerProps } from './charts/helpers'; +import { Visualization } from './visualization'; + +interface SavedObjectVisualizationProps { + savedVisualization: SavedVisualization; + timeRange?: TimeRange; + filters?: Filter[]; + query?: Query; + whereClause?: string; +} + +/** + * Renders a visualization from a {@link SavedVisualization}. + */ +export const SavedObjectVisualization: React.FC = (props) => { + const [visContainerProps, setVisContainerProps] = useState(); + + useEffect(() => { + const pplService = getPPLService(); + const metaData = { ...props.savedVisualization, query: props.savedVisualization.query }; + const userConfigs = metaData.user_configs ? JSON.parse(metaData.user_configs) : {}; + const dataConfig = { ...(userConfigs.dataConfig || {}) }; + const hasBreakdowns = !_.isEmpty(dataConfig.breakdowns); + const realTimeParsedStats = { + ...getDefaultVisConfig(new QueryManager().queryParser().parse(metaData.query).getStats()), + }; + let finalDimensions = [...(realTimeParsedStats.dimensions || [])]; + const breakdowns = [...(dataConfig.breakdowns || [])]; + + // filter out breakdowns from dimnesions + if (hasBreakdowns) { + finalDimensions = _.differenceWith(finalDimensions, breakdowns, (dimn, brkdwn) => + _.isEqual(removeBacktick(dimn.name), removeBacktick(brkdwn.name)) + ); + } + + const finalDataConfig = { + ...dataConfig, + ...realTimeParsedStats, + dimensions: finalDimensions, + breakdowns, + }; + + const mixedUserConfigs = { + availabilityConfig: { + ...(userConfigs.availabilityConfig || {}), + }, + dataConfig: { + ...finalDataConfig, + }, + layoutConfig: { + ...(userConfigs.layoutConfig || {}), + }, + }; + + let query = metaData.query; + + if (props.timeRange) { + query = preprocessQuery({ + rawQuery: metaData.query, + startTime: props.timeRange.from, + endTime: props.timeRange.to, + timeField: props.savedVisualization.selected_timestamp.name, + isLiveQuery: false, + whereClause: props.whereClause, + }); + } + + pplService + .fetch({ query, format: 'jdbc' }) + .then((data) => { + const container = getVizContainerProps({ + vizId: props.savedVisualization.type, + rawVizData: data, + query: { rawQuery: metaData.query }, + indexFields: {}, + userConfigs: mixedUserConfigs, + explorer: { explorerData: data, explorerFields: data.schema }, + }); + setVisContainerProps(container); + }) + .catch((error: Error) => { + console.error(error); + }); + }, [props]); + + return visContainerProps ? ( + {({ size }) => } + ) : null; +}; diff --git a/public/embeddable/filters/filter_parser.ts b/public/embeddable/filters/filter_parser.ts new file mode 100644 index 000000000..15f7f8a24 --- /dev/null +++ b/public/embeddable/filters/filter_parser.ts @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Filter, getFilterField } from '../../../../../src/plugins/data/common'; + +/** + * Parse core {@link Filter} and convert to a PPL where clause. Only supports + * non DSL filters. + */ +export const parseFilters = (filters?: Filter[]) => { + if (!filters) return ''; + return filters + .filter((filter) => !filter.meta.disabled) + .map(parseFilter) + .join(' AND '); +}; + +const parseFilter = (filter: Filter): string => { + const meta = filter.meta; + const field = getFilterField(filter); + if (!meta.negate) { + switch (meta.type) { + case 'phrase': + return `\`${field}\` = '${meta.params.query}'`; + case 'phrases': + return meta.params.map((query: string) => `\`${field}\` = '${query}'`).join(' OR '); + case 'range': + const ranges = []; + if (meta.params.gte != null) ranges.push(`\`${field}\` >= ${meta.params.gte}`); + if (meta.params.lt != null) ranges.push(`\`${field}\` < ${meta.params.lt}`); + return ranges.join(' AND '); + case 'exists': + return `isnotnull(\`${field}\`)`; + } + } else { + switch (meta.type) { + case 'phrase': + return `\`${field}\` != '${meta.params.query}'`; + case 'phrases': + return meta.params.map((query: string) => `\`${field}\` != '${query}'`).join(' AND '); + case 'range': + const ranges = []; + if (meta.params.gte != null) ranges.push(`\`${field}\` < ${meta.params.gte}`); + if (meta.params.lt != null) ranges.push(`\`${field}\` >= ${meta.params.lt}`); + return ranges.join(' OR '); + case 'exists': + return `isnotnull(\`${field}\`)`; + } + } + return ''; +}; diff --git a/public/embeddable/observability_embeddable.tsx b/public/embeddable/observability_embeddable.tsx new file mode 100644 index 000000000..49fbb75cb --- /dev/null +++ b/public/embeddable/observability_embeddable.tsx @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Subscription } from 'rxjs'; +import { AttributeService } from '../../../../src/plugins/dashboard/public'; +import { + Embeddable, + EmbeddableOutput, + IContainer, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { + VisualizationSavedObjectAttributes, + VISUALIZATION_SAVED_OBJECT, +} from '../../common/types/observability_saved_object_attributes'; +import { ObservabilityEmbeddableComponent } from './observability_embeddable_component'; + +// this needs to match the saved object type for the clone and replace panel actions to work +export const OBSERVABILITY_EMBEDDABLE = VISUALIZATION_SAVED_OBJECT; +export const OBSERVABILITY_EMBEDDABLE_ID = 'observability-ppl'; +export const OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME = 'PPL'; +export const OBSERVABILITY_EMBEDDABLE_DESCRIPTION = + 'Create a visualization with Piped Processing Language (PPL). PPL can query data in your indices and also supports federated data sources like Prometheus.'; +export const OBSERVABILITY_EMBEDDABLE_ICON = 'visQueryPPL'; + +export interface ObservabilityOutput extends EmbeddableOutput { + /** + * Will contain the saved object attributes of the Observability Saved Object that matches + * `input.savedObjectId`. If the id is invalid, this may be undefined. + */ + attributes?: VisualizationSavedObjectAttributes; +} + +type ObservabilityEmbeddableConfig = Required< + Pick +>; + +export class ObservabilityEmbeddable extends Embeddable< + SavedObjectEmbeddableInput, + ObservabilityOutput +> { + public readonly type = OBSERVABILITY_EMBEDDABLE; + private subscription: Subscription; + private node?: HTMLElement; + public savedObjectId?: string; + private attributes?: VisualizationSavedObjectAttributes; + + constructor( + config: ObservabilityEmbeddableConfig, + initialInput: SavedObjectEmbeddableInput, + private attributeService: AttributeService, + { + parent, + }: { + parent?: IContainer; + } + ) { + super(initialInput, { editable: true, ...config }, parent); + + this.subscription = this.getInput$().subscribe(async () => { + this.savedObjectId = this.getInput().savedObjectId; + this.reload(); + }); + } + + public render(node: HTMLElement) { + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + this.node = node; + ReactDOM.render(, node); + } + + public async reload() { + this.attributes = await this.attributeService.unwrapAttributes(this.input); + + this.updateOutput({ + attributes: this.attributes, + title: this.input.title || this.attributes.title, + defaultTitle: this.attributes.title, + }); + } + + public destroy() { + super.destroy(); + this.subscription.unsubscribe(); + if (this.node) { + ReactDOM.unmountComponentAtNode(this.node); + } + } +} diff --git a/public/embeddable/observability_embeddable_component.tsx b/public/embeddable/observability_embeddable_component.tsx new file mode 100644 index 000000000..276539be8 --- /dev/null +++ b/public/embeddable/observability_embeddable_component.tsx @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + SavedObjectEmbeddableInput, + withEmbeddableSubscription, +} from '../../../../src/plugins/embeddable/public'; +import { SavedObjectVisualization } from '../components/visualizations/saved_object_visualization'; +import { parseFilters } from './filters/filter_parser'; +import { ObservabilityEmbeddable, ObservabilityOutput } from './observability_embeddable'; + +interface ObservabilityEmbeddableComponentProps { + input: SavedObjectEmbeddableInput; + output: ObservabilityOutput; + embeddable: ObservabilityEmbeddable; +} + +const ObservabilityEmbeddableComponentInner: React.FC = ( + props +) => { + const visualization = props.output.attributes?.savedVisualization; + return visualization ? ( + + ) : null; +}; + +export const ObservabilityEmbeddableComponent = withEmbeddableSubscription< + SavedObjectEmbeddableInput, + ObservabilityOutput, + ObservabilityEmbeddable, + {} +>(ObservabilityEmbeddableComponentInner); diff --git a/public/embeddable/observability_embeddable_factory.tsx b/public/embeddable/observability_embeddable_factory.tsx new file mode 100644 index 000000000..53e7b7859 --- /dev/null +++ b/public/embeddable/observability_embeddable_factory.tsx @@ -0,0 +1,144 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + OverlayStart, + SavedObjectsClientContract, + SimpleSavedObject, +} from '../../../../src/core/public'; +import { AttributeService, DashboardStart } from '../../../../src/plugins/dashboard/public'; +import { + EmbeddableFactoryDefinition, + EmbeddableOutput, + IContainer, + SavedObjectEmbeddableInput, +} from '../../../../src/plugins/embeddable/public'; +import { + checkForDuplicateTitle, + OnSaveProps, + SavedObjectMetaData, +} from '../../../../src/plugins/saved_objects/public'; +import { observabilityID } from '../../common/constants/shared'; +import { + VisualizationSavedObjectAttributes, + VISUALIZATION_SAVED_OBJECT, +} from '../../common/types/observability_saved_object_attributes'; +import { + ObservabilityEmbeddable, + ObservabilityOutput, + OBSERVABILITY_EMBEDDABLE, + OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME, + OBSERVABILITY_EMBEDDABLE_ICON, +} from './observability_embeddable'; + +interface StartServices { + getAttributeService: DashboardStart['getAttributeService']; + savedObjectsClient: SavedObjectsClientContract; + overlays: OverlayStart; +} + +export class ObservabilityEmbeddableFactoryDefinition + implements + EmbeddableFactoryDefinition< + SavedObjectEmbeddableInput, + ObservabilityOutput | EmbeddableOutput, + ObservabilityEmbeddable, + VisualizationSavedObjectAttributes + > { + public readonly type = OBSERVABILITY_EMBEDDABLE; + public readonly savedObjectMetaData: SavedObjectMetaData = { + name: OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME, + includeFields: [], + type: VISUALIZATION_SAVED_OBJECT, // saved object type for finding embeddables in Dashboard + getIconForSavedObject: () => OBSERVABILITY_EMBEDDABLE_ICON, + }; + private attributeService?: AttributeService; + + constructor(private getStartServices: () => Promise) {} + + async createFromSavedObject( + savedObjectId: string, + input: SavedObjectEmbeddableInput, + parent?: IContainer + ) { + const editPath = `#/event_analytics/explorer/${VISUALIZATION_SAVED_OBJECT}:${savedObjectId}`; + const editUrl = `/app/${observabilityID}${editPath}`; + return new ObservabilityEmbeddable( + { + editUrl, + editPath, + editApp: observabilityID, + }, + input, + await this.getAttributeService(), + { parent } + ); + } + + public canCreateNew() { + return false; + } + + async create(_initialInput: SavedObjectEmbeddableInput, _parent?: IContainer) { + return undefined; + } + + async isEditable() { + return true; + } + + getDisplayName() { + return OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME; + } + + private async saveMethod(attributes: VisualizationSavedObjectAttributes, savedObjectId?: string) { + const { savedObjectsClient } = await this.getStartServices(); + if (savedObjectId) { + return savedObjectsClient.update(this.type, savedObjectId, attributes); + } + return savedObjectsClient.create(this.type, attributes); + } + + private async unwrapMethod(savedObjectId: string): Promise { + const { savedObjectsClient } = await this.getStartServices(); + const savedObject: SimpleSavedObject = await savedObjectsClient.get< + VisualizationSavedObjectAttributes + >(this.type, savedObjectId); + return { ...savedObject.attributes }; + } + + private async checkForDuplicateTitleMethod(props: OnSaveProps): Promise { + const start = await this.getStartServices(); + const { savedObjectsClient, overlays } = start; + return checkForDuplicateTitle( + { + title: props.newTitle, + copyOnSave: false, + lastSavedTitle: '', + getOpenSearchType: () => this.type, + getDisplayName: this.getDisplayName || (() => this.type), + }, + props.isTitleDuplicateConfirmed, + props.onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } + + private async getAttributeService() { + if (!this.attributeService) { + this.attributeService = (await this.getStartServices()).getAttributeService< + VisualizationSavedObjectAttributes + >(this.type, { + saveMethod: this.saveMethod.bind(this), + unwrapMethod: this.unwrapMethod.bind(this), + checkForDuplicateTitle: this.checkForDuplicateTitleMethod.bind(this), + }); + } + return this.attributeService!; + } +} diff --git a/public/plugin.ts b/public/plugin.ts index 1610c0dbf..e9b2815d6 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -3,26 +3,58 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.scss'; - import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; +import { CREATE_TAB_PARAM, CREATE_TAB_PARAM_KEY, TAB_CHART_ID } from '../common/constants/explorer'; import { observabilityID, observabilityPluginOrder, observabilityTitle, } from '../common/constants/shared'; -import PPLService from './services/requests/ppl'; -import DSLService from './services/requests/dsl'; -import TimestampUtils from './services/timestamp/timestamp'; -import SavedObjects from './services/saved_objects/event_analytics/saved_objects'; -import { AppPluginStartDependencies, ObservabilitySetup, ObservabilityStart } from './types'; +import { QueryManager } from '../common/query_manager'; +import { VISUALIZATION_SAVED_OBJECT } from '../common/types/observability_saved_object_attributes'; +import { + setOSDHttp, + setOSDSavedObjectsClient, + setPPLService, + uiSettingsService, +} from '../common/utils'; import { convertLegacyNotebooksUrl } from './components/notebooks/components/helpers/legacy_route_helpers'; import { convertLegacyTraceAnalyticsUrl } from './components/trace_analytics/components/common/legacy_route_helpers'; -import { uiSettingsService } from '../common/utils'; -import { QueryManager } from '../common/query_manager'; -export class ObservabilityPlugin implements Plugin { - public setup(core: CoreSetup): ObservabilitySetup { +import { + OBSERVABILITY_EMBEDDABLE, + OBSERVABILITY_EMBEDDABLE_DESCRIPTION, + OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME, + OBSERVABILITY_EMBEDDABLE_ICON, + OBSERVABILITY_EMBEDDABLE_ID, +} from './embeddable/observability_embeddable'; +import { ObservabilityEmbeddableFactoryDefinition } from './embeddable/observability_embeddable_factory'; +import './index.scss'; +import DSLService from './services/requests/dsl'; +import PPLService from './services/requests/ppl'; +import SavedObjects from './services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from './services/timestamp/timestamp'; +import { + AppPluginStartDependencies, + ObservabilitySetup, + ObservabilityStart, + SetupDependencies, +} from './types'; + +export class ObservabilityPlugin + implements + Plugin { + public setup( + core: CoreSetup, + setupDeps: SetupDependencies + ): ObservabilitySetup { uiSettingsService.init(core.uiSettings, core.notifications); + const pplService = new PPLService(core.http); + const qm = new QueryManager(); + setPPLService(pplService); + setOSDHttp(core.http); + core.getStartServices().then(([coreStart]) => { + setOSDSavedObjectsClient(coreStart.savedObjects.client); + }); // redirect legacy notebooks URL to current URL under observability if (window.location.pathname.includes('notebooks-dashboards')) { @@ -46,14 +78,12 @@ export class ObservabilityPlugin implements Plugin ({ + getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, + savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, + overlays: (await core.getStartServices())[0].overlays, + })); + setupDeps.embeddable.registerEmbeddableFactory(OBSERVABILITY_EMBEDDABLE, embeddableFactory); + + setupDeps.visualizations.registerAlias({ + name: OBSERVABILITY_EMBEDDABLE_ID, + title: OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME, + description: OBSERVABILITY_EMBEDDABLE_DESCRIPTION, + icon: OBSERVABILITY_EMBEDDABLE_ICON, + aliasApp: observabilityID, + aliasPath: `#/event_analytics/explorer/?${CREATE_TAB_PARAM_KEY}=${CREATE_TAB_PARAM[TAB_CHART_ID]}`, + stage: 'production', + appExtensions: { + visualizations: { + docTypes: [VISUALIZATION_SAVED_OBJECT], + toListItem: ({ id, attributes, updated_at: updatedAt }) => ({ + description: attributes?.description, + editApp: observabilityID, + editUrl: `#/event_analytics/explorer/${VISUALIZATION_SAVED_OBJECT}:${id}`, + icon: OBSERVABILITY_EMBEDDABLE_ICON, + id, + savedObjectType: VISUALIZATION_SAVED_OBJECT, + title: attributes?.title, + typeTitle: OBSERVABILITY_EMBEDDABLE_DISPLAY_NAME, + stage: 'production', + updated_at: updatedAt, + }), + }, + }, + }); + // Return methods that should be available to other plugins return {}; } diff --git a/public/services/data_fetchers/fetch_interface.ts b/public/services/data_fetchers/fetch_interface.ts new file mode 100644 index 000000000..3bf4589c9 --- /dev/null +++ b/public/services/data_fetchers/fetch_interface.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface IDataFetcher { + search: () => void; +} diff --git a/public/services/data_fetchers/fetcher_base.ts b/public/services/data_fetchers/fetcher_base.ts new file mode 100644 index 000000000..81a0464e0 --- /dev/null +++ b/public/services/data_fetchers/fetcher_base.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IDataFetcher } from './fetch_interface'; + +export abstract class DataFetcherBase implements IDataFetcher { + constructor() {} + abstract search(): void; +} diff --git a/public/services/data_fetchers/ppl/ppl_data_fetcher.ts b/public/services/data_fetchers/ppl/ppl_data_fetcher.ts new file mode 100644 index 000000000..e83b45dc7 --- /dev/null +++ b/public/services/data_fetchers/ppl/ppl_data_fetcher.ts @@ -0,0 +1,208 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isEmpty } from 'lodash'; +import { IDefaultTimestampState, IQuery } from '../../../../common/types/explorer'; +import { IDataFetcher } from '../fetch_interface'; +import { DataFetcherBase } from '../fetcher_base'; +import { composeFinalQuery, getIndexPatternFromRawQuery } from '../../../../common/utils'; +import { + FILTERED_PATTERN, + PATTERNS_REGEX, + PATTERN_REGEX, + RAW_QUERY, + SELECTED_DATE_RANGE, + SELECTED_PATTERN_FIELD, + SELECTED_TIMESTAMP, + TAB_CHART_ID, +} from '../../../../common/constants/explorer'; +import { PPL_STATS_REGEX } from '../../../../common/constants/shared'; + +export class PPLDataFetcher extends DataFetcherBase implements IDataFetcher { + protected queryIndex: string; + protected timestamp!: string; + constructor( + protected readonly query: IQuery, + protected readonly storeContext, + protected readonly searchContext, + protected readonly searchParams, + protected readonly notifications + ) { + super(); + // index/index patterns for this search + this.queryIndex = this.getIndex(this.searchParams.query); + } + + async setTimestamp(index: string) { + try { + const defaultTimestamp = await this.getTimestamp(index); + this.timestamp = defaultTimestamp.default_timestamp || ''; + // schema conflicts for multiple indexes + if (defaultTimestamp.hasSchemaConflict) { + this.notifications.toasts.addError({ + title: 'Schema conflicts', + text: `Schema conflicts detected while fetching default timestamp, ${defaultTimestamp.message}`, + }); + } + } catch (error) { + this.notifications.toasts.addError(error, { + title: 'Unable to get default timestamp', + }); + } + } + + async search() { + const { + query, + appBaseQuery, + startingTime, + endingTime, + isLiveTailOn, + selectedInterval, + } = this.searchParams; + + if (isEmpty(query)) return; + + const { + tabId, + findAutoInterval, + getCountVisualizations, + getLiveTail, + getEvents, + getErrorHandler, + getPatterns, + getAvailableFields, + } = this.searchContext; + const { dispatch, changeQuery } = this.storeContext; + + await this.processTimestamp(query); + if (isEmpty(this.timestamp)) return; + + const curStartTime = startingTime || this.query[SELECTED_DATE_RANGE][0]; + const curEndTime = endingTime || this.query[SELECTED_DATE_RANGE][1]; + + // compose final query + const finalQuery = composeFinalQuery( + this.query[RAW_QUERY], + curStartTime, + curEndTime, + this.timestamp, + isLiveTailOn, + appBaseQuery, + this.query[SELECTED_PATTERN_FIELD], + this.query[PATTERN_REGEX], + this.query[FILTERED_PATTERN] + ); + + // update UI with new query state + await this.updateQueryState(this.query[RAW_QUERY], finalQuery, this.timestamp); + // calculate proper time interval for count distribution + if (!selectedInterval.current || selectedInterval.current.text === 'Auto') { + findAutoInterval(curStartTime, curEndTime); + } + + // get query data + if (isLiveTailOn) { + getLiveTail(finalQuery, getErrorHandler('Error fetching events')); + } else { + getEvents(finalQuery, getErrorHandler('Error fetching events')); + } + // still need all fields when query contains stats + if (finalQuery.match(PPL_STATS_REGEX)) getAvailableFields(`search source=${this.queryIndex}`); + getCountVisualizations(selectedInterval.current.value.replace(/^auto_/, '')); + // patterns + this.setLogPattern(this.query, this.queryIndex, finalQuery); + if (!finalQuery.match(PATTERNS_REGEX)) { + getPatterns(selectedInterval.current.value.replace(/^auto_/, '')); + } + + // live tail - for comparing usage if for the same tab, user changed index from one to another + if (!isLiveTailOn && !this.query.isLoaded) { + dispatch( + changeQuery({ + tabId, + query: { + isLoaded: true, + }, + }) + ); + } + } + + async setLogPattern(query: IQuery, index: string, finalQuery: string) { + const { getErrorHandler, setDefaultPatternsField } = this.searchContext; + // set pattern + if (isEmpty(query[SELECTED_PATTERN_FIELD])) { + await setDefaultPatternsField( + index, + '', + getErrorHandler('Error fetching default pattern field') + ); + } + + // check if above setDefaultPatternsField correctly gets valid patterns + if (isEmpty(query[SELECTED_PATTERN_FIELD])) { + this.notifications.toasts.addError({ + title: 'Invalid pattern field', + text: 'Index does not contain a valid pattern field.', + }); + return; + } + } + + async processTimestamp(query: IQuery) { + if (query[SELECTED_TIMESTAMP]) { + this.timestamp = query[SELECTED_TIMESTAMP]; + } else { + await this.setTimestamp(this.queryIndex); + } + } + + getIndex(query: string) { + return getIndexPatternFromRawQuery(query); + } + + async getTimestamp(indexPattern: string): Promise { + const { timestampUtils } = this.searchContext; + return await timestampUtils.getTimestamp(indexPattern); + } + + async updateQueryState(rawQuery: string, finalQuery: string, curTimestamp: string) { + const { batch, dispatch, changeQuery, changeVizConfig } = this.storeContext; + const { query } = this.searchParams; + const { + tabId, + curVisId, + selectedContentTabId, + queryManager, + getDefaultVisConfig, + } = this.searchContext; + + await batch(() => { + dispatch( + changeQuery({ + tabId, + query: { + finalQuery, + [RAW_QUERY]: query, + [SELECTED_TIMESTAMP]: curTimestamp, + }, + }) + ); + if (selectedContentTabId === TAB_CHART_ID) { + // parse stats section on every search + const statsTokens = queryManager.queryParser().parse(rawQuery).getStats(); + const updatedDataConfig = getDefaultVisConfig(statsTokens); + dispatch( + changeVizConfig({ + tabId, + vizId: curVisId, + data: { dataConfig: { ...updatedDataConfig } }, + }) + ); + } + }); + } +} diff --git a/public/services/saved_objects/event_analytics/saved_objects.ts b/public/services/saved_objects/event_analytics/saved_objects.ts index 0f207ab79..1741723f3 100644 --- a/public/services/saved_objects/event_analytics/saved_objects.ts +++ b/public/services/saved_objects/event_analytics/saved_objects.ts @@ -16,7 +16,7 @@ import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_pa const CONCAT_FIELDS = ['objectIdList', 'objectType']; -interface ISavedObjectRequestParams { +export interface ISavedObjectRequestParams { objectId?: string; objectIdList?: string[] | string; objectType?: string[] | string; @@ -150,28 +150,6 @@ export default class SavedObjects { ); } - async bulkUpdateSavedVisualization(params: IBulkUpdateSavedVisualizationRquest) { - const finalParams = this.buildRequestBody({ - query: params.query, - fields: params.fields, - dateRange: params.dateRange, - chartType: params.type, - name: params.name, - }); - - return await Promise.all( - params.savedObjectList.map((objectToUpdate) => { - finalParams.object_id = objectToUpdate.saved_object.objectId; - return this.http.put( - `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, - { - body: JSON.stringify(finalParams), - } - ); - }) - ); - } - async updateSavedVisualizationById(params: any) { const finalParams = this.buildRequestBody({ query: params.query, @@ -184,7 +162,7 @@ export default class SavedObjects { description: params.description, subType: params.subType, unitsOfMeasure: params.unitsOfMeasure, - selectedLabels: params.selectedLabels + selectedLabels: params.selectedLabels, }); finalParams.object_id = params.objectId; @@ -247,7 +225,7 @@ export default class SavedObjects { description: params.description, subType: params.subType, unitsOfMeasure: params.unitsOfMeasure, - selectedLabels: params.selectedLabels + selectedLabels: params.selectedLabels, }); return await this.http.post( @@ -258,40 +236,6 @@ export default class SavedObjects { ); } - async createSavedTimestamp(params: any) { - const finalParams = { - index: params.index, - name: params.name, - type: params.type, - dsl_type: params.dsl_type, - }; - - return await this.http.post( - `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/timestamp`, - { - body: JSON.stringify(finalParams), - } - ); - } - - async updateTimestamp(params: any) { - const finalParams = { - objectId: params.index, - timestamp: { - name: params.name, - index: params.index, - type: params.type, - dsl_type: params.dsl_type, - }, - }; - return await this.http.put( - `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/timestamp`, - { - body: JSON.stringify(finalParams), - } - ); - } - async deleteSavedObjectsList(deleteObjectRequest: any) { return await this.http.delete( `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/${deleteObjectRequest.objectIdList.join( @@ -299,6 +243,4 @@ export default class SavedObjects { )}` ); } - - deleteSavedObjectsByIdList(deleteObjectRequesList: any) {} } diff --git a/public/services/saved_objects/saved_object_client/client_base.ts b/public/services/saved_objects/saved_object_client/client_base.ts new file mode 100644 index 000000000..443c64c20 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/client_base.ts @@ -0,0 +1,17 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ISavedObjectsClient } from './client_interface'; +import { SavedObjectsCreateResponse, SavedObjectsGetResponse } from './types'; + +export abstract class SavedObjectClientBase implements ISavedObjectsClient { + abstract create(params: unknown): Promise; + abstract get(params: unknown): Promise; + abstract getBulk(params: unknown): Promise; + abstract update(params: unknown): Promise; + abstract updateBulk(params: unknown): Promise>>; + abstract delete(params: unknown): Promise; + abstract deleteBulk(params: unknown): Promise; +} diff --git a/public/services/saved_objects/saved_object_client/client_factory.ts b/public/services/saved_objects/saved_object_client/client_factory.ts new file mode 100644 index 000000000..4af366a83 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/client_factory.ts @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VISUALIZATION_SAVED_OBJECT } from '../../../../common/types/observability_saved_object_attributes'; +import { ISavedObjectsClient } from './client_interface'; +import { OSDSavedObjectClient } from './osd_saved_objects/osd_saved_object_client'; +import { OSDSavedVisualizationClient } from './osd_saved_objects/saved_visualization'; +import { PPLSavedQueryClient, PPLSavedVisualizationClient } from './ppl'; + +interface GetSavedObjectsClientOptions { + objectId: string; + objectType?: string; // only required for non OSD saved objects +} + +export const getSavedObjectsClient = ( + options: GetSavedObjectsClientOptions +): ISavedObjectsClient => { + const type = OSDSavedObjectClient.extractType(options.objectId); + + switch (type) { + case VISUALIZATION_SAVED_OBJECT: + return OSDSavedVisualizationClient.getInstance(); + + default: + break; + } + + switch (options.objectType) { + case 'savedVisualization': + return PPLSavedVisualizationClient.getInstance(); + + default: + return PPLSavedQueryClient.getInstance(); + } +}; diff --git a/public/services/saved_objects/saved_object_client/client_interface.ts b/public/services/saved_objects/saved_object_client/client_interface.ts new file mode 100644 index 000000000..bdf609dd5 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/client_interface.ts @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsCreateResponse, SavedObjectsGetResponse } from './types'; + +export interface ISavedObjectsClient { + create: (params: any) => Promise; + get: (params: any) => Promise; + getBulk: (params: any) => Promise; + update: (params: any) => Promise; + updateBulk: (params: any) => Promise; + delete: (params: any) => Promise; + deleteBulk: (params: any) => Promise; +} diff --git a/public/services/saved_objects/saved_object_client/osd_saved_objects/osd_saved_object_client.ts b/public/services/saved_objects/saved_object_client/osd_saved_objects/osd_saved_object_client.ts new file mode 100644 index 000000000..f0d0baa04 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/osd_saved_objects/osd_saved_object_client.ts @@ -0,0 +1,130 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isEmpty } from 'lodash'; +import { + SavedObjectsClientContract, + SimpleSavedObject, +} from '../../../../../../../src/core/public'; +import { OBSERVABILTY_SAVED_OBJECTS } from '../../../../../common/types/observability_saved_object_attributes'; +import { SavedObjectClientBase } from '../client_base'; +import { ObservabilitySavedObjectsType } from './types'; + +export abstract class OSDSavedObjectClient extends SavedObjectClientBase { + private static TYPE_ID_REGEX = new RegExp( + `(${OBSERVABILTY_SAVED_OBJECTS.join( + '|' + )}):([0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$)` + ); + + constructor(protected readonly client: SavedObjectsClientContract) { + super(); + } + + /** + * OSD saved object client operations requires object type. Type is part of + * the document id but not in operation response id (see + * https://github.com/opensearch-project/opensearch-dashboards/blob/11b98ec05483269917c335fcb1900bf98da8cac6/src/core/server/saved_objects/serialization/serializer.ts#L141). + * In Observability most components only uses id without explicit type. Prepend + * type to id to make it easier to call OSD saved object client. + * + * @param objectId - objectId in the format of id only + * @returns id in the format of 'SavedObjectType:id' + */ + protected abstract prependTypeToId(objectId: string): string; + + protected static extractTypeAndUUID( + objectId: string + ): { + type: '' | ObservabilitySavedObjectsType; + uuid: string; + } { + const matches = objectId.match(OSDSavedObjectClient.TYPE_ID_REGEX); + if (matches === null) { + return { type: '', uuid: objectId }; + } + return { type: matches[1] as ObservabilitySavedObjectsType, uuid: matches[2] }; + } + + /** + * @param objectId - objectId in the format of 'SavedObjectType:UUID' + * @returns ObservabilitySavedObjectsType or empty string if objectId + * is not in expected format. + */ + public static extractType(objectId: string) { + return this.extractTypeAndUUID(objectId).type; + } + + protected static convertToLastUpdatedMs(updatedAt: SimpleSavedObject['updated_at']) { + return (updatedAt ? new Date(updatedAt) : new Date()).getTime(); + } + + buildRequestBody({ + query, + fields, + dateRange, + timestamp, + name = '', + chartType = '', + description = '', + applicationId = '', + userConfigs = '', + subType = '', + unitsOfMeasure = '', + selectedLabels, + objectId = '', + }: any) { + const objRequest: any = { + object: { + query, + selected_date_range: { + start: dateRange[0] || 'now/15m', + end: dateRange[1] || 'now', + text: '', + }, + selected_timestamp: { + name: timestamp || '', + type: 'timestamp', + }, + selected_fields: { + tokens: fields, + text: '', + }, + name: name || '', + description: description || '', + }, + }; + + if (!isEmpty(chartType)) { + objRequest.object.type = chartType; + } + + if (!isEmpty(applicationId)) { + objRequest.object.application_id = applicationId; + } + + if (!isEmpty(userConfigs)) { + objRequest.object.user_configs = userConfigs; + } + + if (!isEmpty(subType)) { + objRequest.object.sub_type = subType; + } + + if (!isEmpty(unitsOfMeasure)) { + objRequest.object.units_of_measure = unitsOfMeasure; + } + + if (!isEmpty(selectedLabels)) { + objRequest.object.selected_labels = selectedLabels; + } + + if (!isEmpty(objectId)) { + objRequest.object_id = objectId; + } + + return objRequest; + } +} diff --git a/public/services/saved_objects/saved_object_client/osd_saved_objects/saved_visualization.ts b/public/services/saved_objects/saved_object_client/osd_saved_objects/saved_visualization.ts new file mode 100644 index 000000000..635199c54 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/osd_saved_objects/saved_visualization.ts @@ -0,0 +1,188 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsFindOptions } from '../../../../../../../src/core/public'; +import { IField } from '../../../../../common/types/explorer'; +import { + SAVED_OBJECT_VERSION, + VisualizationSavedObjectAttributes, + VISUALIZATION_SAVED_OBJECT, +} from '../../../../../common/types/observability_saved_object_attributes'; +import { getOSDSavedObjectsClient } from '../../../../../common/utils'; +import { + SavedObjectsDeleteBulkParams, + SavedObjectsDeleteParams, + SavedObjectsDeleteResponse, + SavedObjectsGetParams, + SavedObjectsGetResponse, +} from '../types'; +import { OSDSavedObjectClient } from './osd_saved_object_client'; +import { OSDSavedObjectCreateResponse, OSDSavedObjectUpdateResponse } from './types'; + +interface CommonParams { + query: string; + fields: IField[]; + dateRange: [string, string]; + type: string; + name: string; + timestamp: string; + applicationId: string; + userConfigs: any; + description: string; + subType: string; + unitsOfMeasure: string; + selectedLabels: string; +} + +type CreateParams = CommonParams & { applicationId: string }; +type UpdateParams = Partial & { objectId: string }; + +export class OSDSavedVisualizationClient extends OSDSavedObjectClient { + private static instance: OSDSavedVisualizationClient; + + protected prependTypeToId(objectId: string) { + return `${VISUALIZATION_SAVED_OBJECT}:${objectId}`; + } + + async create( + params: CreateParams + ): Promise> { + const body = this.buildRequestBody({ + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + chartType: params.type, + name: params.name, + timestamp: params.timestamp, + applicationId: params.applicationId, + userConfigs: params.userConfigs, + description: params.description, + subType: params.subType, + unitsOfMeasure: params.unitsOfMeasure, + selectedLabels: params.selectedLabels, + }); + + const response = await this.client.create( + VISUALIZATION_SAVED_OBJECT, + { + title: params.name, + description: params.description, + version: SAVED_OBJECT_VERSION, + createdTimeMs: new Date().getTime(), + savedVisualization: { + ...body.object, + }, + } + ); + + return { + objectId: this.prependTypeToId(response.id), + object: response, + }; + } + + async update( + params: UpdateParams + ): Promise> { + const body = this.buildRequestBody({ + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + chartType: params.type, + name: params.name, + timestamp: params.timestamp, + applicationId: params.applicationId, + userConfigs: params.userConfigs, + description: params.description, + subType: params.subType, + unitsOfMeasure: params.unitsOfMeasure, + selectedLabels: params.selectedLabels, + }); + + const response = await this.client.update>( + VISUALIZATION_SAVED_OBJECT, + OSDSavedObjectClient.extractTypeAndUUID(params.objectId).uuid, + { + title: params.name, + description: params.description, + version: SAVED_OBJECT_VERSION, + savedVisualization: body.object, + } + ); + + return { + objectId: this.prependTypeToId(response.id), + object: response, + }; + } + + updateBulk(params: unknown): Promise>> { + throw new Error('Method not implemented.'); + } + + async get(params: SavedObjectsGetParams): Promise { + const response = await this.client.get( + VISUALIZATION_SAVED_OBJECT, + OSDSavedObjectClient.extractTypeAndUUID(params.objectId).uuid + ); + return { + observabilityObjectList: [ + { + objectId: this.prependTypeToId(response.id), + createdTimeMs: response.attributes.createdTimeMs, + lastUpdatedTimeMs: OSDSavedObjectClient.convertToLastUpdatedMs(response.updated_at), + savedVisualization: response.attributes.savedVisualization, + }, + ], + }; + } + + async getBulk(params: Partial = {}): Promise { + const observabilityObjectList = await this.client + .find({ + ...params, + type: VISUALIZATION_SAVED_OBJECT, + }) + .then((findRes) => + findRes.savedObjects.map((o) => ({ + objectId: this.prependTypeToId(o.id), + createdTimeMs: o.attributes.createdTimeMs, + lastUpdatedTimeMs: OSDSavedObjectClient.convertToLastUpdatedMs(o.updated_at), + savedVisualization: o.attributes.savedVisualization, + })) + ); + return { totalHits: observabilityObjectList.length, observabilityObjectList }; + } + + async delete(params: SavedObjectsDeleteParams): Promise { + const uuid = OSDSavedObjectClient.extractTypeAndUUID(params.objectId).uuid; + return this.client + .delete(VISUALIZATION_SAVED_OBJECT, uuid) + .then(() => ({ deleteResponseList: { [params.objectId]: 'OK' } })) + .catch((res) => ({ deleteResponseList: { [params.objectId]: res } })); + } + + async deleteBulk(params: SavedObjectsDeleteBulkParams): Promise { + const deleteResponseList: SavedObjectsDeleteResponse['deleteResponseList'] = {}; + await Promise.allSettled(params.objectIdList.map((objectId) => this.delete({ objectId }))).then( + (res) => { + res.forEach((r, i) => { + deleteResponseList[params.objectIdList[i]] = + r.status === 'fulfilled' + ? r.value.deleteResponseList[params.objectIdList[i]] + : r.reason; + }); + } + ); + return { deleteResponseList }; + } + + static getInstance() { + if (!this.instance) { + this.instance = new this(getOSDSavedObjectsClient()); + } + return this.instance; + } +} diff --git a/public/services/saved_objects/saved_object_client/osd_saved_objects/types.ts b/public/services/saved_objects/saved_object_client/osd_saved_objects/types.ts new file mode 100644 index 000000000..3e60d19db --- /dev/null +++ b/public/services/saved_objects/saved_object_client/osd_saved_objects/types.ts @@ -0,0 +1,20 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes, SimpleSavedObject } from '../../../../../../../src/core/public'; +import { OBSERVABILTY_SAVED_OBJECTS } from '../../../../../common/types/observability_saved_object_attributes'; +import { SavedObjectsCreateResponse } from '../types'; + +export type ObservabilitySavedObjectsType = typeof OBSERVABILTY_SAVED_OBJECTS[number]; + +export interface OSDSavedObjectCreateResponse + extends SavedObjectsCreateResponse { + object: SimpleSavedObject; +} + +export interface OSDSavedObjectUpdateResponse + extends SavedObjectsCreateResponse { + object: SimpleSavedObject>; +} diff --git a/public/services/saved_objects/saved_object_client/ppl/index.ts b/public/services/saved_objects/saved_object_client/ppl/index.ts new file mode 100644 index 000000000..36ebb3f4e --- /dev/null +++ b/public/services/saved_objects/saved_object_client/ppl/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { PPLSavedObjectClient } from './ppl_client'; +export { PPLSavedQueryClient } from './saved_query'; +export { PPLSavedVisualizationClient } from './saved_visualization'; +export { PanelSavedObjectClient } from './panels'; diff --git a/public/services/saved_objects/saved_object_client/ppl/panels.ts b/public/services/saved_objects/saved_object_client/ppl/panels.ts new file mode 100644 index 000000000..92d1c6941 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/ppl/panels.ts @@ -0,0 +1,41 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PPLSavedObjectClient } from './ppl_client'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; + +interface PanelRaw { + dateCreated: number; + dateModified: number; + id: string; + name: string; +} + +interface Panel { + label: string; + panel: PanelRaw; +} + +interface CommonParams { + savedVisualizationId: string; + selectedCustomPanels: Panel[]; +} + +type BulkUpdateParams = CommonParams; + +export class PanelSavedObjectClient extends PPLSavedObjectClient { + async updateBulk(params: BulkUpdateParams): Promise>> { + return await Promise.all( + params.selectedCustomPanels.map((panel: Panel) => { + return this.client.post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { + body: JSON.stringify({ + savedVisualizationId: params.savedVisualizationId, + panelId: panel.panel.id, + }), + }); + }) + ); + } +} diff --git a/public/services/saved_objects/saved_object_client/ppl/ppl_client.ts b/public/services/saved_objects/saved_object_client/ppl/ppl_client.ts new file mode 100644 index 000000000..3416def29 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/ppl/ppl_client.ts @@ -0,0 +1,139 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { has, isArray, isEmpty } from 'lodash'; +import { HttpStart } from '../../../../../../../src/core/public'; +import { + EVENT_ANALYTICS, + OBSERVABILITY_BASE, + SAVED_OBJECTS, +} from '../../../../../common/constants/shared'; +import { ISavedObjectRequestParams } from '../../event_analytics/saved_objects'; +import { SavedObjectClientBase } from '../client_base'; +import { ISavedObjectsClient } from '../client_interface'; +import { + SavedObjectsDeleteBulkParams, + SavedObjectsDeleteParams, + SavedObjectsDeleteResponse, + SavedObjectsGetResponse, +} from '../types'; + +const CONCAT_FIELDS = ['objectIdList', 'objectType']; + +export class PPLSavedObjectClient extends SavedObjectClientBase implements ISavedObjectsClient { + constructor(protected readonly client: HttpStart) { + super(); + } + create(params: any): Promise { + throw new Error('Method not implemented.'); + } + get(params: ISavedObjectRequestParams): Promise { + return this.client.get(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}`, { + query: { + ...params, + }, + }); + } + getBulk(params: ISavedObjectRequestParams): Promise { + CONCAT_FIELDS.map((arrayField) => { + this.stringifyList(params, arrayField, ','); + }); + + return this.client.get(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}`, { + query: { + ...params, + }, + }); + } + update(params: any): Promise { + throw new Error('Method not implemented.'); + } + updateBulk(params: any): Promise>> { + throw new Error('Method not implemented.'); + } + delete(params: SavedObjectsDeleteParams): Promise { + return this.client.delete( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/${params.objectId}` + ); + } + deleteBulk(params: SavedObjectsDeleteBulkParams): Promise { + return this.client.delete( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/${params.objectIdList.join(',')}` + ); + } + buildRequestBody({ + query, + fields, + dateRange, + timestamp, + name = '', + chartType = '', + description = '', + applicationId = '', + userConfigs = '', + subType = '', + unitsOfMeasure = '', + selectedLabels, + objectId = '', + }: any) { + const objRequest: any = { + object: { + query, + selected_date_range: { + start: dateRange[0] || 'now/15m', + end: dateRange[1] || 'now', + text: '', + }, + selected_timestamp: { + name: timestamp || '', + type: 'timestamp', + }, + selected_fields: { + tokens: fields, + text: '', + }, + name: name || '', + description: description || '', + }, + }; + + if (!isEmpty(chartType)) { + objRequest.object.type = chartType; + } + + if (!isEmpty(applicationId)) { + objRequest.object.application_id = applicationId; + } + + if (!isEmpty(userConfigs)) { + objRequest.object.user_configs = userConfigs; + } + + if (!isEmpty(subType)) { + objRequest.object.sub_type = subType; + } + + if (!isEmpty(unitsOfMeasure)) { + objRequest.object.units_of_measure = unitsOfMeasure; + } + + if (!isEmpty(selectedLabels)) { + objRequest.object.selected_labels = selectedLabels; + } + + if (!isEmpty(objectId)) { + objRequest.object_id = objectId; + } + + return objRequest; + } + + private stringifyList(targetObj: any, key: string, joinBy: string) { + if (has(targetObj, key) && isArray(targetObj[key])) { + targetObj[key] = targetObj[key].join(joinBy); + } + return targetObj; + } +} diff --git a/public/services/saved_objects/saved_object_client/ppl/saved_query.ts b/public/services/saved_objects/saved_object_client/ppl/saved_query.ts new file mode 100644 index 000000000..4b0f97c03 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/ppl/saved_query.ts @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IField } from '../../../../../common/types/explorer'; +import { PPLSavedObjectClient } from './ppl_client'; +import { + OBSERVABILITY_BASE, + EVENT_ANALYTICS, + SAVED_OBJECTS, + SAVED_QUERY, +} from '../../../../../common/constants/shared'; +import { getOSDHttp } from '../../../../../common/utils'; + +interface CommonParams { + query: string; + fields: IField[]; + dateRange: [string, string]; + name: string; + timestamp: string; +} + +type CreateQueryParams = CommonParams; +type UpdateQueryParams = CommonParams & { + objectId: string; +}; + +export class PPLSavedQueryClient extends PPLSavedObjectClient { + private static instance: PPLSavedQueryClient; + + async create(params: CreateQueryParams): Promise { + return await this.client.post( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_QUERY}`, + { + body: JSON.stringify( + this.buildRequestBody({ + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + name: params.name, + timestamp: params.timestamp, + }) + ), + } + ); + } + + async update(params: UpdateQueryParams): Promise { + return await this.client.put( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_QUERY}`, + { + body: JSON.stringify( + this.buildRequestBody({ + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + name: params.name, + timestamp: params.timestamp, + objectId: params.objectId, + }) + ), + } + ); + } + + static getInstance() { + if (!this.instance) { + this.instance = new this(getOSDHttp()); + } + return this.instance; + } +} diff --git a/public/services/saved_objects/saved_object_client/ppl/saved_visualization.ts b/public/services/saved_objects/saved_object_client/ppl/saved_visualization.ts new file mode 100644 index 000000000..554041294 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/ppl/saved_visualization.ts @@ -0,0 +1,92 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IField } from '../../../../../common/types/explorer'; +import { + OBSERVABILITY_BASE, + EVENT_ANALYTICS, + SAVED_OBJECTS, + SAVED_VISUALIZATION, +} from '../../../../../common/constants/shared'; +import { PPLSavedObjectClient } from './ppl_client'; +import { SavedObjectsCreateResponse, SavedObjectsUpdateResponse } from '../types'; +import { getOSDHttp } from '../../../../../common/utils'; + +interface CommonParams { + query: string; + fields: IField[]; + dateRange: [string, string]; + type: string; + name: string; + timestamp: string; + applicationId: string; + userConfigs: any; + description: string; + subType: string; + unitsOfMeasure: string; + selectedLabels: string; +} + +type CreateParams = CommonParams & { applicationId: string }; +type UpdateParams = CommonParams & { objectId: string }; + +export class PPLSavedVisualizationClient extends PPLSavedObjectClient { + private static instance: PPLSavedVisualizationClient; + + async create(params: CreateParams): Promise { + return await this.client.post( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, + { + body: JSON.stringify( + this.buildRequestBody({ + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + chartType: params.type, + name: params.name, + timestamp: params.timestamp, + applicationId: params.applicationId, + userConfigs: params.userConfigs, + description: params.description, + subType: params.subType, + unitsOfMeasure: params.unitsOfMeasure, + selectedLabels: params.selectedLabels, + }) + ), + } + ); + } + + async update(params: UpdateParams): Promise { + return await this.client.put( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, + { + body: JSON.stringify( + this.buildRequestBody({ + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + chartType: params.type, + name: params.name, + timestamp: params.timestamp, + userConfigs: params.userConfigs, + description: params.description, + subType: params.subType, + unitsOfMeasure: params.unitsOfMeasure, + selectedLabels: params.selectedLabels, + objectId: params.objectId, + }) + ), + } + ); + } + + static getInstance() { + if (!this.instance) { + this.instance = new this(getOSDHttp()); + } + return this.instance; + } +} diff --git a/public/services/saved_objects/saved_object_client/saved_objects_actions.ts b/public/services/saved_objects/saved_object_client/saved_objects_actions.ts new file mode 100644 index 000000000..2b96c6a6c --- /dev/null +++ b/public/services/saved_objects/saved_object_client/saved_objects_actions.ts @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VISUALIZATION_SAVED_OBJECT } from '../../../../common/types/observability_saved_object_attributes'; +import { ISavedObjectRequestParams } from '../event_analytics/saved_objects'; +import { OSDSavedObjectClient } from './osd_saved_objects/osd_saved_object_client'; +import { OSDSavedVisualizationClient } from './osd_saved_objects/saved_visualization'; +import { ObservabilitySavedObjectsType } from './osd_saved_objects/types'; +import { PPLSavedQueryClient } from './ppl'; +import { + ObservabilitySavedObject, + SavedObjectsDeleteBulkParams, + SavedObjectsDeleteParams, + SavedObjectsDeleteResponse, + SavedObjectsGetParams, + SavedObjectsGetResponse, +} from './types'; + +/** + * Helper class that dynamically determines which saved object client to use + * for get and delete operations. This servers as a compatibility layer before + * the .opensearch-observability index is deprecated. + */ +export class SavedObjectsActions { + static get(params: SavedObjectsGetParams): Promise { + const type = OSDSavedObjectClient.extractType(params.objectId); + switch (type) { + case VISUALIZATION_SAVED_OBJECT: + return OSDSavedVisualizationClient.getInstance().get(params); + + default: + // for non-osd objects it does not matter which client implementation + // is used for get() + return PPLSavedQueryClient.getInstance().get(params); + } + } + + static async getBulk( + params: ISavedObjectRequestParams + ): Promise> { + const objects = await PPLSavedQueryClient.getInstance().getBulk(params); + if (params.objectType?.includes('savedVisualization')) { + const osdVisualizationObjects = await OSDSavedVisualizationClient.getInstance().getBulk(); + if (objects.totalHits && osdVisualizationObjects.totalHits) { + objects.totalHits += osdVisualizationObjects.totalHits; + } + objects.observabilityObjectList = [ + ...objects.observabilityObjectList, + ...osdVisualizationObjects.observabilityObjectList, + ]; + } + + if (params.sortOrder === 'asc') { + objects.observabilityObjectList.sort((a, b) => a.lastUpdatedTimeMs - b.lastUpdatedTimeMs); + } else { + objects.observabilityObjectList.sort((a, b) => b.lastUpdatedTimeMs - a.lastUpdatedTimeMs); + } + return objects as SavedObjectsGetResponse; + } + + static delete(params: SavedObjectsDeleteParams): Promise { + const type = OSDSavedObjectClient.extractType(params.objectId); + switch (type) { + case VISUALIZATION_SAVED_OBJECT: + return OSDSavedVisualizationClient.getInstance().delete(params); + + default: + return PPLSavedQueryClient.getInstance().delete(params); + } + } + + /** + * Delete a list of objects. Assumes object is osd visualization if id is a + * UUID. Rest and failed ids will then be deleted by PPL client. + * + * @param params - SavedObjectsDeleteBulkParams + * @returns SavedObjectsDeleteResponse + */ + static async deleteBulk( + params: SavedObjectsDeleteBulkParams + ): Promise { + const idMap = params.objectIdList.reduce((prev, id) => { + const type = OSDSavedObjectClient.extractType(id); + const key = type === '' ? 'non_osd' : type; + return { ...prev, [key]: [...(prev[key] || []), id] }; + }, {} as { [k in 'non_osd' | ObservabilitySavedObjectsType]: string[] }); + + const responses: SavedObjectsDeleteResponse = { deleteResponseList: {} }; + + if (idMap[VISUALIZATION_SAVED_OBJECT]?.length) { + const visualizationDeleteResponses = await OSDSavedVisualizationClient.getInstance().deleteBulk( + { + objectIdList: idMap[VISUALIZATION_SAVED_OBJECT], + } + ); + responses.deleteResponseList = { + ...responses.deleteResponseList, + ...visualizationDeleteResponses.deleteResponseList, + }; + } + + const remainingObjectIds = [ + ...new Set( + idMap.non_osd.concat( + Object.entries(responses.deleteResponseList) + .filter(([_, status]) => status !== 'OK') + .map(([id, _]) => id) + ) + ), + ]; + if (remainingObjectIds.length) { + const remainingDeleteResponses = await PPLSavedQueryClient.getInstance().deleteBulk({ + objectIdList: remainingObjectIds, + }); + responses.deleteResponseList = { + ...responses.deleteResponseList, + ...remainingDeleteResponses.deleteResponseList, + }; + } + return responses; + } +} diff --git a/public/services/saved_objects/saved_object_client/types.ts b/public/services/saved_objects/saved_object_client/types.ts new file mode 100644 index 000000000..81112fd67 --- /dev/null +++ b/public/services/saved_objects/saved_object_client/types.ts @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedQuery, SavedVisualization } from '../../../../common/types/explorer'; +import { SAVED_QUERY, SAVED_VISUALIZATION } from '../../../../common/constants/explorer'; + +export interface SavedObjectsCreateResponse { + objectId: string; +} + +export type SavedObjectsUpdateResponse = SavedObjectsCreateResponse; + +interface ObservabilitySavedObjectBase { + createdTimeMs: number; + lastUpdatedTimeMs: number; + objectId: string; + tenant?: string; +} + +export interface ObservabilitySavedVisualization extends ObservabilitySavedObjectBase { + [SAVED_VISUALIZATION]: SavedVisualization; +} + +export interface ObservabilitySavedQuery extends ObservabilitySavedObjectBase { + [SAVED_QUERY]: SavedQuery; +} + +export type ObservabilitySavedObject = ObservabilitySavedVisualization | ObservabilitySavedQuery; + +export interface SavedObjectsGetParams { + objectId: string; +} + +export interface SavedObjectsGetResponse< + T extends ObservabilitySavedObject = ObservabilitySavedObject +> { + startIndex?: number; + totalHits?: number; + totalHitRelation?: 'eq' | 'gte'; + observabilityObjectList: T[]; +} + +export interface SavedObjectsDeleteParams { + objectId: string; +} + +export interface SavedObjectsDeleteBulkParams { + objectIdList: string[]; +} + +export interface SavedObjectsDeleteResponse { + deleteResponseList: { + [objectId: string]: string; // org.opensearch.rest.RestStatus, e.g. 'OK' + }; +} diff --git a/public/services/saved_objects/saved_object_loaders/loader_base.ts b/public/services/saved_objects/saved_object_loaders/loader_base.ts new file mode 100644 index 000000000..a3c221c53 --- /dev/null +++ b/public/services/saved_objects/saved_object_loaders/loader_base.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ISavedObjectLoader } from './loader_interface'; + +export abstract class SavedObjectLoaderBase implements ISavedObjectLoader { + constructor() {} + abstract load(): void; +} diff --git a/public/services/saved_objects/saved_object_loaders/loader_interface.ts b/public/services/saved_objects/saved_object_loaders/loader_interface.ts new file mode 100644 index 000000000..93dfc613b --- /dev/null +++ b/public/services/saved_objects/saved_object_loaders/loader_interface.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface ISavedObjectLoader { + load: () => void; +} diff --git a/public/services/saved_objects/saved_object_loaders/ppl/index.ts b/public/services/saved_objects/saved_object_loaders/ppl/index.ts new file mode 100644 index 000000000..a850c1690 --- /dev/null +++ b/public/services/saved_objects/saved_object_loaders/ppl/index.ts @@ -0,0 +1,4 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/public/services/saved_objects/saved_object_loaders/ppl/ppl_loader.ts b/public/services/saved_objects/saved_object_loaders/ppl/ppl_loader.ts new file mode 100644 index 000000000..21fc4b27a --- /dev/null +++ b/public/services/saved_objects/saved_object_loaders/ppl/ppl_loader.ts @@ -0,0 +1,242 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { has, isEmpty } from 'lodash'; +import { updateFields as updateFieldsAction } from 'public/components/event_analytics/redux/slices/field_slice'; +import { changeQuery as changeQueryAction } from 'public/components/event_analytics/redux/slices/query_slice'; +import { updateTabName as updateTabNameAction } from 'public/components/event_analytics/redux/slices/query_tab_slice'; +import { change as updateVizConfigAction } from 'public/components/event_analytics/redux/slices/viualization_config_slice'; +import { batch as Batch } from 'react-redux'; +import { NotificationsStart } from '../../../../../../../src/core/public'; +import { + AGGREGATIONS, + BREAKDOWNS, + GROUPBY, + RAW_QUERY, + SAVED_OBJECT_ID, + SAVED_OBJECT_TYPE, + SAVED_QUERY, + SAVED_VISUALIZATION, + SELECTED_DATE_RANGE, + SELECTED_FIELDS, + SELECTED_TIMESTAMP, + TYPE_TAB_MAPPING, +} from '../../../../../common/constants/explorer'; +import { QueryManager } from '../../../../../common/query_manager'; +import { statsChunk } from '../../../../../common/query_manager/ast/types/stats'; +import { IField, SavedQuery, SavedVisualization } from '../../../../../common/types/explorer'; +import { AppDispatch } from '../../../../framework/redux/store'; +import { ISavedObjectsClient } from '../../saved_object_client/client_interface'; +import { ObservabilitySavedObject, ObservabilitySavedQuery } from '../../saved_object_client/types'; +import { SavedObjectLoaderBase } from '../loader_base'; +import { ISavedObjectLoader } from '../loader_interface'; + +interface LoadParams { + objectId: string; +} + +interface LoadContext { + tabId: string; + appLogEvents: boolean; + setStartTime: (startTime: string) => void; + setEndTime: (endTime: string) => void; + queryManager: QueryManager; + getDefaultVisConfig: ( + statsToken: statsChunk + ) => { + [AGGREGATIONS]: IField[]; + [GROUPBY]: IField[]; + [BREAKDOWNS]?: IField[]; + span?: any; + }; + setSelectedPanelName: (savedObjectName: string) => void; + setCurVisId: (visId: string) => void; + setTempQuery: (tmpQuery: string) => void; + setMetricChecked: (metricChecked: boolean) => void; + setMetricMeasure: (metricMeasure: string) => void; + setSubType: (type: string) => void; + setSelectedContentTab: (curTab: string) => void; + fetchData: () => void; +} + +interface Dispatchers { + batch: typeof Batch; + dispatch: AppDispatch; + changeQuery: typeof changeQueryAction; + updateFields: typeof updateFieldsAction; + updateTabName: typeof updateTabNameAction; + updateVizConfig: typeof updateVizConfigAction; +} + +type SavedObjectData = ObservabilitySavedObject; + +function isObjectSavedQuery( + savedObjectData: SavedObjectData +): savedObjectData is ObservabilitySavedQuery { + return SAVED_QUERY in savedObjectData; +} + +function isInnerObjectSavedVisualization( + objectData: SavedQuery | SavedVisualization +): objectData is SavedVisualization { + return 'type' in objectData; +} + +export class PPLSavedObjectLoader extends SavedObjectLoaderBase implements ISavedObjectLoader { + constructor( + protected readonly savedObjectClient: ISavedObjectsClient, + protected readonly notifications: NotificationsStart, + protected readonly dispatchers: Dispatchers, + protected readonly loadParams: LoadParams, + protected readonly loadContext: LoadContext + ) { + super(); + } + + async load() { + await this.getSavedObjectById(this.loadParams.objectId); + } + + async getSavedObjectById(objectId: string) { + try { + const res = await this.savedObjectClient.get({ + objectId, + }); + await this.processSavedData(res.observabilityObjectList[0]); + } catch (error) { + this.notifications.toasts.addError(error, { + title: `Cannot get saved data for object id: ${objectId}`, + }); + } + } + + updateAppAnalyticSelectedDateRange(selectedDateRange: { start: string; end: string }) { + const { setStartTime, setEndTime } = this.loadContext; + setStartTime(selectedDateRange.start); + setEndTime(selectedDateRange.end); + } + + async processSavedData(savedObjectData: SavedObjectData) { + const savedType = isObjectSavedQuery(savedObjectData) ? SAVED_QUERY : SAVED_VISUALIZATION; + const objectData = isObjectSavedQuery(savedObjectData) + ? savedObjectData.savedQuery + : savedObjectData.savedVisualization; + const currQuery = objectData?.query || ''; + const { appLogEvents } = this.loadContext; + + // app analytics specific + if (appLogEvents && objectData.selected_date_range) { + this.updateAppAnalyticSelectedDateRange(objectData.selected_date_range); + } + + // update redux store with this saved object data + await this.updateReduxState(savedType, objectData, currQuery); + + // update UI state with this saved object data + await this.updateUIState(objectData); + + // fetch data based on saved object data + await this.loadDataFromSavedObject(); + } + + async updateReduxState( + savedType: typeof SAVED_QUERY | typeof SAVED_VISUALIZATION, + objectData: SavedQuery | SavedVisualization, + currQuery: string + ) { + const { batch, dispatch, changeQuery, updateFields, updateTabName } = this.dispatchers; + const { tabId } = this.loadContext; + const { objectId } = this.loadParams; + batch(async () => { + await dispatch( + changeQuery({ + tabId, + query: { + [RAW_QUERY]: currQuery, + [SELECTED_TIMESTAMP]: objectData?.selected_timestamp?.name || 'timestamp', + [SAVED_OBJECT_ID]: objectId, + [SAVED_OBJECT_TYPE]: savedType, + [SELECTED_DATE_RANGE]: + objectData?.selected_date_range?.start && objectData?.selected_date_range?.end + ? [objectData.selected_date_range.start, objectData.selected_date_range.end] + : ['now-15m', 'now'], + }, + }) + ); + await dispatch( + updateFields({ + tabId, + data: { + [SELECTED_FIELDS]: [...objectData?.selected_fields?.tokens], + }, + }) + ); + await dispatch( + updateTabName({ + tabId, + tabName: objectData.name, + }) + ); + if (isInnerObjectSavedVisualization(objectData)) { + await this.updateVisualizationConfig(objectData); + } + }); + } + + async updateVisualizationConfig(objectData: SavedVisualization) { + const { dispatch, updateVizConfig } = this.dispatchers; + const { tabId, queryManager, getDefaultVisConfig } = this.loadContext; + // fill saved user configs + let visConfig = {}; + const customConfig = objectData.user_configs ? JSON.parse(objectData.user_configs) : {}; + if (!isEmpty(customConfig.dataConfig) && !isEmpty(customConfig.dataConfig?.series)) { + visConfig = { ...customConfig }; + } else { + const statsTokens = queryManager.queryParser().parse(objectData.query).getStats(); + visConfig = { dataConfig: { ...getDefaultVisConfig(statsTokens) } }; + } + await dispatch( + updateVizConfig({ + tabId, + vizId: objectData?.type, + data: visConfig, + }) + ); + } + + async updateUIState(objectData: SavedQuery | SavedVisualization) { + const { + setSelectedPanelName, + setCurVisId, + setTempQuery, + setMetricChecked, + setMetricMeasure, + setSubType, + setSelectedContentTab, + } = this.loadContext; + // update UI state with saved data + setSelectedPanelName(objectData?.name || ''); + setCurVisId(objectData?.type || 'bar'); + setTempQuery((staleTempQuery) => { + return objectData?.query || staleTempQuery; + }); + if (isInnerObjectSavedVisualization(objectData)) { + if (objectData.sub_type === 'metric') { + setMetricChecked(true); + setMetricMeasure(objectData.units_of_measure || ''); + } + setSubType(objectData.sub_type); + } + const tabToBeFocused = isInnerObjectSavedVisualization(objectData) + ? TYPE_TAB_MAPPING[SAVED_VISUALIZATION] + : TYPE_TAB_MAPPING[SAVED_QUERY]; + setSelectedContentTab(tabToBeFocused); + } + + async loadDataFromSavedObject() { + const { fetchData } = this.loadContext; + await fetchData(); + } +} diff --git a/public/services/saved_objects/saved_object_savers/index.ts b/public/services/saved_objects/saved_object_savers/index.ts new file mode 100644 index 000000000..b20c5d91a --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { ISavedObjectSaver } from './saver_interface'; +export { SavedObjectSaverBase } from './saver_base'; +export { SavedQuerySaver } from './ppl/saved_query_saver'; +export { SaveAsNewQuery } from './ppl/save_as_new_query'; +export { SaveAsCurrentQuery } from './ppl/save_current_query'; +export { SaveAsNewVisualization } from './ppl/save_as_new_vis'; +export { SaveAsCurrentVisualization } from './ppl/save_as_current_vis'; diff --git a/public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts b/public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts new file mode 100644 index 000000000..d6708ae4f --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/ppl/save_as_current_vis.ts @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedQuerySaver } from './saved_query_saver'; + +export class SaveAsCurrentVisualization extends SavedQuerySaver { + constructor( + private readonly saveContext, + protected readonly dispatchers, + private readonly saveClient, + private readonly panelClient, + private readonly saveParams + ) { + super(dispatchers); + } + + save(): void { + const { dispatch, updateTabName } = this.dispatchers; + const { tabId, notifications } = this.saveContext; + const { name, selectedPanels } = this.saveParams; + this.saveClient + .update({ ...this.saveParams }) + .then((res: any) => { + notifications.toasts.addSuccess({ + title: 'Saved successfully.', + text: `Visualization '${name}' has been successfully updated.`, + }); + + if (selectedPanels?.length) + this.addToPanel({ selectedPanels, saveTitle: name, notifications, visId: res.objectId }); + + dispatch( + updateTabName({ + tabId, + tabName: name, + }) + ); + }) + .catch((error: any) => { + notifications.toasts.addError(error, { + title: `Cannot update Visualization '${name}'`, + }); + }); + } + + addToPanel({ selectedPanels, saveTitle, notifications, visId }) { + this.panelClient + .updateBulk({ + selectedCustomPanels: selectedPanels, + savedVisualizationId: visId, + }) + .then((res: any) => { + notifications.toasts.addSuccess({ + title: 'Saved successfully.', + text: `Visualization '${saveTitle}' has been successfully saved to operation panels.`, + }); + }) + .catch((error: any) => { + notifications.toasts.addError(error, { + title: 'Failed to save', + text: `Cannot add Visualization '${saveTitle}' to operation panels`, + }); + }); + } +} diff --git a/public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts new file mode 100644 index 000000000..1c174438d --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_query.ts @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SAVED_OBJECT_ID, + SAVED_OBJECT_TYPE, + SAVED_QUERY, +} from '../../../../../common/constants/explorer'; +import { SavedQuerySaver } from './saved_query_saver'; + +export class SaveAsNewQuery extends SavedQuerySaver { + constructor( + private readonly saveContext, + protected readonly dispatchers, + private readonly saveClient, + private readonly saveParams + ) { + super(dispatchers); + } + + save(): void { + const { batch, dispatch, changeQuery, updateTabName } = this.dispatchers; + const { tabId, history, notifications, showPermissionErrorToast } = this.saveContext; + const { name } = this.saveParams; + console.log('this.saveParams: ', this.saveParams); + this.saveClient + .create({ ...this.saveParams }) + .then((res: any) => { + history.replace(`/event_analytics/explorer/${res.objectId}`); + notifications.toasts.addSuccess({ + title: 'Saved successfully.', + text: `New query '${name}' has been successfully saved.`, + }); + batch(() => { + dispatch( + changeQuery({ + tabId, + query: { + [SAVED_OBJECT_ID]: res.objectId, + [SAVED_OBJECT_TYPE]: SAVED_QUERY, + }, + }) + ); + dispatch( + updateTabName({ + tabId, + tabName: name, + }) + ); + }); + history.replace(`/event_analytics/explorer/${res.objectId}`); + return res; + }) + .catch((error: any) => { + if (error?.body?.statusCode === 403) { + showPermissionErrorToast(); + } else { + notifications.toasts.addError(error, { + title: `Cannot save query '${name}'`, + }); + } + }); + } +} diff --git a/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts new file mode 100644 index 000000000..b0c57d4c7 --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/ppl/save_as_new_vis.ts @@ -0,0 +1,97 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + SAVED_OBJECT_ID, + SAVED_OBJECT_TYPE, + SAVED_VISUALIZATION, +} from '../../../../../common/constants/explorer'; +import { ISavedObjectsClient } from '../../saved_object_client/client_interface'; +import { SavedQuerySaver } from './saved_query_saver'; + +export class SaveAsNewVisualization extends SavedQuerySaver { + constructor( + private readonly saveContext, + protected readonly dispatchers, + private readonly saveClient: ISavedObjectsClient, + private readonly panelClient, + private readonly saveParams + ) { + super(dispatchers); + } + + save(): void { + const { batch, dispatch, changeQuery, updateTabName } = this.dispatchers; + const { + tabId, + history, + notifications, + addVisualizationToPanel, + appLogEvents, + } = this.saveContext; + const { name, selectedPanels } = this.saveParams; + + this.saveClient + .create({ ...this.saveParams }) + .then((res: any) => { + notifications.toasts.addSuccess({ + title: 'Saved successfully.', + text: `New visualization '${name}' has been successfully saved.`, + }); + + if (selectedPanels?.length) + this.addToPanel({ selectedPanels, saveTitle: name, notifications, visId: res.objectId }); + + if (appLogEvents) { + addVisualizationToPanel(res.objectId, name); + } else { + history.replace(`/event_analytics/explorer/${res.objectId}`); + } + + batch(() => { + dispatch( + changeQuery({ + tabId, + query: { + [SAVED_OBJECT_ID]: res.objectId, + [SAVED_OBJECT_TYPE]: SAVED_VISUALIZATION, + }, + }) + ); + dispatch( + updateTabName({ + tabId, + tabName: name, + }) + ); + }); + }) + .catch((error: any) => { + notifications.toasts.addError(error, { + title: `Cannot save Visualization '${name}'`, + }); + }); + } + + addToPanel({ selectedPanels, saveTitle, notifications, visId }) { + this.panelClient + .updateBulk({ + selectedCustomPanels: selectedPanels, + savedVisualizationId: visId, + }) + .then((res: any) => { + notifications.toasts.addSuccess({ + title: 'Saved successfully.', + text: `Visualization '${saveTitle}' has been successfully saved to operation panels.`, + }); + }) + .catch((error: any) => { + notifications.toasts.addError(error, { + title: 'Failed to save', + text: `Cannot add Visualization '${saveTitle}' to operation panels`, + }); + }); + } +} diff --git a/public/services/saved_objects/saved_object_savers/ppl/save_current_query.ts b/public/services/saved_objects/saved_object_savers/ppl/save_current_query.ts new file mode 100644 index 000000000..05c2d3560 --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/ppl/save_current_query.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedQuerySaver } from './saved_query_saver'; + +export class SaveAsCurrentQuery extends SavedQuerySaver { + constructor( + private readonly saveContext, + protected readonly dispatchers, + private readonly saveClient, + private readonly saveParams + ) { + super(dispatchers); + } + + save(): void { + const { dispatch, updateTabName } = this.dispatchers; + const { tabId, notifications } = this.saveContext; + const { name } = this.saveParams; + this.saveClient + .update({ ...this.saveParams }) + .then((res: any) => { + notifications.toasts.addSuccess({ + title: 'Saved successfully.', + text: `Query '${name}' has been successfully updated.`, + }); + dispatch( + updateTabName({ + tabId, + tabName: name, + }) + ); + }) + .catch((error: any) => { + notifications.toasts.addError(error, { + title: `Cannot update query '${name}'`, + }); + }); + } +} diff --git a/public/services/saved_objects/saved_object_savers/ppl/saved_query_saver.ts b/public/services/saved_objects/saved_object_savers/ppl/saved_query_saver.ts new file mode 100644 index 000000000..23036a0d3 --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/ppl/saved_query_saver.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ISavedObjectSaver } from '../saver_interface'; +import { SavedObjectSaverBase } from '../saver_base'; + +export class SavedQuerySaver extends SavedObjectSaverBase implements ISavedObjectSaver { + save() { + throw new Error('Method not implemented.'); + } +} diff --git a/public/services/saved_objects/saved_object_savers/saver_base.ts b/public/services/saved_objects/saved_object_savers/saver_base.ts new file mode 100644 index 000000000..e1bbc85fd --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/saver_base.ts @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { capitalize } from 'lodash'; +import { CoreStart } from '../../../../../../src/core/public'; +import { ISavedObjectSaver } from './saver_interface'; + +export abstract class SavedObjectSaverBase implements ISavedObjectSaver { + constructor(protected readonly dispatchers) {} + abstract save(): any; + handleResponse( + notification: CoreStart['notifications'], + type: 'success' | 'danger', + msg: string + ) { + notification.toasts[`add${capitalize(type)}`]({ + title: 'Saving objects', + text: msg, + }); + } +} diff --git a/public/services/saved_objects/saved_object_savers/saver_interface.ts b/public/services/saved_objects/saved_object_savers/saver_interface.ts new file mode 100644 index 000000000..9eb7c6035 --- /dev/null +++ b/public/services/saved_objects/saved_object_savers/saver_interface.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface ISavedObjectSaver { + save: () => void; +} diff --git a/public/types.ts b/public/types.ts index 82bd6543a..48f0ad9de 100644 --- a/public/types.ts +++ b/public/types.ts @@ -3,14 +3,30 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { SavedObjectsClient } from '../../../src/core/server'; import { DashboardStart } from '../../../src/plugins/dashboard/public'; +import { DataPublicPluginSetup } from '../../../src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public'; import { NavigationPublicPluginStart } from '../../../src/plugins/navigation/public'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { VisualizationsSetup } from '../../../src/plugins/visualizations/public'; export interface AppPluginStartDependencies { navigation: NavigationPublicPluginStart; + embeddable: EmbeddableStart; dashboard: DashboardStart; + savedObjectsClient: SavedObjectsClient; } +export interface SetupDependencies { + embeddable: EmbeddableSetup; + visualizations: VisualizationsSetup; + data: DataPublicPluginSetup; + uiActions: UiActionsStart; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ObservabilitySetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ObservabilityStart {} diff --git a/public/variables.scss b/public/variables.scss index 943d50749..8849b6e19 100644 --- a/public/variables.scss +++ b/public/variables.scss @@ -3,5 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +/** Shard **/ +$content-tab-padding: 8px; + // dark mode border color $border-color-on-dark: #343741; + +/** Event Analytics **/ + +// add-new tab +$tab-new-line-height: 2.7; +$tab-new-padding: $content-tab-padding; \ No newline at end of file diff --git a/server/plugin.ts b/server/plugin.ts index 69738f73e..bf86ea997 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -14,6 +14,7 @@ import { import { OpenSearchObservabilityPlugin } from './adaptors/opensearch_observability_plugin'; import { PPLPlugin } from './adaptors/ppl_plugin'; import { setupRoutes } from './routes/index'; +import { visualizationSavedObject } from './saved_objects/observability_saved_object'; import { ObservabilityPluginSetup, ObservabilityPluginStart } from './types'; export class ObservabilityPlugin @@ -30,10 +31,7 @@ export class ObservabilityPlugin const openSearchObservabilityClient: ILegacyClusterClient = core.opensearch.legacy.createClient( 'opensearch_observability', { - plugins: [ - PPLPlugin, - OpenSearchObservabilityPlugin, - ], + plugins: [PPLPlugin, OpenSearchObservabilityPlugin], } ); @@ -48,6 +46,13 @@ export class ObservabilityPlugin // Register server side APIs setupRoutes({ router, client: openSearchObservabilityClient }); + core.savedObjects.registerType(visualizationSavedObject); + core.capabilities.registerProvider(() => ({ + observability: { + show: true, + }, + })); + return {}; } diff --git a/server/routes/event_analytics/event_analytics_router.ts b/server/routes/event_analytics/event_analytics_router.ts index 4d343c732..c0ea4a44f 100644 --- a/server/routes/event_analytics/event_analytics_router.ts +++ b/server/routes/event_analytics/event_analytics_router.ts @@ -26,7 +26,6 @@ export const registerEventAnalyticsRouter = ({ router: IRouter; savedObjectFacet: SavedObjectFacet; }) => { - router.get( { path: `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}`, @@ -49,7 +48,7 @@ export const registerEventAnalyticsRouter = ({ return res.custom(result); } ); - + router.get( { path: `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/{objectId}`, @@ -145,12 +144,14 @@ export const registerEventAnalyticsRouter = ({ name: schema.string(), description: schema.string(), application_id: schema.maybe(schema.string()), - user_configs: schema.string(), - sub_type: schema.string(), + user_configs: schema.maybe(schema.string()), + sub_type: schema.maybe(schema.string()), units_of_measure: schema.maybe(schema.string()), - selected_labels: schema.maybe(schema.object({ - label: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), - })), + selected_labels: schema.maybe( + schema.object({ + label: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), + }) + ), }), }), }, @@ -241,12 +242,14 @@ export const registerEventAnalyticsRouter = ({ name: schema.string(), description: schema.string(), application_id: schema.maybe(schema.string()), - user_configs: schema.string(), - sub_type: schema.string(), + user_configs: schema.maybe(schema.string()), + sub_type: schema.maybe(schema.string()), units_of_measure: schema.maybe(schema.string()), - selected_labels: schema.maybe(schema.object({ - labels: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), - })), + selected_labels: schema.maybe( + schema.object({ + labels: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), + }) + ), }), }), }, diff --git a/server/saved_objects/observability_saved_object.ts b/server/saved_objects/observability_saved_object.ts new file mode 100644 index 000000000..5c47a0c22 --- /dev/null +++ b/server/saved_objects/observability_saved_object.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType } from '../../../../src/core/server'; +import { observabilityID } from '../../common/constants/shared'; +import { VISUALIZATION_SAVED_OBJECT } from '../../common/types/observability_saved_object_attributes'; + +export const visualizationSavedObject: SavedObjectsType = { + name: VISUALIZATION_SAVED_OBJECT, + hidden: false, + namespaceType: 'single', + management: { + defaultSearchField: 'title', + importableAndExportable: true, + icon: 'visQueryPPL', + getTitle(obj) { + return obj.attributes.title; + }, + getInAppUrl(obj) { + const editPath = `#/event_analytics/explorer/${VISUALIZATION_SAVED_OBJECT}:${obj.id}`; + const editUrl = `/app/${observabilityID}${editPath}`; + return { + path: editUrl, + uiCapabilitiesPath: 'observability.show', + }; + }, + }, + mappings: { + dynamic: false, + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + version: { type: 'integer' }, + }, + }, + migrations: {}, +}; diff --git a/server/services/facets/saved_objects.ts b/server/services/facets/saved_objects.ts index 30286d21b..ee4363c4d 100644 --- a/server/services/facets/saved_objects.ts +++ b/server/services/facets/saved_objects.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { ILegacyClusterClient, ScopeableRequest } from '../../../../../src/core/server'; import { sampleQueries, sampleVisualizations, @@ -10,11 +11,11 @@ import { // eslint-disable-next-line import/no-default-export export default class SavedObjectFacet { - constructor(private client: any) { + constructor(private client: ILegacyClusterClient) { this.client = client; } - fetch = async (request: any, params: any, format: string) => { + fetch = async (request: ScopeableRequest, params: Record, format: string) => { const res = { success: false, data: {}, @@ -152,7 +153,7 @@ export default class SavedObjectFacet { const savedQueryIds: any[] = []; if (['panels', 'event_analytics'].includes(request.params.sampleRequestor)) { - for (var i = 0; i < sampleVisualizations.length; i++) { + for (let i = 0; i < sampleVisualizations.length; i++) { const params = { body: { savedVisualization: { @@ -164,7 +165,7 @@ export default class SavedObjectFacet { savedVizIds.push(savedVizRes.objectId); } - for (var i = 0; i < sampleQueries.length; i++) { + for (let i = 0; i < sampleQueries.length; i++) { const params = { body: { savedQuery: { @@ -196,7 +197,7 @@ export default class SavedObjectFacet { return this.fetch( request, { - ...params + ...params, }, 'observability.getObject' ); diff --git a/test/__mocks__/coreMocks.ts b/test/__mocks__/coreMocks.ts index 5e2a10d8d..101c27a12 100644 --- a/test/__mocks__/coreMocks.ts +++ b/test/__mocks__/coreMocks.ts @@ -3,25 +3,17 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { of } from 'rxjs'; import { CoreStart } from '../../../../src/core/public'; +import { coreMock } from '../../../../src/core/public/mocks'; import httpClientMock from './httpClientMock'; -import { of } from 'rxjs'; +const coreStart = coreMock.createStart(); +coreStart.savedObjects.client.find = jest.fn(() => Promise.resolve({ savedObjects: [] })) as any; + +// TODO use coreMock for http const coreStartMock = ({ - uiSettings: { - get: jest.fn(), - }, - chrome: { - setBreadcrumbs: jest.fn(), - getIsNavDrawerLocked$: jest.fn(() => of(true)), - }, - notifications: { - toasts: { - addDanger: jest.fn().mockName('addDanger'), - addSuccess: jest.fn().mockName('addSuccess'), - addError: jest.fn().mockName('addError'), - }, - }, + ...coreStart, http: httpClientMock, } as unknown) as CoreStart; diff --git a/test/jest.config.js b/test/jest.config.js index 7ce1f85d8..b6fc147c1 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -21,12 +21,15 @@ module.exports = { '/test/', '/public/requests/', ], + transform: { + '^.+\\.tsx?$': ['ts-jest', { diagnostics: false }], + }, transformIgnorePatterns: ['/node_modules'], moduleNameMapper: { '\\.(css|less|sass|scss)$': '/test/__mocks__/styleMock.js', '\\.(gif|ttf|eot|svg|png)$': '/test/__mocks__/fileMock.js', '\\@algolia/autocomplete-theme-classic$': '/test/__mocks__/styleMock.js', - "^!!raw-loader!.*": "jest-raw-loader", + '^!!raw-loader!.*': 'jest-raw-loader', }, - testEnvironment: "jsdom", + testEnvironment: 'jsdom', }; diff --git a/test/setup.jest.ts b/test/setup.jest.ts index 4d1413289..7f612f908 100644 --- a/test/setup.jest.ts +++ b/test/setup.jest.ts @@ -5,11 +5,13 @@ // import '@testing-library/jest-dom/extend-expect'; import { configure } from '@testing-library/react'; +import { setOSDHttp, setOSDSavedObjectsClient } from '../common/utils'; +import { coreStartMock } from './__mocks__/coreMocks'; configure({ testIdAttribute: 'data-test-subj' }); window.URL.createObjectURL = () => ''; -HTMLCanvasElement.prototype.getContext = () => ''; +HTMLCanvasElement.prototype.getContext = () => '' as any; window.IntersectionObserver = class IntersectionObserver { constructor() {} @@ -28,7 +30,7 @@ window.IntersectionObserver = class IntersectionObserver { unobserve() { return null; } -}; +} as any; jest.mock('@elastic/eui/lib/components/form/form_row/make_id', () => () => 'random-id'); @@ -38,4 +40,20 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ }, })); +jest.mock('../public/services/saved_objects/saved_object_client/saved_objects_actions', () => { + return { + SavedObjectsActions: { + get: jest.fn().mockResolvedValue({ + observabilityObjectList: [], + }), + getBulk: jest.fn().mockResolvedValue({ + observabilityObjectList: [], + }), + }, + }; +}); + jest.setTimeout(30000); + +setOSDHttp(coreStartMock.http); +setOSDSavedObjectsClient(coreStartMock.savedObjects.client); diff --git a/yarn.lock b/yarn.lock index 5def9bb31..952d6ff8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -156,6 +156,25 @@ gud "^1.0.0" warning "^4.0.3" +"@jest/schemas@^29.4.3": + version "29.4.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.3.tgz#39cf1b8469afc40b6f5a2baaa146e332c4151788" + integrity sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg== + dependencies: + "@sinclair/typebox" "^0.25.16" + +"@jest/types@^29.5.0": + version "29.5.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" + integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== + dependencies: + "@jest/schemas" "^29.4.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@nteract/markdown@^4.5.2": version "4.6.2" resolved "https://registry.yarnpkg.com/@nteract/markdown/-/markdown-4.6.2.tgz#5e3dc44047f7af761b3fb8cf76f6d239e7bb65c3" @@ -211,6 +230,11 @@ dependencies: any-observable "^0.3.0" +"@sinclair/typebox@^0.25.16": + version "0.25.24" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718" + integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ== + "@types/cheerio@*": version "0.22.30" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.30.tgz#6c1ded70d20d890337f0f5144be2c5e9ce0936e6" @@ -250,6 +274,25 @@ dependencies: "@types/unist" "*" +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/node@*": version "16.7.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.2.tgz#0465a39b5456b61a04d98bd5545f8b34be340cb7" @@ -325,6 +368,18 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.24" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" + integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + dependencies: + "@types/yargs-parser" "*" + acorn-jsx@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -520,6 +575,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -573,7 +635,7 @@ chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -611,6 +673,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.2.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" + integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== + classnames@^2.2, classnames@^2.2.5, classnames@^2.2.6: version "2.3.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" @@ -1143,7 +1210,7 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -1339,6 +1406,11 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + gud@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" @@ -1687,6 +1759,18 @@ jest-dom@^4.0.0: resolved "https://registry.yarnpkg.com/jest-dom/-/jest-dom-4.0.0.tgz#94eba3cbc6576e7bd6821867c92d176de28920eb" integrity sha512-gBxYZlZB1Jgvf2gP2pRfjjUWF8woGBHj/g5rAQgFPB/0K2atGuhVcPO+BItyjWeKg9zM+dokgcMOH01vrWVMFA== +jest-util@^29.0.0: + version "29.5.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" + integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== + dependencies: + "@jest/types" "^29.5.0" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -1725,6 +1809,11 @@ json-stringify-safe@~5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -1821,6 +1910,11 @@ lodash.flow@^3.5.0: resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + lodash.once@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -1870,6 +1964,18 @@ lowlight@^1.17.0: fault "^1.0.0" highlight.js "~10.7.0" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + markdown-escapes@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535" @@ -2109,6 +2215,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picomatch@^2.2.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + pify@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2535,6 +2646,13 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +semver@7.x: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -2800,6 +2918,20 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== +ts-jest@^29.1.0: + version "29.1.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.0.tgz#4a9db4104a49b76d2b368ea775b6c9535c603891" + integrity sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -3047,6 +3179,16 @@ xtend@^4.0.0, xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^21.0.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"