From af155bbb602e83fbc839a8c35e04bd1ac743fc4f Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Thu, 4 Nov 2021 17:52:11 -0700 Subject: [PATCH 1/4] added panels modifications and bug fix (#194) Signed-off-by: Shenoy Pratik --- .../common/constants/shared.ts | 12 ++-- .../custom_panels/custom_panel_view.tsx | 59 ++++++++++--------- .../panel_modules/empty_panel.tsx | 2 +- .../visualization_container.scss | 4 +- .../visualization_container.tsx | 15 +++-- .../count_distribution/count_distribution.tsx | 3 +- .../components/visualizations/charts/bar.tsx | 23 ++++++-- .../components/visualizations/charts/line.tsx | 4 +- 8 files changed, 72 insertions(+), 50 deletions(-) diff --git a/dashboards-observability/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts index b72331d26..6cfade137 100644 --- a/dashboards-observability/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -49,15 +49,17 @@ export const OPENSEARCH_PANELS_API = { export const SAVED_OBJECT = '/object'; // Color Constants -export const PlotlyColorWay = [ +export const PLOTLY_COLOR = [ '#3CA1C7', '#8C55A3', '#DB748A', '#F2BE4B', '#68CCC2', - '#127871', - '#5F1084', - '#005EB8', - '#BD800F', + '#2A7866', + '#843769', + '#374FB8', + '#BD6F26', '#4C636F', ]; + +export const LONG_CHART_COLOR = PLOTLY_COLOR[1]; diff --git a/dashboards-observability/public/components/custom_panels/custom_panel_view.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_view.tsx index 49a92e5a4..4731b032a 100644 --- a/dashboards-observability/public/components/custom_panels/custom_panel_view.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_view.tsx @@ -376,12 +376,23 @@ export const CustomPanelView = ({ iconSide="right" disabled={addVizDisabled} onClick={onPopoverClick} - fill > Add Visualization ); + // Panel Actions Button + const panelActionsButton = ( + setPanelsMenuPopover(true)} + disabled={addVizDisabled} + > + Panel actions + + ); + let flyout; if (isFlyoutVisible) { flyout = ( @@ -407,7 +418,7 @@ export const CustomPanelView = ({ title: 'Panel actions', items: [ { - name: 'Refresh panel', + name: 'Reload panel', onClick: () => { setPanelsMenuPopover(false); fetchCustomPanel(); @@ -513,21 +524,28 @@ export const CustomPanelView = ({ setPanelsMenuPopover(true)} - > - Panel actions - - } + button={panelActionsButton} isOpen={panelsMenuPopover} closePopover={() => setPanelsMenuPopover(false)} > + + + + + @@ -553,30 +571,13 @@ export const CustomPanelView = ({ isDisabled={dateDisabled} /> - - - - - - {panelVisualizations.length === 0 ? ( + {panelVisualizations.length === 0 && ( - ) : ( - <> )} Start by adding your first visualization - Use PPL Queries to fetch and filter Observability Data to Create Visualizations + Use PPL Queries to fetch & filter Observability Data and Create Visualizations diff --git a/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss index 79e517fc2..900a0f895 100644 --- a/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss +++ b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss @@ -23,7 +23,7 @@ .visualization-div { width: 100%; height: 90%; - overflow: scroll; + overflow: auto; text-align: center; } @@ -44,7 +44,7 @@ } .visualization-error-div { - overflow: scroll; + overflow: auto; position: relative; @extend %center-div; } diff --git a/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx index 49d50f349..2dc8450ec 100644 --- a/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx +++ b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx @@ -25,10 +25,7 @@ import { import React, { useEffect, useMemo, useState } from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; import PPLService from '../../../../services/requests/ppl'; -import { - displayVisualization, - renderSavedVisualization, -} from '../../helpers/utils'; +import { displayVisualization, renderSavedVisualization } from '../../helpers/utils'; import './visualization_container.scss'; /* @@ -93,6 +90,16 @@ export const VisualizationContainer = ({ { + closeActionsMenu(); + window.location.assign(`#/event_analytics/explorer/${savedVisualizationId}`); + }} + > + Edit + , + { closeActionsMenu(); showFlyout(true, visualizationId); diff --git a/dashboards-observability/public/components/explorer/visualizations/count_distribution/count_distribution.tsx b/dashboards-observability/public/components/explorer/visualizations/count_distribution/count_distribution.tsx index ca9236642..fd6997531 100644 --- a/dashboards-observability/public/components/explorer/visualizations/count_distribution/count_distribution.tsx +++ b/dashboards-observability/public/components/explorer/visualizations/count_distribution/count_distribution.tsx @@ -9,6 +9,7 @@ * GitHub history for details. */ +import { LONG_CHART_COLOR } from '../../../../../common/constants/shared'; import React from 'react'; import { Bar } from '../../../visualizations/charts/bar'; @@ -31,7 +32,7 @@ export const CountDistribution = ({ countDistribution }: any) => { pad: 0, }, height: 220, - colorway: ['#127871'], + colorway: [LONG_CHART_COLOR], }; return ( diff --git a/dashboards-observability/public/components/visualizations/charts/bar.tsx b/dashboards-observability/public/components/visualizations/charts/bar.tsx index 409d0f188..512977e13 100644 --- a/dashboards-observability/public/components/visualizations/charts/bar.tsx +++ b/dashboards-observability/public/components/visualizations/charts/bar.tsx @@ -13,22 +13,28 @@ import React from 'react'; import { take, merge } from 'lodash'; import { Plt } from '../plotly/plot'; -import { PlotlyColorWay } from '../../../../common/constants/shared'; +import { LONG_CHART_COLOR, PLOTLY_COLOR } from '../../../../common/constants/shared'; -export const Bar = ({ visualizations, barConfig = {}, layoutConfig = {}, isUniColor = false }: any) => { +export const Bar = ({ + visualizations, + barConfig = {}, + layoutConfig = {}, + isUniColor = false, +}: any) => { const { data, metadata: { fields }, } = visualizations; const stackLength = fields.length - 1; - // Individual bars have different colors when stackLength = 1 and chart is not unicolor + // Individual bars have different colors + // when: stackLength = 1 and length of result buckets < 16 and chart is not unicolor // Else each stacked bar has its own color using colorway let marker = {}; - if (stackLength == 1 && !isUniColor) { + if (stackLength == 1 && data[fields[stackLength].name].length < 16 && !isUniColor) { marker = { color: data[fields[stackLength].name].map((_: string, index: number) => { - return PlotlyColorWay[index % PlotlyColorWay.length]; + return PLOTLY_COLOR[index % PLOTLY_COLOR.length]; }), }; } @@ -56,11 +62,16 @@ export const Bar = ({ visualizations, barConfig = {}, layoutConfig = {}, isUniCo layoutConfig ); + // If chart has length of result buckets < 16 + // then use the LONG_CHART_COLOR for all the bars in the chart + const plotlyColorway = + data[fields[stackLength].name].length < 16 ? PLOTLY_COLOR : [LONG_CHART_COLOR]; + return ( { const { @@ -44,7 +44,7 @@ export const Line = ({ visualizations, lineConfig = {}, layoutConfig = {} }: any Date: Fri, 5 Nov 2021 12:18:13 -0700 Subject: [PATCH 2/4] Update plugin ID and bug fixes (#195) --- .../.cypress/integration/notebooks.spec.js | 6 +++--- .../integration/trace_analytics_dashboard.spec.js | 6 +++--- .../.cypress/integration/trace_analytics_services.spec.js | 8 ++++---- .../.cypress/integration/trace_analytics_traces.spec.js | 6 +++--- dashboards-observability/common/utils/query_utils.ts | 4 ++-- dashboards-observability/public/components/app.tsx | 4 ++-- .../public/components/common/search/date_picker.tsx | 1 + .../public/components/common/search/search.tsx | 2 +- .../public/components/custom_panels/helpers/utils.tsx | 6 +++--- .../components/__test__/legacy_route_helpers.test.ts | 6 +++--- .../trace_analytics/components/common/search_bar.tsx | 2 +- 11 files changed, 26 insertions(+), 25 deletions(-) diff --git a/dashboards-observability/.cypress/integration/notebooks.spec.js b/dashboards-observability/.cypress/integration/notebooks.spec.js index 58e3fa39d..2da84abc8 100644 --- a/dashboards-observability/.cypress/integration/notebooks.spec.js +++ b/dashboards-observability/.cypress/integration/notebooks.spec.js @@ -47,7 +47,7 @@ describe('Adding sample data and visualization', () => { describe('Testing notebooks table', () => { beforeEach(() => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability#/notebooks`); + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/notebooks`); cy.wait(delay * 3); }); @@ -134,7 +134,7 @@ describe('Testing notebooks table', () => { describe('Test reporting integration if plugin installed', () => { beforeEach(() => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability#/notebooks`); + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/notebooks`); cy.get('.euiTableCellContent').contains(TEST_NOTEBOOK).click(); cy.wait(delay * 3); cy.get('body').then($body => { @@ -184,7 +184,7 @@ describe('Test reporting integration if plugin installed', () => { describe('Testing paragraphs', () => { beforeEach(() => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability#/notebooks`); + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-dashboards#/notebooks`); cy.get('.euiTableCellContent').contains(TEST_NOTEBOOK).click(); cy.wait(delay * 3); }); diff --git a/dashboards-observability/.cypress/integration/trace_analytics_dashboard.spec.js b/dashboards-observability/.cypress/integration/trace_analytics_dashboard.spec.js index e7d26d9ef..8ada138a0 100644 --- a/dashboards-observability/.cypress/integration/trace_analytics_dashboard.spec.js +++ b/dashboards-observability/.cypress/integration/trace_analytics_dashboard.spec.js @@ -88,7 +88,7 @@ describe('Dump test data', () => { describe('Testing dashboard table empty state', () => { beforeEach(() => { - cy.visit('app/observability#/trace_analytics/home', { + cy.visit('app/observability-dashboards#/trace_analytics/home', { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, @@ -104,7 +104,7 @@ describe('Testing dashboard table empty state', () => { describe('Testing dashboard table', () => { beforeEach(() => { - cy.visit('app/observability#/trace_analytics/home', { + cy.visit('app/observability-dashboards#/trace_analytics/home', { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, @@ -163,7 +163,7 @@ describe('Testing dashboard table', () => { describe('Testing plots', () => { beforeEach(() => { - cy.visit('app/observability#/trace_analytics/home', { + cy.visit('app/observability-dashboards#/trace_analytics/home', { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, diff --git a/dashboards-observability/.cypress/integration/trace_analytics_services.spec.js b/dashboards-observability/.cypress/integration/trace_analytics_services.spec.js index fee885397..0a68ad93e 100644 --- a/dashboards-observability/.cypress/integration/trace_analytics_services.spec.js +++ b/dashboards-observability/.cypress/integration/trace_analytics_services.spec.js @@ -30,7 +30,7 @@ import { delay, SERVICE_NAME, setTimeFilter } from '../utils/constants'; describe('Testing services table empty state', () => { beforeEach(() => { - cy.visit('app/observability#/trace_analytics/services', { + cy.visit('app/observability-dashboards#/trace_analytics/services', { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, @@ -46,7 +46,7 @@ describe('Testing services table empty state', () => { describe('Testing services table', () => { beforeEach(() => { - cy.visit('app/observability#/trace_analytics/services', { + cy.visit('app/observability-dashboards#/trace_analytics/services', { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, @@ -77,7 +77,7 @@ describe('Testing service view empty state', () => { if (err.message.includes('ResizeObserver loop')) return false; }); - cy.visit(`app/observability#/trace_analytics/services/${SERVICE_NAME}`, { + cy.visit(`app/observability-dashboards#/trace_analytics/services/${SERVICE_NAME}`, { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, @@ -99,7 +99,7 @@ describe('Testing service view', () => { if (err.message.includes('ResizeObserver loop')) return false; }); - cy.visit(`app/observability#/trace_analytics/services/${SERVICE_NAME}`, { + cy.visit(`app/observability-dashboards#/trace_analytics/services/${SERVICE_NAME}`, { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, diff --git a/dashboards-observability/.cypress/integration/trace_analytics_traces.spec.js b/dashboards-observability/.cypress/integration/trace_analytics_traces.spec.js index 53ccdd4dc..473a68c32 100644 --- a/dashboards-observability/.cypress/integration/trace_analytics_traces.spec.js +++ b/dashboards-observability/.cypress/integration/trace_analytics_traces.spec.js @@ -30,7 +30,7 @@ import { delay, setTimeFilter, SPAN_ID, TRACE_ID } from '../utils/constants'; describe('Testing traces table empty state', () => { beforeEach(() => { - cy.visit('app/observability#/trace_analytics/traces', { + cy.visit('app/observability-dashboards#/trace_analytics/traces', { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, @@ -46,7 +46,7 @@ describe('Testing traces table empty state', () => { describe('Testing traces table', () => { beforeEach(() => { - cy.visit('app/observability#/trace_analytics/traces', { + cy.visit('app/observability-dashboards#/trace_analytics/traces', { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, @@ -77,7 +77,7 @@ describe('Testing traces table', () => { describe('Testing trace view', () => { beforeEach(() => { - cy.visit(`app/observability#/trace_analytics/traces/${TRACE_ID}`, { + cy.visit(`app/observability-dashboards#/trace_analytics/traces/${TRACE_ID}`, { onBeforeLoad: (win) => { win.sessionStorage.clear(); }, diff --git a/dashboards-observability/common/utils/query_utils.ts b/dashboards-observability/common/utils/query_utils.ts index a40e68b1b..51007f307 100644 --- a/dashboards-observability/common/utils/query_utils.ts +++ b/dashboards-observability/common/utils/query_utils.ts @@ -48,7 +48,7 @@ export const insertDateRangeToQuery = ({ const tokens = rawQuery.replaceAll(PPL_NEWLINE_REGEX, '').match(PPL_INDEX_INSERT_POINT_REGEX); if (isEmpty(tokens)) return finalQuery; - finalQuery = `${tokens![1]}=${tokens![2]} | where ${timeField} >= timestamp('${start}') and ${timeField} <= timestamp('${end}')${tokens![3]}`; + finalQuery = `${tokens![1]}=${tokens![2]} | where ${timeField} >= '${start}' and ${timeField} <= '${end}'${tokens![3]}`; return finalQuery; -}; \ No newline at end of file +}; diff --git a/dashboards-observability/public/components/app.tsx b/dashboards-observability/public/components/app.tsx index e1bfdebea..228d67432 100644 --- a/dashboards-observability/public/components/app.tsx +++ b/dashboards-observability/public/components/app.tsx @@ -14,7 +14,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { HashRouter, Route, Switch } from 'react-router-dom'; import { CoreStart } from '../../../../src/core/public'; -import { observabilityTitle } from '../../common/constants/shared'; +import { observabilityID, observabilityTitle } from '../../common/constants/shared'; import store from '../framework/redux/store'; import { AppPluginStartDependencies } from '../types'; import { renderPageWithSidebar } from './common/side_nav'; @@ -43,7 +43,7 @@ export const App = ({ const { chrome, http, notifications } = CoreStart; const parentBreadcrumb = { text: observabilityTitle, - href: 'observability#/', + href: `${observabilityID}#/`, }; const customPanelBreadcrumb = { diff --git a/dashboards-observability/public/components/common/search/date_picker.tsx b/dashboards-observability/public/components/common/search/date_picker.tsx index 53dd0733b..48311c71e 100644 --- a/dashboards-observability/public/components/common/search/date_picker.tsx +++ b/dashboards-observability/public/components/common/search/date_picker.tsx @@ -62,6 +62,7 @@ export function DatePicker(props: IDatePickerProps) { return ( { /> - + Final Query is as follows: * -> finalQuery = indexPartOfQuery + timeQueryFilter + panelFilterQuery + filterPartOfQuery * -> finalQuery = source=opensearch_dashboards_sample_data_flights - * + | where utc_time > timestamp(‘2021-07-01 00:00:00’) and utc_time < timestamp(‘2021-07-02 00:00:00’) + * + | where utc_time > ‘2021-07-01 00:00:00’ and utc_time < ‘2021-07-02 00:00:00’ * + | where Carrier='OpenSearch-Air' * + | stats sum(FlightDelayMin) as delays by Carrier */ @@ -102,9 +102,9 @@ const queryAccumulator = ( } const indexPartOfQuery = indexMatchArray[0]; const filterPartOfQuery = originalQuery.replace(PPL_INDEX_REGEX, ''); - const timeQueryFilter = ` | where ${timestampField} >= timestamp('${convertDateTime( + const timeQueryFilter = ` | where ${timestampField} >= '${convertDateTime( startTime - )}') and ${timestampField} <= timestamp('${convertDateTime(endTime, false)}')`; + )}' and ${timestampField} <= '${convertDateTime(endTime, false)}'`; const pplFilterQuery = panelFilterQuery === '' ? '' : ` | ${panelFilterQuery}`; return indexPartOfQuery + timeQueryFilter + pplFilterQuery + filterPartOfQuery; }; diff --git a/dashboards-observability/public/components/notebooks/components/__test__/legacy_route_helpers.test.ts b/dashboards-observability/public/components/notebooks/components/__test__/legacy_route_helpers.test.ts index 2097084c4..70ecd5425 100644 --- a/dashboards-observability/public/components/notebooks/components/__test__/legacy_route_helpers.test.ts +++ b/dashboards-observability/public/components/notebooks/components/__test__/legacy_route_helpers.test.ts @@ -32,9 +32,9 @@ describe('Test legacy route helpers', () => { }, ] as Location[]; const expected = [ - '/app/observability#/notebooks/GQ5icXwBJCegTOBKO4Um', - '/app/observability#/notebooks/clPiPXwBEM7l9gC0xTpA?view=view_both', - `/testBasePath/app/observability#/notebooks/GQ5icXwBJCegTOBKO4Um?_g=(time:(from:'2021-10-15T20:25:09.556Z',to:'2021-10-15T20:55:09.556Z'))&view=output_only&security_tenant=global`, + '/app/observability-dashboards#/notebooks/GQ5icXwBJCegTOBKO4Um', + '/app/observability-dashboards#/notebooks/clPiPXwBEM7l9gC0xTpA?view=view_both', + `/testBasePath/app/observability-dashboards#/notebooks/GQ5icXwBJCegTOBKO4Um?_g=(time:(from:'2021-10-15T20:25:09.556Z',to:'2021-10-15T20:55:09.556Z'))&view=output_only&security_tenant=global`, ] as RedirectProps['to'][]; expect(locations.map((location) => convertLegacyNotebooksUrl(location))).toEqual(expected); }); diff --git a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx index 3fe206047..0fa2942c7 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx @@ -96,7 +96,7 @@ export function SearchBar(props: SearchBarOwnProps) { /> )} - + {renderDatePicker(props.startTime, props.setStartTime, props.endTime, props.setEndTime)} From ae70b379ca165dc9c8612be725a8827da308c183 Mon Sep 17 00:00:00 2001 From: Eric Wei <80358241+mengweieric@users.noreply.github.com> Date: Fri, 5 Nov 2021 14:55:14 -0700 Subject: [PATCH 3/4] Feature event analytics imporovements and fixes (#199) * redo signoff Signed-off-by: Eric Wei * removed comments Signed-off-by: Eric Wei * signoff amendment Signed-off-by: Eric Wei --- .../components/common/search/autocomplete.tsx | 107 +++-- .../components/common/search/date_picker.tsx | 2 +- .../components/common/search/search.scss | 8 +- .../components/common/search/search.tsx | 5 +- .../public/components/explorer/data_grid.scss | 1 + .../components/explorer/event_analytics.tsx | 117 +++-- .../public/components/explorer/explorer.tsx | 453 +++++++++++------- .../public/components/explorer/home.scss | 20 + .../public/components/explorer/home.tsx | 251 ++++++---- .../explorer/home_table/history_table.tsx | 112 +++-- .../explorer/hooks/use_fetch_events.ts | 1 - .../components/explorer/log_explorer.scss | 11 +- .../components/explorer/log_explorer.tsx | 75 ++- .../public/components/explorer/no_results.tsx | 2 +- .../components/explorer/sidebar/field.tsx | 23 +- .../components/explorer/sidebar/sidebar.tsx | 28 +- .../explorer/slices/query_tab_slice.ts | 13 +- .../event_analytics/saved_objects.ts | 37 +- .../event_analytics/event_analytics_router.ts | 44 +- .../server/services/facets/saved_objects.ts | 22 +- 20 files changed, 845 insertions(+), 487 deletions(-) create mode 100644 dashboards-observability/public/components/explorer/home.scss diff --git a/dashboards-observability/public/components/common/search/autocomplete.tsx b/dashboards-observability/public/components/common/search/autocomplete.tsx index be7d42460..9c424d0b1 100644 --- a/dashboards-observability/public/components/common/search/autocomplete.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.tsx @@ -333,7 +333,7 @@ export function Autocomplete({ }, } ); - }, []); + }, [query]); return (
-
- {autocompleteState.isOpen && - autocompleteState.collections.map((collection, index) => { - const { source, items } = collection; - return ( -
-
- {items.length > 0 && ( -
    - {items.map((item, index) => { - const prefix = item.input.split(' '); - return ( -
  • -
    -
    -
    -
    - ${prefix[prefix.length-1]}${item.suggestion} -
    ` - }} - /> + {autocompleteState.isOpen && ( +
    + {autocompleteState.collections.map((collection, index) => { + const { source, items } = collection; + return ( +
    +
    + {items.length > 0 && ( +
      + {items.map((item, index) => { + const prefix = item.input.split(' '); + return ( +
    • +
      +
      +
      +
      + ${prefix[prefix.length-1]}${item.suggestion} +
      ` + }} + /> +
      -
    -
  • - ); - })} -
- )} + + ); + })} + + )} +
-
- ); - })} -
+ ); + })} + + )} ); } diff --git a/dashboards-observability/public/components/common/search/date_picker.tsx b/dashboards-observability/public/components/common/search/date_picker.tsx index 48311c71e..b78dee6db 100644 --- a/dashboards-observability/public/components/common/search/date_picker.tsx +++ b/dashboards-observability/public/components/common/search/date_picker.tsx @@ -61,7 +61,7 @@ export function DatePicker(props: IDatePickerProps) { return ( { savedObjects, showSavePanelOptionsList, showSaveButton = true, - setToast + setToast, + runButtonText } = props; const [isSavePanelOpen, setIsSavePanelOpen] = useState(false); @@ -140,7 +141,7 @@ export const Search = (props: any) => { memorizedHandleQuerySearch(); }} > - { isEmpty(explorerData) ? 'Run' : 'Refresh' } + { runButtonText ? runButtonText : isEmpty(explorerData) ? 'Run' : 'Refresh' } { showSaveButton && ( diff --git a/dashboards-observability/public/components/explorer/data_grid.scss b/dashboards-observability/public/components/explorer/data_grid.scss index 85c7858c6..1f89c5278 100644 --- a/dashboards-observability/public/components/explorer/data_grid.scss +++ b/dashboards-observability/public/components/explorer/data_grid.scss @@ -232,6 +232,7 @@ discover-app { .dscWrapper { padding-left: $euiSizeXL; padding-right: $euiSizeS; + margin-top: $euiSizeM; z-index: 1; @include euiBreakpoint('xs', 's', 'm') { padding-left: $euiSizeS; diff --git a/dashboards-observability/public/components/explorer/event_analytics.tsx b/dashboards-observability/public/components/explorer/event_analytics.tsx index a873fed53..caebaf309 100644 --- a/dashboards-observability/public/components/explorer/event_analytics.tsx +++ b/dashboards-observability/public/components/explorer/event_analytics.tsx @@ -10,12 +10,14 @@ */ import React, { useState, ReactChild } from 'react'; +import { isEmpty } from 'lodash'; import { HashRouter, Route, Switch } from 'react-router-dom'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; import { EuiGlobalToastList } from '@elastic/eui'; import { LogExplorer } from './log_explorer'; import { Home as EventExplorerHome } from './home'; import { renderPageWithSidebar } from '../common/side_nav'; +import { RAW_QUERY } from '../../../common/constants/explorer'; export const EventAnalytics = ({ chrome, @@ -40,6 +42,21 @@ export const EventAnalytics = ({ setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); }; + const getExistingEmptyTab = ({ tabIds, queries, explorerData }) => { + let emptyTabId = ''; + for (let i = 0; i < tabIds.length; i++) { + const tid = tabIds[i]; + if ( + isEmpty(queries[tid][RAW_QUERY]) && + isEmpty(explorerData[tid]) + ) { + emptyTabId = tid; + break; + } + } + return emptyTabId; + }; + return ( <> - - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - eventAnalyticsBreadcrumb, - { - text: 'Explorer', - href: '#/event_analytics/explorer', - }, - ]); - return ( - - ); - }} - /> - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - eventAnalyticsBreadcrumb, - { - text: 'Home', - href: '#/event_analytics', - } - ]); - return renderPageWithSidebar( - - ); - }} - /> - - + + { + chrome.setBreadcrumbs([ + parentBreadcrumb, + eventAnalyticsBreadcrumb, + { + text: 'Explorer', + href: `#/event_analytics/explorer`, + } + ]); + return ( + + ); + }} + /> + { + chrome.setBreadcrumbs([ + parentBreadcrumb, + eventAnalyticsBreadcrumb, + { + text: 'Home', + href: '#/event_analytics', + } + ]); + return renderPageWithSidebar( + + ); + }} + /> + + ); } \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/explorer.tsx b/dashboards-observability/public/components/explorer/explorer.tsx index c300a2db7..fb5ba68d0 100644 --- a/dashboards-observability/public/components/explorer/explorer.tsx +++ b/dashboards-observability/public/components/explorer/explorer.tsx @@ -40,7 +40,10 @@ import { NoResults } from './no_results'; import { HitsCounter } from './hits_counter/hits_counter'; import { TimechartHeader } from './timechart_header'; import { ExplorerVisualizations } from './visualizations'; -import { IField, IQueryTab } from '../../../common/types/explorer'; +import { + IField, + IQueryTab +} from '../../../common/types/explorer'; import { TAB_CHART_TITLE, TAB_EVENT_TITLE, @@ -54,14 +57,29 @@ import { AVAILABLE_FIELDS, INDEX, TIME_INTERVAL_OPTIONS, - HAS_SAVED_TIMESTAMP, + HAS_SAVED_TIMESTAMP } from '../../../common/constants/explorer'; -import { PPL_STATS_REGEX } from '../../../common/constants/shared'; -import { getIndexPatternFromRawQuery, insertDateRangeToQuery } from '../../../common/utils'; -import { useFetchEvents, useFetchVisualizations } from './hooks'; -import { changeQuery, changeDateRange, selectQueries } from './slices/query_slice'; +import { PPL_STATS_REGEX, PPL_NEWLINE_REGEX } from '../../../common/constants/shared'; +import { + getIndexPatternFromRawQuery, + insertDateRangeToQuery +} from '../../../common/utils'; +import { + useFetchEvents, + useFetchVisualizations, +} from './hooks'; +import { + changeQuery, + changeDateRange, + selectQueries +} from './slices/query_slice'; import { selectQueryResult } from './slices/query_result_slice'; -import { selectFields, updateFields, sortFields } from './slices/field_slice'; +import { + selectFields, + updateFields, + sortFields +} from './slices/field_slice'; +import { updateTabName, selectQueryTabs } from './slices/query_tab_slice'; import { selectCountDistribution } from './slices/count_distribution_slice'; import { selectExplorerVisualization } from './slices/visualization_slice'; import PPLService from '../../services/requests/ppl'; @@ -69,8 +87,12 @@ import DSLService from '../../services/requests/dsl'; import SavedObjects from '../../services/saved_objects/event_analytics/saved_objects'; import TimestampUtils from 'public/services/timestamp/timestamp'; -const TAB_EVENT_ID = uniqueId(TAB_EVENT_ID_TXT_PFX); -const TAB_CHART_ID = uniqueId(TAB_CHART_ID_TXT_PFX); +const TAB_EVENT_ID = 'main-content-events'; +const TAB_CHART_ID = 'main-content-vis'; +const TYPE_TAB_MAPPING = { + 'savedQuery': TAB_EVENT_ID, + 'savedVisualization': TAB_CHART_ID +}; interface IExplorerProps { pplService: PPLService; @@ -92,17 +114,25 @@ export const Explorer = ({ tabId, savedObjects, timestampUtils, - setToast, + setToast }: IExplorerProps) => { const dispatch = useDispatch(); - const requestParams = { tabId }; - const { isEventsLoading, getEvents, getAvailableFields } = useFetchEvents({ + const requestParams = { tabId, }; + const { + isEventsLoading, + getEvents, + getAvailableFields + } = useFetchEvents({ pplService, - requestParams, + requestParams }); - const { isVisLoading, getVisualizations, getCountVisualizations } = useFetchVisualizations({ + const { + isVisLoading, + getVisualizations, + getCountVisualizations + } = useFetchVisualizations({ pplService, - requestParams, + requestParams }); const query = useSelector(selectQueries)[tabId]; @@ -110,13 +140,14 @@ export const Explorer = ({ const explorerFields = useSelector(selectFields)[tabId]; const countDistribution = useSelector(selectCountDistribution)[tabId]; const explorerVisualizations = useSelector(selectExplorerVisualization)[tabId]; - + const queryTabName = useSelector(selectQueryTabs)['tabNames'][tabId]; + const [selectedContentTabId, setSelectedContentTab] = useState(TAB_EVENT_ID); const [selectedCustomPanelOptions, setSelectedCustomPanelOptions] = useState([]); const [selectedPanelName, setSelectedPanelName] = useState(''); const [curVisId, setCurVisId] = useState('bar'); const [prevIndex, setPrevIndex] = useState(''); - const [isPanelTextFieldInvalid, setIsPanelTextFieldInvalid] = useState(false); + const [isPanelTextFieldInvalid, setIsPanelTextFieldInvalid ] = useState(false); const [isSidebarClosed, setIsSidebarClosed] = useState(false); const [timeIntervalOptions, setTimeIntervalOptions] = useState(TIME_INTERVAL_OPTIONS); const [isOverridingTimestamp, setIsOverridingTimestamp] = useState(false); @@ -124,9 +155,11 @@ export const Explorer = ({ const queryRef = useRef(); const selectedPanelNameRef = useRef(); const explorerFieldsRef = useRef(); + const queryTabsRef = useRef(); queryRef.current = query; selectedPanelNameRef.current = selectedPanelName; explorerFieldsRef.current = explorerFields; + // queryTabsRef.current = queryTabs; let minInterval = 'y'; const findAutoInterval = (startTime: string, endTime: string) => { @@ -166,29 +199,81 @@ export const Explorer = ({ }); }; + const getSavedDataById = async (objectId: string) => { + // load saved query/visualization if object id exists + await savedObjects.fetchSavedObjects({ + objectId, + }) + .then((res) => { + const savedData = res['observabilityObjectList'][0]; + const isSavedQuery = has(savedData, 'savedQuery'); + const objectData = isSavedQuery ? savedData.savedQuery : savedData.savedVisualization; + batch(async () => { + await dispatch(changeQuery({ + tabId, + query: { + [RAW_QUERY]: objectData?.query || '', + [SELECTED_TIMESTAMP]: objectData?.selected_timestamp?.name || 'timestamp', + [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'] + })); + }); + + // populate name field in save panel for default name + setSelectedPanelName(objectData?.name || ''); + setCurVisId(objectData?.type || 'bar'); + const tabToBeFocused = isSavedQuery ? TYPE_TAB_MAPPING['savedQuery'] : TYPE_TAB_MAPPING['savedVisualization'] + setSelectedContentTab(tabToBeFocused); + }) + .catch((error) => { + setToast(`Cannot get saved data for object id: ${objectId}, error: ${error.message}`, 'danger'); + }); + }; + + const fetchSavedResult = async () => { + const curQuery = queryRef.current; + if (!isEmpty(curQuery!['savedObjectId'])) { + await getSavedDataById(curQuery!['savedObjectId']); + } + }; + const fetchData = async () => { const curQuery = queryRef.current; const rawQueryStr = curQuery![RAW_QUERY]; const curIndex = getIndexPatternFromRawQuery(rawQueryStr); - if (isEmpty(rawQueryStr)) return; + if ( + isEmpty(rawQueryStr) || + (!isEmpty(curQuery!['savedObjectId']) && isEmpty(queryTabName)) + ) return; if (isEmpty(curIndex)) { setToast('Query does not include vaild index.', 'danger'); return; } - + let curTimestamp = ''; let hasSavedTimestamp = false; // determines timestamp for search if (isEmpty(curQuery![SELECTED_TIMESTAMP]) || !isEqual(curIndex, prevIndex)) { - const savedTimestamps = await savedObjects - .fetchSavedObjects({ - objectId: curIndex, - }) - .catch((error: any) => { - console.error(`Unable to get saved timestamp for this index: ${error.message}`); - }); + const savedTimestamps = await savedObjects.fetchSavedObjects({ + objectId: curIndex + }).catch((error: any) => { + console.log(`Unable to get saved timestamp for this index: ${error.message}`); + }); if (savedTimestamps?.observabilityObjectList[0]?.timestamp?.name) { // from saved objects hasSavedTimestamp = true; @@ -208,24 +293,22 @@ export const Explorer = ({ // compose final query const finalQuery = composeFinalQuery(curQuery, curTimestamp || curQuery![SELECTED_TIMESTAMP]); - - await dispatch( - changeQuery({ - tabId, - query: { - finalQuery, - [SELECTED_TIMESTAMP]: curTimestamp || curQuery![SELECTED_TIMESTAMP], - [HAS_SAVED_TIMESTAMP]: hasSavedTimestamp, - }, - }) - ); + + await dispatch(changeQuery({ + tabId, + query: { + finalQuery, + [SELECTED_TIMESTAMP]: curTimestamp || curQuery![SELECTED_TIMESTAMP], + [HAS_SAVED_TIMESTAMP]: hasSavedTimestamp + } + })); // search if (rawQueryStr.match(PPL_STATS_REGEX)) { getVisualizations(); getAvailableFields(`search source=${curIndex}`); } else { - findAutoInterval(curQuery![SELECTED_DATE_RANGE][0], curQuery![SELECTED_DATE_RANGE][1]); + findAutoInterval(curQuery![SELECTED_DATE_RANGE][0], curQuery![SELECTED_DATE_RANGE][1]) getEvents(); getCountVisualizations(minInterval); } @@ -234,57 +317,64 @@ export const Explorer = ({ setPrevIndex(curTimestamp || curQuery![SELECTED_TIMESTAMP]); }; - // should run in two usecases - // 1. load explorer for the first time - // 2. when overrides default timestamp - useEffect(() => { - fetchData(); - }, [query[SELECTED_TIMESTAMP]]); + useEffect( + () => { + if ( + (isEmpty(query.savedObjectId) && !isEmpty(query[RAW_QUERY])) || + query.savedObjectId + ) { + fetchSavedResult(); + fetchData(); + } + }, + [ + query.savedObjectId, + queryTabName + ] + ); const handleAddField = (field: IField) => toggleFields(field, AVAILABLE_FIELDS, SELECTED_FIELDS); - const handleRemoveField = (field: IField) => - toggleFields(field, SELECTED_FIELDS, AVAILABLE_FIELDS); + const handleRemoveField = (field: IField) => toggleFields(field, SELECTED_FIELDS, AVAILABLE_FIELDS); const handleTimePickerChange = async (timeRange: Array) => { - await dispatch( - changeDateRange({ - tabId: requestParams.tabId, - data: { - [SELECTED_DATE_RANGE]: timeRange, - }, - }) - ); + await dispatch(changeDateRange({ + tabId: requestParams.tabId, + data: { + [SELECTED_DATE_RANGE]: timeRange + } + })); fetchData(); - }; + } /** * Toggle fields between selected and unselected sets * @param field field to be toggled * @param FieldSetToRemove set where this field to be removed from * @param FieldSetToAdd set where this field to be added - */ - const toggleFields = (field: IField, FieldSetToRemove: string, FieldSetToAdd: string) => { + */ + const toggleFields = ( + field: IField, + FieldSetToRemove: string, + FieldSetToAdd: string + ) => { + const nextFields = cloneDeep(explorerFields); const thisFieldSet = nextFields[FieldSetToRemove]; const nextFieldSet = thisFieldSet.filter((fd: IField) => fd.name !== field.name); nextFields[FieldSetToRemove] = nextFieldSet; nextFields[FieldSetToAdd].push(field); batch(() => { - dispatch( - updateFields({ - tabId, - data: { - ...nextFields, - }, - }) - ); - dispatch( - sortFields({ - tabId, - data: [FieldSetToAdd], - }) - ); + dispatch(updateFields({ + tabId, + data: { + ...nextFields + } + })); + dispatch(sortFields({ + tabId, + data: [FieldSetToAdd] + })); }); }; @@ -305,13 +395,13 @@ export const Explorer = ({ index: curIndex, name: timestamp.name, type: timestamp.type, - dsl_type: 'date', + dsl_type: 'date' }; - if (isEmpty(rawQueryStr) || isEmpty(curIndex)) { + if (isEmpty(rawQueryStr) || isEmpty(curIndex)) { setToast('Cannot override timestamp because there was no valid index found.', 'danger'); return; } - + setIsOverridingTimestamp(true); let saveTimestampRes; @@ -347,14 +437,14 @@ export const Explorer = ({ if (!has(saveTimestampRes, 'objectId')) return; - await dispatch( - changeQuery({ - tabId, - query: { - [SELECTED_TIMESTAMP]: '', - }, - }) - ); + await dispatch(changeQuery({ + tabId, + query: { + [SELECTED_TIMESTAMP]: '' + } + })); + + fetchData(); }; const getMainContent = () => { @@ -373,10 +463,10 @@ export const Explorer = ({ explorerFields={ explorerFields } explorerData={ explorerData } selectedTimestamp={ query[SELECTED_TIMESTAMP] } - isOverridingTimestamp={ isOverridingTimestamp } handleOverrideTimestamp={ handleOverrideTimestamp } handleAddField={ (field: IField) => handleAddField(field) } handleRemoveField={ (field: IField) => handleRemoveField(field) } + isFieldToggleButtonDisabled={isEmpty(explorerData.jsonData) || !isEmpty(queryRef.current![RAW_QUERY].match(PPL_STATS_REGEX))} /> )} @@ -437,6 +527,7 @@ export const Explorer = ({ ) } +
JSX.Element; + tabId: string, + tabTitle: string, + getContent: () => JSX.Element }) { return { id: tabId, - name: ( + name: (<> + + { tabTitle } + + ), + content: ( <> - - {tabTitle} - - - ), - content: <>{getContent()}, + { getContent() } + ) }; - } + }; const getExplorerVis = () => { return ( ); }; 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(), - }), + getMainContentTab( + { + tabId: TAB_EVENT_ID, + tabTitle: TAB_EVENT_TITLE, + getContent: () => getMainContent() + } + ), + getMainContentTab( + { + tabId: TAB_CHART_ID, + tabTitle: TAB_CHART_TITLE, + getContent: () => getExplorerVis() + } + ) ]; }; @@ -529,31 +629,33 @@ export const Explorer = ({ isSidebarClosed, countDistribution, explorerVisualizations, + selectedContentTabId, isOverridingTimestamp ] ); const handleContentTabClick = (selectedTab: IQueryTab) => setSelectedContentTab(selectedTab.id); - + const handleQuerySearch = () => fetchData(); const handleQueryChange = async (query: string, index: string) => { await dispatch(changeQuery({ tabId, query: { - [RAW_QUERY]: query, + [RAW_QUERY]: query.replaceAll(PPL_NEWLINE_REGEX, ''), [INDEX]: index }, })); } const handleSavingObject = async () => { + const currQuery = queryRef.current; const currFields = explorerFieldsRef.current; - if (isEmpty(currQuery![RAW_QUERY])) { + if (isEmpty(currQuery![RAW_QUERY])) { setToast('No query to save.', 'danger'); - return; - } + return; + }; if (isEmpty(selectedPanelNameRef.current)) { setIsPanelTextFieldInvalid(true); @@ -562,84 +664,95 @@ export const Explorer = ({ } setIsPanelTextFieldInvalid(false); + const params = { + query: currQuery![RAW_QUERY], + fields: currFields![SELECTED_FIELDS], + dateRange: currQuery![SELECTED_DATE_RANGE], + name: selectedPanelNameRef.current, + timestamp: currQuery![SELECTED_TIMESTAMP] + }; + if (isEqual(selectedContentTabId, TAB_EVENT_ID)) { - // 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], - }) + if (!isEmpty(currQuery!['savedObjectId'])) { + params['objectId'] = currQuery!['savedObjectId']; + await savedObjects.updateSavedQueryById(params) .then((res: any) => { - setToast( - `Query '${selectedPanelNameRef.current}' has been successfully saved.`, - 'success' - ); + setToast(`Query '${selectedPanelNameRef.current}' has been successfully updated.`, 'success'); + return res; }) .catch((error: any) => { - setToast( - `Cannot save query '${selectedPanelNameRef.current}', error: ${error.message}`, - 'danger' - ); + setToast(`Cannot update query '${selectedPanelNameRef.current}', error: ${error.message}`, 'danger'); + }); + } else { + // create new saved query + savedObjects.createSavedQuery(params) + .then((res: any) => { + setToast(`Query '${selectedPanelNameRef.current}' has been successfully saved.`, 'success'); + }) + .catch((error: any) => { + setToast(`Cannot save query '${selectedPanelNameRef.current}', error: ${error.message}`, 'danger'); }); + } // 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(explorerVisualizations)) { setToast(`There is no query or(and) visualization to save`, 'danger'); return; } - - // create new saved visualization - const savingVisRes = await savedObjects - .createSavedVisualization({ + + let savingVisRes; + + if (!isEmpty(currQuery!['savedObjectId'])) { + params['objectId'] = currQuery!['savedObjectId']; + params['type'] = curVisId; + savingVisRes = await savedObjects.updateSavedVisualizationById(params) + .then((res: any) => { + setToast(`Visualization '${selectedPanelNameRef.current}' has been successfully updated.`, 'success'); + return res; + }) + .catch((error: any) => { + setToast(`Cannot update Visualization '${selectedPanelNameRef.current}', error: ${error.message}`, 'danger'); + }); + } else { + // create new saved visualization + savingVisRes = await savedObjects.createSavedVisualization({ query: currQuery![RAW_QUERY], fields: currFields![SELECTED_FIELDS], dateRange: currQuery![SELECTED_DATE_RANGE], type: curVisId, name: selectedPanelNameRef.current, - timestamp: currQuery![SELECTED_TIMESTAMP], + timestamp: currQuery![SELECTED_TIMESTAMP] }) .then((res: any) => { - setToast( - `Visualization '${selectedPanelNameRef.current}' has been successfully saved.`, - 'success' - ); + setToast(`Visualization '${selectedPanelNameRef.current}' has been successfully saved.`, 'success'); return res; }) .catch((error: any) => { - setToast( - `Cannot save Visualization '${selectedPanelNameRef.current}', error: ${error.message}`, - 'danger' - ); + setToast(`Cannot save Visualization '${selectedPanelNameRef.current}', error: ${error.message}`, 'danger'); }); + } 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) => { - setToast( - `Cannot add Visualization '${selectedPanelNameRef.current}' to operation panels, error: ${error.message}`, - 'danger' - ); - }); + + 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) => { + setToast(`Cannot add Visualization '${selectedPanelNameRef.current}' to operation panels, error: ${error.message}`, 'danger'); + }); } } }; @@ -669,12 +782,10 @@ export const Explorer = ({ /> { - tab.id === selectedContentTabId; - })} - onTabClick={(selectedTab: EuiTabbedContentTab) => handleContentTabClick(selectedTab)} - tabs={memorizedMainContentTabs} + initialSelectedTab={ memorizedMainContentTabs[0] } + selectedTab={ memorizedMainContentTabs.find(tab => tab.id === selectedContentTabId) } + onTabClick={ (selectedTab: EuiTabbedContentTab) => handleContentTabClick(selectedTab) } + tabs={ memorizedMainContentTabs } /> ); diff --git a/dashboards-observability/public/components/explorer/home.scss b/dashboards-observability/public/components/explorer/home.scss new file mode 100644 index 000000000..a170283cd --- /dev/null +++ b/dashboards-observability/public/components/explorer/home.scss @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +#event-home { + float: left; + width: 100%; + max-width: 1130px; +} + +#home-his-title { + padding-left: 10px; +} \ No newline at end of file diff --git a/dashboards-observability/public/components/explorer/home.tsx b/dashboards-observability/public/components/explorer/home.tsx index 1c6ab32f1..23f665456 100644 --- a/dashboards-observability/public/components/explorer/home.tsx +++ b/dashboards-observability/public/components/explorer/home.tsx @@ -9,8 +9,10 @@ * GitHub history for details. */ -import React, { useState } from 'react'; -import { useDispatch, batch } from 'react-redux'; +import './home.scss'; + +import React, { useState, ReactElement, useRef } from 'react'; +import { useDispatch, batch, useSelector } from 'react-redux'; import { uniqueId } from 'lodash'; import { useHistory } from 'react-router-dom'; import { @@ -20,10 +22,13 @@ import { EuiPageHeaderSection, EuiTitle, EuiPageContent, - EuiListGroup, EuiSpacer, EuiFlexGroup, - EuiFlexItem + EuiFlexItem, + EuiButton, + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, } from '@elastic/eui'; import { Search } from '../common/search/search'; import { @@ -36,20 +41,26 @@ import { } from '../../../common/constants/explorer'; import { useEffect } from 'react'; import SavedObjects from '../../services/saved_objects/event_analytics/saved_objects'; -import { addTab } from './slices/query_tab_slice'; -import { init as initFields, updateFields } from './slices/field_slice'; +import { addTab, selectQueryTabs } from './slices/query_tab_slice'; +import { init as initFields } from './slices/field_slice'; import { init as initQuery, changeQuery } from './slices/query_slice'; -import { init as initQueryResult } from './slices/query_result_slice'; -import { Table } from './home_table/history_table'; - +import { init as initQueryResult, selectQueryResult } from './slices/query_result_slice'; +import { Histories as EventHomeHistories } from './home_table/history_table'; +import { selectQueries } from './slices/query_slice'; interface IHomeProps { pplService: any; dslService: any; savedObjects: SavedObjects; + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void; } export const Home = (props: IHomeProps) => { @@ -57,12 +68,26 @@ export const Home = (props: IHomeProps) => { pplService, dslService, savedObjects, + setToast, + getExistingEmptyTab } = props; const history = useHistory(); const dispatch = useDispatch(); + + const queries = useSelector(selectQueries); + const explorerData = useSelector(selectQueryResult); + const tabIds = useSelector(selectQueryTabs)['queryTabIds'] + const queryRef = useRef(); + const tabIdsRef = useRef(); + const explorerDataRef = useRef(); + queryRef.current = queries; + tabIdsRef.current = tabIds; + explorerDataRef.current = explorerData; + const [searchQuery, setSearchQuery] = useState(''); const [selectedDateRange, setSelectedDateRange] = useState>(['now-15m', 'now']); const [savedHistories, setSavedHistories] = useState([]); + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const fetchHistories = async () => { const res = await savedObjects.fetchSavedObjects({ @@ -70,7 +95,24 @@ export const Home = (props: IHomeProps) => { sortOrder: 'desc', fromIndex: 0 }); - setSavedHistories(res['observabilityObjectList'] || []); + setSavedHistories(res['observabilityObjectList']); + }; + + const deleteHistoryById = async(objectId: string, name: string) => { + await savedObjects + .deleteSavedObjectsById({ objectId }) + .then(async (res) => { + setSavedHistories((staleHistories) => { + return staleHistories.filter((his) => { + return his.objectId !== objectId; + }); + }); + setToast(`History '${name}' has been successfully deleted.`, 'success'); + }) + .catch((error) => { + console.log('delete error: ', error); + setToast(`Cannot delete history '${name}', error: ${error.message}`, 'danger'); + }); }; const addNewTab = async () => { @@ -103,8 +145,13 @@ export const Home = (props: IHomeProps) => { } const handleQuerySearch = async () => { - // create new tab - const newTabId = await addNewTab(); + + const emptyTabId = getExistingEmptyTab({ + tabIds: tabIdsRef.current, + queries: queryRef.current, + explorerData: explorerDataRef.current + }); + const newTabId = emptyTabId ? emptyTabId : await addNewTab(); // update this new tab with data await addSearchInput(newTabId); @@ -117,104 +164,100 @@ export const Home = (props: IHomeProps) => { const handleTimePickerChange = async (timeRange: Array) => setSelectedDateRange(timeRange); - const addSavedQueryInput = async ( - tabId: string, - searchQuery: string, - selectedDateRange: [], - selectedTimeStamp: string - ) => { - dispatch( - changeQuery({ - tabId, - query: { - [RAW_QUERY]: searchQuery, - [SELECTED_DATE_RANGE]: selectedDateRange, - [SELECTED_TIMESTAMP]: selectedTimeStamp, - }, - }) - ); - }; - - const addSavedFields = async ( - tabId: string, - selectedFields: [] - ) => { - dispatch( - updateFields({ - tabId, - data: { - [SELECTED_FIELDS]: selectedFields, - }, - }) - ); - }; - - const savedQuerySearch = async (searchQuery: string, selectedDateRange: [], selectedTimeStamp: string, selectedFields: []) => { - // create new tab - const newTabId = await addNewTab(); - - // update this new tab with data - await addSavedQueryInput(newTabId, searchQuery, selectedDateRange, selectedTimeStamp); - await addSavedFields(newTabId, selectedFields); - + const handleHistoryClick = async (objectId: string) => { // redirect to explorer - history.push('/event_analytics/explorer'); + history.push(`/event_analytics/explorer/${objectId}`); }; + const popoverButton = ( + setIsActionsPopoverOpen(!isActionsPopoverOpen)} + > + Actions + + ); + + const popoverItems: ReactElement[] = [ + { + setIsActionsPopoverOpen(false); + history.push(`/event_analytics/explorer`); + }} + > + Event Explorer + + ]; return ( -
- - - - - -

Event Analytics

-
-
-
-
-
- - {} } - setEndTime={ () => {} } - setIsOutputStale={ () => {} } - liveStreamChecked={ false } - onLiveStreamChange={ () => {} } - showSaveButton={ false } - /> - - - - + + + + +

Event Analytics

+
+
+
+ + + + {} } + setEndTime={ () => {} } + setIsOutputStale={ () => {} } + liveStreamChecked={ false } + onLiveStreamChange={ () => {} } + showSaveButton={ false } + runButtonText="New Query" + /> + + + + + - -

{ "Queries and Visualizations" }

-
- - - { savedQuerySearch(searchQuery, selectedDateRange, selectedTimeStamp, selectedFields) } } + + +

{ "Queries and Visualizations" }

+
+
+ + setIsActionsPopoverOpen(false)} + > + + + + + + + + + - - - - - + + + + + ); }; diff --git a/dashboards-observability/public/components/explorer/home_table/history_table.tsx b/dashboards-observability/public/components/explorer/home_table/history_table.tsx index 566697855..7fd0b69f0 100644 --- a/dashboards-observability/public/components/explorer/home_table/history_table.tsx +++ b/dashboards-observability/public/components/explorer/home_table/history_table.tsx @@ -10,21 +10,26 @@ */ import React, { useState, useRef } from 'react'; - -import { EuiSpacer, EuiLink, EuiInMemoryTable, EuiIcon, EuiLoadingChart } from '@elastic/eui'; +import { + EuiLink, + EuiInMemoryTable, + EuiIcon, + EuiLoadingChart, + EuiButtonIcon +} from '@elastic/eui'; import { FILTER_OPTIONS } from '../../../../common/constants/explorer'; interface TableData { - savedHistory: []; - savedQuerySearch: ( - searchQuery: string, - selectedDateRange: [], - selectedTimeStamp, - selectedFields: [] - ) => void; + savedHistories: Array; + handleHistoryClick: (objectId: string) => void; + handleDeleteHistory: (objectId: string, type: string) => void; } -export function Table(options: TableData) { +export function Histories({ + savedHistories, + handleHistoryClick, + handleDeleteHistory +}: TableData) { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const pageIndexRef = useRef(); @@ -43,7 +48,8 @@ export function Table(options: TableData) { { field: 'type', name: '', - width: '3%', + sortable: true, + width: '40px', render: (item) => { if (item == 'Visualization') { return ( @@ -63,17 +69,14 @@ export function Table(options: TableData) { { field: 'data', name: 'Name', - align: 'left', + width: '70%', + sortable: true, + truncateText: true, render: (item) => { return ( { - options.savedQuerySearch( - item.query, - [item.date_start, item.date_end], - item.timestamp, - item.fields - ); + handleHistoryClick(item.objectId); }} > {item.name} @@ -83,33 +86,47 @@ export function Table(options: TableData) { }, { field: 'type', - name: 'Type', - align: 'right', + name: 'Type' }, + { + field: 'delete', + width: '40px', + align: 'right', + name: '', + render: (item) => { + handleDeleteHistory( + item.objectId, + item.name + ); + }} + /> + } ]; - let queryType = ''; - const queries = options.savedHistory.map((h) => { - const savedObject = h.hasOwnProperty('savedVisualization') - ? h.savedVisualization - : h.savedQuery; + const queries = savedHistories.map((h) => { const isSavedVisualization = h.hasOwnProperty('savedVisualization'); - if (isSavedVisualization) { - queryType = 'Visualization'; - } else { - queryType = 'Query'; - } + const savedObject = isSavedVisualization ? h.savedVisualization : h.savedQuery; + const curType = isSavedVisualization ? 'savedVisualization' : 'savedQuery'; + const record = { + objectId: h.objectId, + objectType: curType, + name: savedObject.name, + query: savedObject.query, + date_start: savedObject.selected_date_range.start, + date_end: savedObject.selected_date_range.end, + timestamp: savedObject.selected_timestamp?.name, + fields: savedObject.selected_fields?.tokens || [] + }; return { - data: { - name: savedObject.name, - query: savedObject.query, - date_start: savedObject.selected_date_range.start, - date_end: savedObject.selected_date_range.end, - timestamp: savedObject.selected_timestamp?.name, - fields: savedObject.selected_fields?.tokens || [], - }, + data: record, name: savedObject.name, - type: queryType, + type: isSavedVisualization ? 'Visualization' : 'Query', + delete: record }; }); @@ -142,15 +159,12 @@ export function Table(options: TableData) { }; return ( -
- - -
+ ); } diff --git a/dashboards-observability/public/components/explorer/hooks/use_fetch_events.ts b/dashboards-observability/public/components/explorer/hooks/use_fetch_events.ts index 6cd92285e..7c68a232b 100644 --- a/dashboards-observability/public/components/explorer/hooks/use_fetch_events.ts +++ b/dashboards-observability/public/components/explorer/hooks/use_fetch_events.ts @@ -84,7 +84,6 @@ export const useFetchEvents = ({ dispatch(updateFields({ tabId: requestParams.tabId, data: { - [SELECTED_FIELDS]: [], [UNSELECTED_FIELDS]: res?.schema ? [ ...res.schema ] : [], [QUERIED_FIELDS]: [], [AVAILABLE_FIELDS]: res?.schema ? [...res.schema] : [] diff --git a/dashboards-observability/public/components/explorer/log_explorer.scss b/dashboards-observability/public/components/explorer/log_explorer.scss index e391866b9..e9e60d078 100644 --- a/dashboards-observability/public/components/explorer/log_explorer.scss +++ b/dashboards-observability/public/components/explorer/log_explorer.scss @@ -11,10 +11,19 @@ .queryTabs { .euiTabs { + .tab-title { + max-width: 8rem; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + svg { + vertical-align: inherit; + } .linkNewTag{ display: inline-block; text-align: center; - font-size: 0.75rem; + font-size: 0.875rem; line-height: 3.5; padding: 0.5rem; } diff --git a/dashboards-observability/public/components/explorer/log_explorer.tsx b/dashboards-observability/public/components/explorer/log_explorer.tsx index fafb3af32..909b3b696 100644 --- a/dashboards-observability/public/components/explorer/log_explorer.tsx +++ b/dashboards-observability/public/components/explorer/log_explorer.tsx @@ -10,11 +10,13 @@ */ import './log_explorer.scss'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { useDispatch, useSelector, batch } from 'react-redux'; import { uniqueId, - map + map, + isEmpty, + forEach } from 'lodash'; import $ from 'jquery'; import { @@ -27,7 +29,8 @@ import { Explorer } from './explorer'; import { ILogExplorerProps } from '../../../common/types/explorer'; import { TAB_TITLE, - TAB_ID_TXT_PFX + TAB_ID_TXT_PFX, + RAW_QUERY } from '../../../common/constants/explorer'; import { selectQueryTabs, @@ -35,17 +38,20 @@ import { setSelectedQueryTab, removeTab } from './slices/query_tab_slice'; +import { selectQueries } from './slices/query_slice'; import { init as initFields, remove as removefields } from './slices/field_slice'; import { init as initQuery, - remove as removeQuery + remove as removeQuery, + changeQuery } from './slices/query_slice'; import { init as initQueryResult, remove as removeQueryResult, + selectQueryResult, } from './slices/query_result_slice'; export const LogExplorer = ({ @@ -53,13 +59,24 @@ export const LogExplorer = ({ dslService, savedObjects, timestampUtils, - http, - setToast + setToast, + savedObjectId, + getExistingEmptyTab }: ILogExplorerProps) => { const dispatch = useDispatch(); const tabIds = useSelector(selectQueryTabs)['queryTabIds']; + const tabNames = useSelector(selectQueryTabs)['tabNames']; + const queries = useSelector(selectQueries); const curSelectedTabId = useSelector(selectQueryTabs)['selectedQueryTab']; + const explorerData = useSelector(selectQueryResult); + const queryRef = useRef(); + const tabIdsRef = useRef(); + const explorerDataRef = useRef(); + queryRef.current = queries; + tabIdsRef.current = tabIds; + explorerDataRef.current = explorerData; + // Append add-new-tab link to the end of the tab list, and remove it once tabs state changes useEffect(() => { @@ -104,16 +121,45 @@ export const LogExplorer = ({ }); }; - function addNewTab () { - const tabId: string = uniqueId(TAB_ID_TXT_PFX); - batch(() => { + const addNewTab = async () => { + + // get a new tabId + const tabId = uniqueId(TAB_ID_TXT_PFX); + + // create a new tab + await batch(() => { dispatch(initQuery({ tabId, })); dispatch(initQueryResult({ tabId, })); dispatch(initFields({ tabId, })); dispatch(addTab({ tabId, })); }); + + return tabId; + }; + + const dispatchSavedObjectId = async () => { + + const emptyTabId = getExistingEmptyTab({ + tabIds: tabIdsRef.current, + queries: queryRef.current, + explorerData: explorerDataRef.current + }); + const newTabId = emptyTabId ? emptyTabId : await addNewTab(); + + await dispatch(changeQuery({ + tabId: newTabId, + query: { + 'savedObjectId': savedObjectId + } + })); }; + useEffect(() => { + if (!isEmpty(savedObjectId)) { + dispatchSavedObjectId(); + } + }, []); + function getQueryTab ({ tabTitle, tabId, @@ -157,16 +203,21 @@ export const LogExplorer = ({ } const memorizedTabs = useMemo(() => { - return map(tabIds, (tabId) => { + const res = map(tabIds, (tabId) => { return getQueryTab( { - tabTitle: TAB_TITLE, + tabTitle: tabNames[tabId] || TAB_TITLE, tabId, handleTabClose, } ); }); - }, [ tabIds ]); + + return res; + }, [ + tabIds, + tabNames + ]); return ( <> diff --git a/dashboards-observability/public/components/explorer/no_results.tsx b/dashboards-observability/public/components/explorer/no_results.tsx index 490f70ed1..604264677 100644 --- a/dashboards-observability/public/components/explorer/no_results.tsx +++ b/dashboards-observability/public/components/explorer/no_results.tsx @@ -51,7 +51,7 @@ export const NoResults = () => {

diff --git a/dashboards-observability/public/components/explorer/sidebar/field.tsx b/dashboards-observability/public/components/explorer/sidebar/field.tsx index 5963c711f..805f6cc80 100644 --- a/dashboards-observability/public/components/explorer/sidebar/field.tsx +++ b/dashboards-observability/public/components/explorer/sidebar/field.tsx @@ -32,6 +32,7 @@ interface IFieldProps { selected: boolean; showToggleButton: boolean; showTimestampOverrideButton: boolean; + isFieldToggleButtonDisabled: boolean; onToggleField: (field: IField) => void; } @@ -43,7 +44,7 @@ export const Field = (props: IFieldProps) => { isOverridingTimestamp, handleOverrideTimestamp, selected, - showToggleButton = true, + isFieldToggleButtonDisabled = false, showTimestampOverrideButton = true, onToggleField } = props; @@ -101,15 +102,23 @@ export const Field = (props: IFieldProps) => { <> { - showToggleButton ? ( + isFieldToggleButtonDisabled ? ( + + ) : ( { data-test-subj={`fieldToggle-${field.name}`} aria-label={ selected ? removeLabelAria : addLabelAria } /> - ) : null + ) } diff --git a/dashboards-observability/public/components/explorer/sidebar/sidebar.tsx b/dashboards-observability/public/components/explorer/sidebar/sidebar.tsx index 3a408dc05..47b4d58ed 100644 --- a/dashboards-observability/public/components/explorer/sidebar/sidebar.tsx +++ b/dashboards-observability/public/components/explorer/sidebar/sidebar.tsx @@ -29,6 +29,7 @@ interface ISidebarProps { explorerData: any; selectedTimestamp: string; isOverridingTimestamp: boolean; + isFieldToggleButtonDisabled: boolean; handleOverrideTimestamp: (timestamp: { name: string, type: string }) => void; handleAddField: (field: IField) => void; handleRemoveField: (field: IField) => void; @@ -41,6 +42,7 @@ export const Sidebar = (props: ISidebarProps) => { explorerData, selectedTimestamp, isOverridingTimestamp, + isFieldToggleButtonDisabled, handleOverrideTimestamp, handleAddField, handleRemoveField @@ -99,7 +101,7 @@ export const Sidebar = (props: ISidebarProps) => { selectedTimestamp={ selectedTimestamp } handleOverrideTimestamp={ handleOverrideTimestamp } selected={ true } - showToggleButton={ false } + isFieldToggleButtonDisabled={ true } showTimestampOverrideButton={ false } onToggleField={ handleRemoveField } /> @@ -138,11 +140,11 @@ export const Sidebar = (props: ISidebarProps) => { field={ field } selectedTimestamp={ selectedTimestamp } isOverridingTimestamp={ isOverridingTimestamp } - handleOverrideTimestamp={ handleOverrideTimestamp } - selected={ true } - showToggleButton={ true } - showTimestampOverrideButton={ true } - onToggleField={ handleRemoveField } + handleOverrideTimestamp={handleOverrideTimestamp} + selected={true} + isFieldToggleButtonDisabled={isFieldToggleButtonDisabled} + showTimestampOverrideButton={true} + onToggleField={handleRemoveField} /> )}) @@ -200,13 +202,13 @@ export const Sidebar = (props: ISidebarProps) => { > )}) diff --git a/dashboards-observability/public/components/explorer/slices/query_tab_slice.ts b/dashboards-observability/public/components/explorer/slices/query_tab_slice.ts index 70e234216..53108150c 100644 --- a/dashboards-observability/public/components/explorer/slices/query_tab_slice.ts +++ b/dashboards-observability/public/components/explorer/slices/query_tab_slice.ts @@ -19,10 +19,12 @@ import { NEW_SELECTED_QUERY_TAB, REDUX_EXPL_SLICE_QUERY_TABS } from '../../../../common/constants/explorer'; +import { assign } from 'lodash'; const initialState = { queryTabIds: [initialTabId], - selectedQueryTab: initialTabId + selectedQueryTab: initialTabId, + tabNames: {} }; export const queryTabsSlice = createSlice({ @@ -40,6 +42,12 @@ export const queryTabsSlice = createSlice({ }); state[SELECTED_QUERY_TAB] = payload[NEW_SELECTED_QUERY_TAB]; }, + updateTabName: (state, { payload }) => { + const newTabNames = { + [payload.tabId]: payload.tabName + }; + assign(state.tabNames, newTabNames); + }, setSelectedQueryTab: (state, { payload }) => { state[SELECTED_QUERY_TAB] = payload.tabId; } @@ -50,7 +58,8 @@ export const queryTabsSlice = createSlice({ export const { addTab, removeTab, - setSelectedQueryTab + setSelectedQueryTab, + updateTabName } = queryTabsSlice.actions; export const selectQueryTabs = (state) => state.explorerTabs; diff --git a/dashboards-observability/public/services/saved_objects/event_analytics/saved_objects.ts b/dashboards-observability/public/services/saved_objects/event_analytics/saved_objects.ts index ed00d1e17..63f93a582 100644 --- a/dashboards-observability/public/services/saved_objects/event_analytics/saved_objects.ts +++ b/dashboards-observability/public/services/saved_objects/event_analytics/saved_objects.ts @@ -176,11 +176,34 @@ export default class SavedObjects { query: params.query, fields: params.fields, dateRange: params.dateRange, + chartType: params.type, + name: params.name, + timestamp: params.timestamp }); finalParams['object_id'] = params.objectId; - return await this.http.post( + return await this.http.put( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, + { + body: JSON.stringify(finalParams) + } + ); + } + + async updateSavedQueryById(params: any) { + const finalParams = this.buildRequestBody({ + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + chartType: params.type, + name: params.name, + timestamp: params.timestamp + }); + + finalParams['object_id'] = params.objectId; + + return await this.http.put( `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_QUERY}`, { body: JSON.stringify(finalParams) @@ -259,7 +282,17 @@ export default class SavedObjects { ); } - deleteSavedObjectsById(deleteObjectRequest: any) {} + async deleteSavedObjectsById(deleteObjectRequest: any) { + const finalParams = { + objectId: deleteObjectRequest.objectId + }; + return await this.http.delete( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}`, + { + body: JSON.stringify(finalParams) + } + ); + } deleteSavedObjectsByIdList(deleteObjectRequesList: any) {} diff --git a/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts b/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts index 91cc6d9c6..203155591 100644 --- a/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts +++ b/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts @@ -153,6 +153,10 @@ export const registerEventAnalyticsRouter = ({ end: schema.string(), text: schema.string(), }), + selected_timestamp: schema.object({ + name: schema.string(), + type: schema.string() + }), selected_fields: schema.object({ tokens: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), text: schema.string(), @@ -191,6 +195,10 @@ export const registerEventAnalyticsRouter = ({ end: schema.string(), text: schema.string(), }), + selected_timestamp: schema.object({ + name: schema.string(), + type: schema.string() + }), selected_fields: schema.object({ tokens: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), text: schema.string(), @@ -206,15 +214,15 @@ export const registerEventAnalyticsRouter = ({ req, res ) : Promise> => { - const savedRes = await savedObjectFacet.updateSavedVisualization(req); + const updateRes = await savedObjectFacet.updateSavedVisualization(req); const result: any = { body: { - ...savedRes['data'] + ...updateRes['data'] } }; - if (savedRes['success']) return res.ok(result); + if (updateRes['success']) return res.ok(result); result['statusCode'] = 500; - result['message'] = savedRes['data']; + result['message'] = updateRes['data']; return res.custom(result); }); @@ -280,4 +288,32 @@ export const registerEventAnalyticsRouter = ({ result['message'] = savedRes['data']; return res.custom(result); }); + + router.delete( + { + path: `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}`, + validate: { + body: schema.object({ + objectId: schema.string() + }), + }, + }, + async ( + context, + req, + res + ): Promise> => { + + const deleteResponse = await savedObjectFacet.deleteSavedObject(req); + const result: any = { + body: { + ...deleteResponse['data'] + } + }; + if (deleteResponse['success']) return res.ok(result); + result['statusCode'] = 500; + result['message'] = deleteResponse['data']; + return res.custom(result); + } + ); } \ No newline at end of file diff --git a/dashboards-observability/server/services/facets/saved_objects.ts b/dashboards-observability/server/services/facets/saved_objects.ts index 9e27a9ccb..b41b2ffdc 100644 --- a/dashboards-observability/server/services/facets/saved_objects.ts +++ b/dashboards-observability/server/services/facets/saved_objects.ts @@ -110,7 +110,7 @@ export default class SavedObjectFacet { res['data'] = savedQueryRes; } catch (err: any) { console.error('Event analytics update error: ', err); - res['data'] = err; + res['data'] = err.message; } return res; }; @@ -138,35 +138,29 @@ export default class SavedObjectFacet { res['data'] = savedQueryRes; } catch (err: any) { console.error('Event analytics update error: ', err); - res['data'] = err; + res['data'] = err.message; } return res; }; delete = async ( request: any, - format: string, - objectType: string + format: string ) => { const res = { success: false, data: {} }; try { - const params = { - objectId: request.body.object_id, - body: { - [objectType]: { - ...request.body.object - } - } + const params = { + objectId: request.body.objectId }; const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedQueryRes; } catch (err: any) { console.error('Event analytics delete error: ', err); - res['data'] = err; + res['data'] = err.message; } return res; }; @@ -203,7 +197,7 @@ export default class SavedObjectFacet { return this.update(request, 'observability.updateObjectById', 'savedVisualization'); }; - deleteSavedQuery = async (request: any) => { - return this.delete(request, 'observability.deleteObjectByIdList', 'savedQuery'); + deleteSavedObject = async (request: any) => { + return this.delete(request, 'observability.deleteObjectById', 'savedQuery'); }; } \ No newline at end of file From b2428bffb4577d845878cf9eca74865e9850b435 Mon Sep 17 00:00:00 2001 From: Shenoy Pratik Date: Fri, 5 Nov 2021 17:08:02 -0700 Subject: [PATCH 4/4] added support for sample panels (#200) Signed-off-by: Shenoy Pratik --- .../custom_panels/custom_panel_table.tsx | 11 + .../public/components/custom_panels/home.tsx | 56 ++++++ .../custom_panels/custom_panel_adaptor.ts | 19 ++ .../helpers/custom_panels/sample_panels.ts | 94 +++++++++ .../events_explorer/sample_savedObjects.ts | 189 ++++++++++++++++++ .../routes/custom_panels/panels_router.ts | 39 ++++ .../event_analytics/event_analytics_router.ts | 25 +++ .../server/services/facets/saved_objects.ts | 129 +++++++----- 8 files changed, 508 insertions(+), 54 deletions(-) create mode 100644 dashboards-observability/server/common/helpers/custom_panels/sample_panels.ts create mode 100644 dashboards-observability/server/common/helpers/events_explorer/sample_savedObjects.ts diff --git a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx index 1c1f5bd34..3cdc6cdeb 100644 --- a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx @@ -77,6 +77,7 @@ type Props = { renameCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; cloneCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; deleteCustomPanelList: (customPanelIdList: string[], toastMessage: string) => any; + addSamplePanels: () => void; }; export const CustomPanelTable = ({ @@ -89,6 +90,7 @@ export const CustomPanelTable = ({ renameCustomPanel, cloneCustomPanel, deleteCustomPanelList, + addSamplePanels }: Props) => { const [isModalVisible, setIsModalVisible] = useState(false); // Modal Toggle const [modalLayout, setModalLayout] = useState(); // Modal Layout @@ -235,6 +237,15 @@ export const CustomPanelTable = ({ > Delete , + { + setIsActionsPopoverOpen(false); + addSamplePanels(); + }} + > + Add sample panels + , ]; const tableColumns = [ diff --git a/dashboards-observability/public/components/custom_panels/home.tsx b/dashboards-observability/public/components/custom_panels/home.tsx index f0d7c92f1..9a5299cce 100644 --- a/dashboards-observability/public/components/custom_panels/home.tsx +++ b/dashboards-observability/public/components/custom_panels/home.tsx @@ -25,6 +25,11 @@ import { renderPageWithSidebar } from '../common/side_nav'; import { CustomPanelTable } from './custom_panel_table'; import { CustomPanelView } from './custom_panel_view'; import { isNameValid } from './helpers/utils'; +import { + EVENT_ANALYTICS, + OBSERVABILITY_BASE, + SAVED_OBJECTS, +} from '../../../common/constants/shared'; /* * "Home" module is initial page for Operantional Panels @@ -220,6 +225,56 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } }); }; + const addSamplePanels = async () => { + try { + setLoading(true); + const flights = await http + .get('../api/saved_objects/_find', { + query: { + type: 'index-pattern', + search_fields: 'title', + search: 'opensearch_dashboards_sample_data_flights', + }, + }) + .then((resp) => resp.total === 0); + const logs = await http + .get('../api/saved_objects/_find', { + query: { + type: 'index-pattern', + search_fields: 'title', + search: 'opensearch_dashboards_sample_data_logs', + }, + }) + .then((resp) => resp.total === 0); + if (flights || logs) setToast('Adding sample data. This can take some time.'); + await Promise.all([ + flights ? http.post('../api/sample_data/flights') : Promise.resolve(), + logs ? http.post('../api/sample_data/logs') : Promise.resolve(), + ]); + + let savedVisualizationIds = []; + await http + .get(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/addSampleSavedObjects/panels`) + .then((resp) => (savedVisualizationIds = [...resp.savedObjectIds])); + + await http + .post(`${CUSTOM_PANELS_API_PREFIX}/panels/addSamplePanels`, { + body: JSON.stringify({ + savedVisualizationIds: savedVisualizationIds, + }), + }) + .then((res) => { + setcustomPanelData([...customPanelData, ...res.demoPanelsData]); + }); + setToast(`Sample panels successfully added.`); + } catch (err) { + setToast('Error adding sample panels.', 'danger'); + console.error(err.body.message); + } finally { + setLoading(false); + } + }; + return (
); }} diff --git a/dashboards-observability/server/adaptors/custom_panels/custom_panel_adaptor.ts b/dashboards-observability/server/adaptors/custom_panels/custom_panel_adaptor.ts index 7c2550840..c909cb14b 100644 --- a/dashboards-observability/server/adaptors/custom_panels/custom_panel_adaptor.ts +++ b/dashboards-observability/server/adaptors/custom_panels/custom_panel_adaptor.ts @@ -12,6 +12,7 @@ import { v4 as uuidv4 } from 'uuid'; import { PanelType, VisualizationType } from '../../../common/types/custom_panels'; import { ILegacyScopedClusterClient } from '../../../../../src/core/server'; +import { createDemoPanel } from '../../common/helpers/custom_panels/sample_panels'; interface boxType { x1: number; @@ -387,4 +388,22 @@ export class CustomPanelsAdaptor { throw new Error('Edit Visualizations Error:' + error); } }; + + // Create Sample Panels + addSamplePanels = async (client: ILegacyScopedClusterClient, savedVisualizationIds: string[]) => { + try { + const panelBody = createDemoPanel(savedVisualizationIds); + const indexResponse = await this.indexPanel(client, panelBody); + const fetchPanel = await this.getPanel(client, indexResponse.objectId); + const fetchResponse = { + name: fetchPanel.operationalPanel.name, + id: fetchPanel.objectId, + dateCreated: fetchPanel.createdTimeMs, + dateModified: fetchPanel.lastUpdatedTimeMs, + }; + return [fetchResponse]; + } catch (error) { + throw new Error('Create New Panel Error:' + error); + } + }; } diff --git a/dashboards-observability/server/common/helpers/custom_panels/sample_panels.ts b/dashboards-observability/server/common/helpers/custom_panels/sample_panels.ts new file mode 100644 index 000000000..357d0b2f2 --- /dev/null +++ b/dashboards-observability/server/common/helpers/custom_panels/sample_panels.ts @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { v4 as uuidv4 } from 'uuid'; + +export const createDemoPanel = (savedVisualizationIds: string[]) => { + return { + name: 'Sample Panel', + visualizations: [ + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[0], + x: 0, + y: 0, + w: 4, + h: 3, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[1], + x: 4, + y: 0, + w: 5, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[2], + x: 4, + y: 2, + w: 8, + h: 3, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[3], + x: 0, + y: 5, + w: 12, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[4], + x: 9, + y: 0, + w: 3, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[5], + x: 0, + y: 3, + w: 4, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[6], + x: 5, + y: 7, + w: 7, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[7], + x: 0, + y: 7, + w: 5, + h: 2, + }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationIds[8], + x: 0, + y: 9, + w: 12, + h: 1, + }, + ], + timeRange: { to: 'now/y', from: 'now/y' }, + queryFilter: { query: '', language: 'ppl' }, + }; +}; diff --git a/dashboards-observability/server/common/helpers/events_explorer/sample_savedObjects.ts b/dashboards-observability/server/common/helpers/events_explorer/sample_savedObjects.ts new file mode 100644 index 000000000..5e37c47a2 --- /dev/null +++ b/dashboards-observability/server/common/helpers/events_explorer/sample_savedObjects.ts @@ -0,0 +1,189 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export const sampleVisualizationSet1 = [ + { + name: 'sample-visualization-1', + description: '', + query: + 'index = opensearch_dashboards_sample_data_logs | stats max(bytes) as mbytes, max(bytes+1000) as m1bytes, max(bytes+1500) as m2bytes, max(bytes+2000) as m3bytes, max(bytes+2500) as m4bytes, avg(bytes) as abytes, avg(bytes+1000) as a1bytes, avg(bytes+1500) as a2bytes, avg(bytes+2000) as a3bytes, avg(bytes+2500) as a4bytes by host', + type: 'bar', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-2', + description: '', + query: + 'index = opensearch_dashboards_sample_data_logs | stats max(bytes) as mbytes, max(bytes+1000) as m1bytes, max(bytes+1500) as m2bytes, max(bytes+2000) as m3bytes, max(bytes+2500) as m4bytes, avg(bytes) as abytes, avg(bytes+1000) as a1bytes, avg(bytes+1500) as a2bytes, avg(bytes+2000) as a3bytes, avg(bytes+2500) as a4bytes by host', + type: 'horizontal_bar', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-3', + description: '', + query: + 'index = opensearch_dashboards_sample_data_logs | stats max(bytes) as mbytes, max(bytes+1000) as m1bytes, max(bytes+1500) as m2bytes, max(bytes+2000) as m3bytes, max(bytes+2500) as m4bytes, avg(bytes) as abytes, avg(bytes+1000) as a1bytes, avg(bytes+1500) as a2bytes, avg(bytes+2000) as a3bytes, avg(bytes+2500) as a4bytes by host', + type: 'line', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-4', + description: '', + query: + 'source = opensearch_dashboards_sample_data_logs | stats sum(bytes) by span(timestamp, 1h)', + type: 'line', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-5', + description: '', + query: 'source = opensearch_dashboards_sample_data_logs | stats count() by agent', + type: 'bar', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-6', + description: '', + query: 'source = opensearch_dashboards_sample_data_logs | stats max(bytes) by host', + type: 'bar', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-7', + description: '', + query: + 'source = opensearch_dashboards_sample_data_flights | stats avg(FlightDelayMin) by Carrier', + type: 'bar', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-8', + description: '', + query: 'source = opensearch_dashboards_sample_data_logs | stats count() by span(timestamp,1d)', + type: 'line', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'utc_time', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, + { + name: 'sample-visualization-9', + description: '', + query: 'source = opensearch_dashboards_sample_data_logs | stats count() by host', + type: 'line', + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'utc_time', + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }, +]; diff --git a/dashboards-observability/server/routes/custom_panels/panels_router.ts b/dashboards-observability/server/routes/custom_panels/panels_router.ts index e999dbf2f..9f28c3e3a 100644 --- a/dashboards-observability/server/routes/custom_panels/panels_router.ts +++ b/dashboards-observability/server/routes/custom_panels/panels_router.ts @@ -339,4 +339,43 @@ export function PanelsRouter(router: IRouter) { } } ); + + // Add Sample Panels + router.post( + { + path: `${API_PREFIX}/panels/addSamplePanels`, + validate: { + body: schema.object({ + savedVisualizationIds: schema.arrayOf(schema.string()), + }), + }, + }, + async ( + context, + request, + response + ): Promise> => { + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); + + try { + const panelsData = await customPanelBackend.addSamplePanels( + opensearchNotebooksClient, + request.body.savedVisualizationIds + ); + return response.ok({ + body: { + demoPanelsData: panelsData, + }, + }); + } catch (error) { + console.error('Issue in fetching panel list:', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); } diff --git a/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts b/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts index 203155591..4d172aa52 100644 --- a/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts +++ b/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts @@ -316,4 +316,29 @@ export const registerEventAnalyticsRouter = ({ return res.custom(result); } ); + + router.get( + { + path: `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/addSampleSavedObjects/{sampleRequestor}`, + validate: { + params: schema.object({ + sampleRequestor: schema.string(), + }), + }, + }, + async (context, req, res): Promise> => { + const savedRes = await savedObjectFacet.createSampleSavedObjects(req); + const result: any = { + body: { + ...savedRes['data'], + }, + }; + + if (savedRes['success']) return res.ok(result); + + result['statusCode'] = 500; + result['message'] = savedRes['data']; + return res.custom(result); + } + ); } \ No newline at end of file diff --git a/dashboards-observability/server/services/facets/saved_objects.ts b/dashboards-observability/server/services/facets/saved_objects.ts index b41b2ffdc..872b8097d 100644 --- a/dashboards-observability/server/services/facets/saved_objects.ts +++ b/dashboards-observability/server/services/facets/saved_objects.ts @@ -9,24 +9,23 @@ * GitHub history for details. */ +import { sampleVisualizationSet1 } from '../../common/helpers/events_explorer/sample_savedObjects'; + export default class SavedObjectFacet { constructor(private client: any) { this.client = client; } - fetch = async ( - request: any, - format: string - ) => { + fetch = async (request: any, format: string) => { const res = { success: false, - data: {} + data: {}, }; try { const params = { - ...request.url.query - }; - const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + ...request.url.query, + }; + const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedQueryRes; } catch (err: any) { @@ -36,24 +35,20 @@ export default class SavedObjectFacet { return res; }; - create = async ( - request: any, - format: string, - objectType: string - ) => { + create = async (request: any, format: string, objectType: string) => { const res = { success: false, - data: {} + data: {}, }; try { const params = { body: { [objectType]: { - ...request.body.object - } - } - }; - const savedRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + ...request.body.object, + }, + }, + }; + const savedRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedRes; } catch (err: any) { @@ -63,24 +58,21 @@ export default class SavedObjectFacet { return res; }; - createTimestamp = async ( - request: any, - format: string, - ) => { + createTimestamp = async (request: any, format: string) => { const res = { success: false, - data: {} + data: {}, }; try { const params = { body: { objectId: request.body.index, - 'timestamp': { - ...request.body - } - } - }; - const savedRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + timestamp: { + ...request.body, + }, + }, + }; + const savedRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedRes; } catch (err: any) { @@ -88,24 +80,21 @@ export default class SavedObjectFacet { res['data'] = err; } return res; - } + }; - updateTimestamp = async ( - request: any, - format: string, - ) => { + updateTimestamp = async (request: any, format: string) => { const res = { success: false, - data: {} + data: {}, }; try { const params = { objectId: request.body.objectId, body: { - ...request.body - } + ...request.body, + }, }; - const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedQueryRes; } catch (err: any) { @@ -115,25 +104,21 @@ export default class SavedObjectFacet { return res; }; - update = async ( - request: any, - format: string, - objectType: string - ) => { + update = async (request: any, format: string, objectType: string) => { const res = { success: false, - data: {} + data: {}, }; try { const params = { objectId: request.body.object_id, - body: { + body: { [objectType]: { - ...request.body.object - } - } + ...request.body.object, + }, + }, }; - const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedQueryRes; } catch (err: any) { @@ -149,13 +134,13 @@ export default class SavedObjectFacet { ) => { const res = { success: false, - data: {} + data: {}, }; try { const params = { objectId: request.body.objectId }; - const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedQueryRes; } catch (err: any) { @@ -165,6 +150,38 @@ export default class SavedObjectFacet { return res; }; + createSamples = async (request: any, format: string) => { + const res = { + success: false, + data: {}, + }; + try { + let savedRes: any[] = []; + + // TODO: add support for samples in event analytics + if (request.params.sampleRequestor === 'panels') { + for (var i = 0; i < sampleVisualizationSet1.length; i++) { + const params = { + body: { + savedVisualization: { + ...sampleVisualizationSet1[i], + }, + }, + }; + const savedVizRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + savedRes.push(savedVizRes.objectId); + } + } + + res['success'] = true; + res['data'] = { savedObjectIds: savedRes }; + } catch (err: any) { + console.error('Event analytics create error: ', err); + res['data'] = err; + } + return res; + }; + getSavedQuery = async (request: any) => { return this.fetch(request, 'observability.getObject'); }; @@ -192,7 +209,7 @@ export default class SavedObjectFacet { updateSavedQuery = (request: any) => { return this.update(request, 'observability.updateObjectById', 'savedQuery'); }; - + updateSavedVisualization = (request: any) => { return this.update(request, 'observability.updateObjectById', 'savedVisualization'); }; @@ -200,4 +217,8 @@ export default class SavedObjectFacet { deleteSavedObject = async (request: any) => { return this.delete(request, 'observability.deleteObjectById', 'savedQuery'); }; -} \ No newline at end of file + + createSampleSavedObjects = async (request: any) => { + return this.createSamples(request, 'observability.createObject'); + }; +}