diff --git a/common/constants/metrics.ts b/common/constants/metrics.ts index 853ef28658..7e92e6663b 100644 --- a/common/constants/metrics.ts +++ b/common/constants/metrics.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +export const METRIC_EXPLORER_BASE_PATH = 'observability-metrics#/'; + // requests constants export const VISUALIZATION = 'viz'; export const SAVED_VISUALIZATION = 'savedVisualization'; @@ -24,13 +26,10 @@ export const resolutionOptions = [ { value: 'y', text: 'years' }, ]; -export const DEFAULT_METRIC_HEIGHT = 2; -export const DEFAULT_METRIC_WIDTH = 12; - export const AGGREGATION_OPTIONS = [ - { label: 'avg' }, - { label: 'sum' }, - { label: 'count' }, - { label: 'min' }, - { label: 'max' }, + { value: 'avg', text: 'avg()' }, + { value: 'sum', text: 'sum()' }, + { value: 'count', text: 'count()' }, + { value: 'min', text: 'min()' }, + { value: 'max', text: 'max()' }, ]; diff --git a/common/constants/shared.ts b/common/constants/shared.ts index 2116454852..9baffcf101 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -78,6 +78,10 @@ export const PPL_PATTERNS_DOCUMENTATION_URL = export const UI_DATE_FORMAT = 'MM/DD/YYYY hh:mm A'; export const PPL_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSSSSS'; export const SPAN_REGEX = /span/; + +export const PROMQL_METRIC_SUBTYPE = 'promqlmetric'; +export const PPL_METRIC_SUBTYPE = 'metric'; + export const PPL_SPAN_REGEX = /by\s*span/i; export const PPL_STATS_REGEX = /\|\s*stats/i; export const PPL_INDEX_INSERT_POINT_REGEX = /(search source|source|index)\s*=\s*([^|\s]+)(.*)/i; @@ -190,6 +194,7 @@ export const LIVE_OPTIONS = [ ]; export const LIVE_END_TIME = 'now'; + export interface DefaultChartStylesProps { DefaultModeLine: string; Interpolation: string; @@ -243,6 +248,7 @@ export const VISUALIZATION_ERROR = { NO_DATA: 'No data found.', INVALID_DATA: 'Invalid visualization data', NO_SERIES: 'Add a field to start', + NO_METRIC: 'Invalid Metric MetaData', }; export const S3_DATASOURCE_TYPE = 'S3_DATASOURCE'; diff --git a/common/types/custom_panels.ts b/common/types/custom_panels.ts index a24cbe839f..11834906c3 100644 --- a/common/types/custom_panels.ts +++ b/common/types/custom_panels.ts @@ -50,7 +50,7 @@ export interface SavedVisualizationType { selected_date_range: { start: string; end: string; text: string }; timeField: string; application_id?: string; - user_configs: any; + userConfigs: any; } export interface PPLResponse { diff --git a/common/types/explorer.ts b/common/types/explorer.ts index 9daf690743..c46a652491 100644 --- a/common/types/explorer.ts +++ b/common/types/explorer.ts @@ -8,23 +8,23 @@ import Plotly from 'plotly.js-dist'; import { QueryManager } from 'common/query_manager'; import { VIS_CHART_TYPES } from '../../common/constants/shared'; import { - RAW_QUERY, - SELECTED_FIELDS, - UNSELECTED_FIELDS, + AGGREGATIONS, AVAILABLE_FIELDS, - QUERIED_FIELDS, - INDEX, + BREAKDOWNS, + CUSTOM_LABEL, FINAL_QUERY, - SELECTED_TIMESTAMP, - SELECTED_DATE_RANGE, GROUPBY, - AGGREGATIONS, - CUSTOM_LABEL, - BREAKDOWNS, + INDEX, + QUERIED_FIELDS, + RAW_QUERY, + SELECTED_DATE_RANGE, + SELECTED_FIELDS, + SELECTED_TIMESTAMP, + UNSELECTED_FIELDS, } from '../constants/explorer'; import { - CoreStart, CoreSetup, + CoreStart, HttpSetup, HttpStart, NotificationsStart, @@ -39,6 +39,7 @@ import { } from '../../../../src/core/public/saved_objects'; import { ChromeBreadcrumb } from '../../../../src/core/public/chrome'; import { DataSourceType } from '../../../../src/plugins/data/public'; +import { PROMQL_METRIC_SUBTYPE } from '../constants/shared'; export interface IQueryTab { id: string; @@ -173,7 +174,7 @@ export interface SavedVisualization extends SavedObjectAttributes { selected_fields: { text: string; tokens: [] }; selected_timestamp: IField; type: string; - sub_type?: 'metric' | 'visualization'; // exists if sub type is metric + subType?: 'metric' | 'visualization' | typeof PROMQL_METRIC_SUBTYPE; // exists if sub type is metric user_configs?: string; units_of_measure?: string; application_id?: string; @@ -346,6 +347,7 @@ export interface DataConfigPanelProps { visualizations: IVisualizationContainerProps; queryManager?: QueryManager; } + export interface GetTooltipHoverInfoType { tooltipMode: string; tooltipText: string; diff --git a/common/types/metrics.ts b/common/types/metrics.ts index 64ebd07aba..7cefde98b8 100644 --- a/common/types/metrics.ts +++ b/common/types/metrics.ts @@ -5,12 +5,6 @@ import { VisualizationType } from './custom_panels'; -export interface MetricData { - metricId: string; - metricType: 'savedCustomMetric' | 'prometheusMetric'; - metricName: string; -} - export interface MetricType extends VisualizationType { id: string; savedVisualizationId: string; @@ -18,5 +12,11 @@ export interface MetricType extends VisualizationType { y: number; w: number; h: number; - metricType: 'savedCustomMetric' | 'prometheusMetric'; + query: { + type: 'savedCustomMetric' | 'prometheusMetric'; + aggregation: string; + attributesGroupBy: string[]; + catalog: string; + availableAttributes?: string[]; + }; } diff --git a/public/components/application_analytics/helpers/utils.tsx b/public/components/application_analytics/helpers/utils.tsx index 08ba77f32e..8c66cc2153 100644 --- a/public/components/application_analytics/helpers/utils.tsx +++ b/public/components/application_analytics/helpers/utils.tsx @@ -218,7 +218,9 @@ export const calculateAvailability = async ( const visData = await fetchVisualizationById(http, visualizationId, (value: string) => console.error(value) ); - const userConfigs = visData.user_configs ? JSON.parse(visData.user_configs) : {}; + const userConfigs = visData.userConfigs + ? JSON.parse(visData.user_configs || visData.userConfigs) + : {}; // If there are levels, we get the current value if (userConfigs.availabilityConfig?.hasOwnProperty('level')) { // For every saved visualization with availability levels we push it to visWithAvailability diff --git a/public/components/common/query_utils/index.ts b/public/components/common/query_utils/index.ts index 9f4024bb31..819ba3bc2a 100644 --- a/public/components/common/query_utils/index.ts +++ b/public/components/common/query_utils/index.ts @@ -6,7 +6,7 @@ import dateMath from '@elastic/datemath'; import { Moment } from 'moment-timezone'; import { isEmpty } from 'lodash'; -import { SearchMetaData } from 'public/components/event_analytics/redux/slices/search_meta_data_slice'; +import { SearchMetaData } from '../../event_analytics/redux/slices/search_meta_data_slice'; import { PPL_DEFAULT_PATTERN_REGEX_FILETER, SELECTED_DATE_RANGE, @@ -20,6 +20,7 @@ import { PPL_NEWLINE_REGEX, } from '../../../../common/constants/shared'; import { IExplorerFields, IQuery } from '../../../../common/types/explorer'; +import { updateCatalogVisualizationQuery } from '../../custom_panels/helpers/utils'; /* * "Query Utils" This file contains different reused functions in operational panels @@ -35,6 +36,32 @@ const escapeQuotes = (literal: string) => { return literal.replaceAll("'", "''"); }; +export const findMinInterval = (start: string = '', end: string = '') => { + const momentStart = dateMath.parse(start)!; + const momentEnd = dateMath.parse(end, { roundUp: true })!; + const diffSeconds = momentEnd.unix() - momentStart.unix(); + let minInterval = 'y'; + + // less than 1 second + if (diffSeconds <= 1) minInterval = 'ms'; + // less than 2 minutes + else if (diffSeconds <= 60 * 2) minInterval = 's'; + // less than 2 hours + else if (diffSeconds <= 3600 * 2) minInterval = 'm'; + // less than 2 days + else if (diffSeconds <= 86400 * 2) minInterval = 'h'; + // less than 1 month + else if (diffSeconds <= 86400 * 31) minInterval = 'd'; + // less than 3 months + else if (diffSeconds <= 86400 * 93) minInterval = 'w'; + // less than 2 year + else if (diffSeconds <= 86400 * 366) minInterval = 'w'; + + console.log('findMinInterval', { momentStart, momentEnd, diffSeconds, minInterval }); + + return minInterval; +}; + export const convertDateTime = ( datetime: string, isStart = true, @@ -128,13 +155,6 @@ export const updatePromQLQueryFilters = ( const { connection, metric, aggregation, attributesGroupBy } = parsePromQLIntoKeywords( promQLQuery ); - console.log('updatePromQLQueryFilters', { - connection, - metric, - aggregation, - attributesGroupBy, - promQLQuery, - }); const promQLPart = buildPromQLFromMetricQuery({ metric, attributesGroupBy: attributesGroupBy.split(','), @@ -157,6 +177,24 @@ export const getIndexPatternFromRawQuery = (query: string): string => { return getPromQLIndex(query) || getPPLIndex(query); }; +export const preprocessMetricQuery = ({ metaData, startTime, endTime }) => { + // convert to moment + const start = convertDateTime(startTime, true); + const end = convertDateTime(endTime, false); + + const resolution = findMinInterval(start, end); + + const visualizationQuery = updateCatalogVisualizationQuery({ + ...metaData.queryMetaData, + start, + end, + span: '1', + resolution, + }); + + return visualizationQuery; +}; + // insert time filter command and additional commands based on raw query export const preprocessQuery = ({ rawQuery, diff --git a/public/components/common/search/search.tsx b/public/components/common/search/search.tsx index de450d09c1..c849c25cbd 100644 --- a/public/components/common/search/search.tsx +++ b/public/components/common/search/search.tsx @@ -323,9 +323,13 @@ export const Search = (props: any) => { } curVisId={curVisId} setSubType={setSubType} - isSaveAsMetricEnabled={ - isEqual(curVisId, 'line') && tempQuery.match(PPL_SPAN_REGEX) !== null - } + isSaveAsMetricEnabled={() => { + return ( + isEqual(curVisId, 'line') && + tempQuery && + tempQuery.match(PPL_SPAN_REGEX) !== null + ); + }} /> diff --git a/public/components/custom_panels/custom_panel_view_so.tsx b/public/components/custom_panels/custom_panel_view_so.tsx index 65b10416a0..382bc50d0f 100644 --- a/public/components/custom_panels/custom_panel_view_so.tsx +++ b/public/components/custom_panels/custom_panel_view_so.tsx @@ -45,7 +45,6 @@ import { addVisualizationPanel } from './helpers/add_visualization_helper'; import { AddVisualizationPopover } from './helpers/add_visualization_popover'; import { getCustomModal } from './helpers/modal_containers'; import { - convertDateTime, isDateValid, isNameValid, isPPLFilterValid, @@ -66,7 +65,8 @@ import { import { useToast } from '../common/toast'; import PPLService from '../../services/requests/ppl'; import DSLService from '../../services/requests/dsl'; - +import { convertDateTime } from '../common/query_utils'; + /* * "CustomPanelsView" module used to render an Observability Dashboard * @@ -181,7 +181,9 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { timeProps.end, recentlyUsedRanges ); - dispatch(updatePanel({ ...panel, timeRange: { from: timeProps.start, to: timeProps.end } }, '', '')); + dispatch( + updatePanel({ ...panel, timeRange: { from: timeProps.start, to: timeProps.end } }, '', '') + ); setRecentlyUsedRanges(updatedRanges.slice(0, 9)); onRefreshFilters(timeProps.start, timeProps.end); @@ -354,7 +356,11 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { }; const cloneVisualization = (visualzationTitle: string, savedVisualizationId: string) => { - addVisualizationToCurrentPanel({ savedVisualizationId, successMsg: `Visualization ${visualzationTitle} successfully added!`, failureMsg: `Error in adding ${visualzationTitle} visualization to the panel` }); + addVisualizationToCurrentPanel({ + savedVisualizationId, + successMsg: `Visualization ${visualzationTitle} successfully added!`, + failureMsg: `Error in adding ${visualzationTitle} visualization to the panel`, + }); }; const cancelButton = ( diff --git a/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap b/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap index adf75a180e..bd5d1ac5d4 100644 --- a/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap +++ b/public/components/custom_panels/helpers/__tests__/__snapshots__/utils.test.tsx.snap @@ -257,7 +257,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -420,7 +420,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "id": "bar", "label": "Vertical bar", "labelangle": 0, - "legendposition": "v", + "legendposition": "h", "linewidth": 0, "mode": "group", "name": "bar", @@ -712,7 +712,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -875,7 +875,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "id": "bar", "label": "Vertical bar", "labelangle": 0, - "legendposition": "v", + "legendposition": "h", "linewidth": 0, "mode": "group", "name": "bar", @@ -919,7 +919,23 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "responsive": true, } } - layout={[Function]} + layout={ + Object { + "height": 1180, + "legend": Object { + "orientation": "v", + "traceorder": "normal", + }, + "margin": Object { + "b": 30, + "l": 60, + "pad": 0, + "r": 30, + "t": 50, + }, + "showlegend": true, + } + } visualizations={ Object { "data": Object { @@ -1174,7 +1190,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -1337,7 +1353,7 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "id": "bar", "label": "Vertical bar", "labelangle": 0, - "legendposition": "v", + "legendposition": "h", "linewidth": 0, "mode": "group", "name": "bar", @@ -1433,9 +1449,11 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "#BD6F26", "#4C636F", ], + "height": 1180, "hovermode": "closest", "legend": Object { - "orientation": "v", + "orientation": "h", + "traceorder": "normal", }, "margin": Object { "b": 30, @@ -1526,9 +1544,11 @@ exports[`Utils helper functions renders displayVisualization function 1`] = ` "#BD6F26", "#4C636F", ], + "height": 1180, "hovermode": "closest", "legend": Object { - "orientation": "v", + "orientation": "h", + "traceorder": "normal", }, "margin": Object { "b": 30, @@ -1792,7 +1812,7 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -2308,7 +2328,7 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -2628,7 +2648,47 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` }, } } - layout={[Function]} + layout={ + Object { + "colorway": Array [ + "#3CA1C7", + "#8C55A3", + "#DB748A", + "#F2BE4B", + "#68CCC2", + "#2A7866", + "#843769", + "#374FB8", + "#BD6F26", + "#4C636F", + ], + "height": 1180, + "legend": Object { + "orientation": "v", + "traceorder": "normal", + }, + "margin": Object { + "b": 30, + "l": 60, + "pad": 0, + "r": 30, + "t": 50, + }, + "paper_bgcolor": "rgba(0, 0, 0, 0)", + "plot_bgcolor": "rgba(0, 0, 0, 0)", + "showlegend": true, + "xaxis": Object { + "fixedrange": true, + "showgrid": false, + "visible": true, + }, + "yaxis": Object { + "fixedrange": true, + "showgrid": false, + "visible": true, + }, + } + } visualizations={ Object { "data": Object { @@ -2838,7 +2898,7 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -3205,8 +3265,22 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` layout={ Object { "autosize": true, + "colorway": Array [ + "#3CA1C7", + "#8C55A3", + "#DB748A", + "#F2BE4B", + "#68CCC2", + "#2A7866", + "#843769", + "#374FB8", + "#BD6F26", + "#4C636F", + ], + "height": 1180, "legend": Object { - "orientation": "v", + "orientation": "h", + "traceorder": "normal", }, "margin": Object { "b": 30, @@ -3215,6 +3289,8 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "r": 5, "t": 50, }, + "paper_bgcolor": "rgba(0, 0, 0, 0)", + "plot_bgcolor": "rgba(0, 0, 0, 0)", "shapes": Array [], "showlegend": true, "title": "", @@ -3295,9 +3371,23 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` Object { "autosize": true, "barmode": "stack", + "colorway": Array [ + "#3CA1C7", + "#8C55A3", + "#DB748A", + "#F2BE4B", + "#68CCC2", + "#2A7866", + "#843769", + "#374FB8", + "#BD6F26", + "#4C636F", + ], + "height": 1180, "hovermode": "closest", "legend": Object { - "orientation": "v", + "orientation": "h", + "traceorder": "normal", }, "margin": Object { "b": 30, @@ -3306,6 +3396,8 @@ exports[`Utils helper functions renders displayVisualization function 2`] = ` "r": 5, "t": 50, }, + "paper_bgcolor": "rgba(0, 0, 0, 0)", + "plot_bgcolor": "rgba(0, 0, 0, 0)", "shapes": Array [], "showlegend": true, "title": "", @@ -3602,7 +3694,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -3765,7 +3857,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "id": "horizontal_bar", "label": "Horizontal bar", "labelangle": 0, - "legendposition": "v", + "legendposition": "h", "linewidth": 0, "mode": "group", "name": "horizontal_bar", @@ -4057,7 +4149,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -4220,7 +4312,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "id": "horizontal_bar", "label": "Horizontal bar", "labelangle": 0, - "legendposition": "v", + "legendposition": "h", "linewidth": 0, "mode": "group", "name": "horizontal_bar", @@ -4264,7 +4356,23 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "responsive": true, } } - layout={[Function]} + layout={ + Object { + "height": 1180, + "legend": Object { + "orientation": "v", + "traceorder": "normal", + }, + "margin": Object { + "b": 30, + "l": 60, + "pad": 0, + "r": 30, + "t": 50, + }, + "showlegend": true, + } + } visualizations={ Object { "data": Object { @@ -4519,7 +4627,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "props": Object { "defaultSelections": Array [ Object { - "id": "v", + "id": "h", "name": "Right", }, ], @@ -4682,7 +4790,7 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "id": "horizontal_bar", "label": "Horizontal bar", "labelangle": 0, - "legendposition": "v", + "legendposition": "h", "linewidth": 0, "mode": "group", "name": "horizontal_bar", @@ -4778,9 +4886,11 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "#BD6F26", "#4C636F", ], + "height": 1180, "hovermode": "closest", "legend": Object { - "orientation": "v", + "orientation": "h", + "traceorder": "normal", }, "margin": Object { "b": 30, @@ -4871,9 +4981,11 @@ exports[`Utils helper functions renders displayVisualization function 3`] = ` "#BD6F26", "#4C636F", ], + "height": 1180, "hovermode": "closest", "legend": Object { - "orientation": "v", + "orientation": "h", + "traceorder": "normal", }, "margin": Object { "b": 30, diff --git a/public/components/custom_panels/helpers/utils.tsx b/public/components/custom_panels/helpers/utils.tsx index aac1477181..8586ed8f4e 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -80,11 +80,12 @@ export const mergeLayoutAndVisualizations = ( export const updateQuerySpanInterval = ( query: string, timestampField: string, - spanParam: string + span: number | string = '1', + resolution: string = 'h' ) => { return query.replace( new RegExp(`span\\(\\s*${timestampField}\\s*,(.*?)\\)`), - `span(${timestampField},${spanParam})` + `span(${timestampField},${span}${resolution})` ); }; @@ -210,29 +211,38 @@ export const getQueryResponse = ( }; // Fetches savedVisualization by Id and runs getQueryResponse -export const renderSavedVisualization = async ( - http: CoreStart['http'], - pplService: PPLService, - savedVisualizationId: string, - startTime: string, - endTime: string, - filterQuery: string, - spanParam: string | undefined, - setVisualizationTitle: React.Dispatch>, - setVisualizationType: React.Dispatch>, - setVisualizationData: React.Dispatch>, - setVisualizationMetaData: React.Dispatch>, - setIsLoading: React.Dispatch>, - setIsError: React.Dispatch> -) => { +export const renderSavedVisualization = async ({ + pplService, + startTime, + endTime, + filterQuery, + span = '1', + resolution = 'h', + setVisualizationTitle, + setVisualizationType, + setVisualizationData, + setVisualizationMetaData, + setIsLoading, + setIsError, + visualization, +}: { + pplService: PPLService; + startTime: string; + endTime: string; + filterQuery: string; + span?: number | string; + resolution?: string; + setVisualizationTitle: React.Dispatch>; + setVisualizationType: React.Dispatch>; + setVisualizationData: React.Dispatch>; + setVisualizationMetaData: React.Dispatch>; + setIsLoading: React.Dispatch>; + setIsError: React.Dispatch>; + visualization: SavedVisualizationType; +}) => { setIsLoading(true); setIsError({} as VizContainerError); - let visualization: SavedVisualizationType = {}; - let updatedVisualizationQuery = ''; - - visualization = await fetchVisualizationById(http, savedVisualizationId, setIsError); - if (_.isEmpty(visualization)) { setIsLoading(false); return; @@ -246,15 +256,10 @@ export const renderSavedVisualization = async ( setVisualizationType(visualization.type); } - if (spanParam !== undefined) { - updatedVisualizationQuery = updateQuerySpanInterval( - visualization.query, - visualization.timeField, - spanParam - ); - } else { - updatedVisualizationQuery = visualization.query; - } + const updatedVisualizationQuery = + span !== undefined + ? updateQuerySpanInterval(visualization.query, visualization.timeField, span, resolution) + : visualization.query; setVisualizationMetaData({ ...visualization, query: updatedVisualizationQuery }); @@ -299,34 +304,35 @@ const createCatalogVisualizationMetaData = ( }; }; -const updateCatalogVisualizationQuery = ({ +export const updateCatalogVisualizationQuery = ({ catalogSourceName, catalogTableName, aggregation, attributesGroupBy, - startTime, - endTime, - spanParam, + start, + end, + span = '1', + resolution = 'h', }: { catalogSourceName: string; catalogTableName: string; aggregation: string; attributesGroupBy: string[]; - startTime: string; - endTime: string; - spanParam: string | undefined; + start: string; + end: string; + span: string; + resolution: string; }) => { - const attributesGroupString = attributesGroupBy.toString(); - const startEpochTime = convertDateTime(startTime, true, false, true); - const endEpochTime = convertDateTime(endTime, false, false, true); - // const promQuery = - // attributesGroupBy.length === 0 - // ? `${aggregation} (${catalogTableName})` - // : `${aggregation} by(${attributesGroupString}) (${catalogTableName})`; - - const promQuery = `${aggregation} (${catalogTableName})`; - - return `source = ${catalogSourceName}.query_range('${promQuery}', ${startEpochTime}, ${endEpochTime}, '${spanParam}')`; + const attributesGroupString = attributesGroupBy.join(','); + const startEpochTime = convertDateTime(start, true, false, true); + const endEpochTime = convertDateTime(end, false, false, true); + const promQuery = + attributesGroupBy.length === 0 + ? `${aggregation} (${catalogTableName})` + : `${aggregation} by(${attributesGroupString}) (${catalogTableName})`; + + const newQuery = `source = ${catalogSourceName}.query_range('${promQuery}', ${startEpochTime}, ${endEpochTime}, '${span}${resolution}')`; + return newQuery; }; // Creates a catalogVisualization for a runtime catalog based PPL query and runs getQueryResponse @@ -337,7 +343,8 @@ export const renderCatalogVisualization = async ({ startTime, endTime, filterQuery, - spanParam, + span, + resolution, setVisualizationTitle, setVisualizationType, setVisualizationData, @@ -345,7 +352,7 @@ export const renderCatalogVisualization = async ({ setIsLoading, setIsError, spanResolution, - queryMetaData, + visualization, }: { http: CoreStart['http']; pplService: PPLService; @@ -353,7 +360,8 @@ export const renderCatalogVisualization = async ({ startTime: string; endTime: string; filterQuery: string; - spanParam: string | undefined; + span?: number | string; + resolution?: string; setVisualizationTitle: React.Dispatch>; setVisualizationType: React.Dispatch>; setVisualizationData: React.Dispatch>; @@ -362,6 +370,7 @@ export const renderCatalogVisualization = async ({ setIsError: React.Dispatch>; spanResolution?: string; queryMetaData?: MetricType; + visualization: SavedVisualizationType; }) => { setIsLoading(true); setIsError({} as VizContainerError); @@ -369,20 +378,12 @@ export const renderCatalogVisualization = async ({ const visualizationType = 'line'; const visualizationTimeField = '@timestamp'; - const catalogSourceName = catalogSource.split('.')[0]; - const catalogTableName = catalogSource.split('.')[1]; - - const defaultAggregation = 'avg'; // pass in attributes to this function - const attributes: string[] = []; - const visualizationQuery = updateCatalogVisualizationQuery({ - catalogSourceName, - catalogTableName, - aggregation: defaultAggregation, - attributesGroupBy: attributes, - startTime, - endTime, - spanParam, + ...visualization.queryMetaData, + start: startTime, + end: endTime, + span, + resolution, }); const visualizationMetaData = createCatalogVisualizationMetaData( @@ -392,18 +393,17 @@ export const renderCatalogVisualization = async ({ visualizationTimeField ); - visualizationMetaData.user_configs = { + visualizationMetaData.userConfigs = { layoutConfig: { - height: 390, - margin: { t: 5 }, - legend: { visible: false }, + height: 280, + legend: { orientation: 'h', y: -0.3 }, }, }; - setVisualizationTitle(catalogSource); + setVisualizationTitle(visualization.name); setVisualizationType(visualizationType); - setVisualizationMetaData({ ...visualizationMetaData, query: visualizationQuery }); + setVisualizationMetaData(visualizationMetaData); getQueryResponse( pplService, @@ -449,9 +449,9 @@ export const parseSavedVisualizations = ( timeField: visualization.savedVisualization.selected_timestamp.name, selected_date_range: visualization.savedVisualization.selected_date_range, selected_fields: visualization.savedVisualization.selected_fields, - user_configs: visualization.savedVisualization.user_configs || {}, - sub_type: visualization.savedVisualization.hasOwnProperty('sub_type') - ? visualization.savedVisualization.sub_type + userConfigs: visualization.savedVisualization.userConfigs || {}, + subType: visualization.savedVisualization.hasOwnProperty('subType') + ? visualization.savedVisualization.subType : '', units_of_measure: visualization.savedVisualization.hasOwnProperty('units_of_measure') ? visualization.savedVisualization.units_of_measure @@ -549,7 +549,7 @@ export const displayVisualization = (metaData: any, data: any, type: string) => return <>; } - const dataConfig = { ...(metaData.user_configs?.dataConfig || {}) }; + const dataConfig = { ...(metaData.userConfigs?.dataConfig || {}) }; const hasBreakdowns = !_.isEmpty(dataConfig.breakdowns); const realTimeParsedStats = { ...getDefaultVisConfig(new QueryManager().queryParser().parse(metaData.query).getStats()), @@ -576,13 +576,13 @@ export const displayVisualization = (metaData: any, data: any, type: string) => const mixedUserConfigs = { availabilityConfig: { - ...(metaData.user_configs?.availabilityConfig || {}), + ...(metaData.userConfigs?.availabilityConfig || {}), }, dataConfig: { ...finalDataConfig, }, layoutConfig: { - ...(metaData.user_configs?.layoutConfig || {}), + ...(metaData.userConfigs?.layoutConfig || {}), }, }; diff --git a/public/components/custom_panels/home.tsx b/public/components/custom_panels/home.tsx index 2e2af8de8c..c5067dd573 100644 --- a/public/components/custom_panels/home.tsx +++ b/public/components/custom_panels/home.tsx @@ -3,19 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiBreadcrumb, ShortDate, htmlIdGenerator } from '@elastic/eui'; +import { EuiBreadcrumb, htmlIdGenerator, ShortDate } from '@elastic/eui'; import React, { useState } from 'react'; -import { useDispatch, batch } from 'react-redux'; +import { batch, useDispatch } from 'react-redux'; // eslint-disable-next-line @osd/eslint/module_migration import { StaticContext } from 'react-router'; import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { CoreStart, SavedObjectsStart } from '../../../../../src/core/public'; -import { CUSTOM_PANELS_API_PREFIX } from '../../../common/constants/custom_panels'; import { EVENT_ANALYTICS, + OBSERVABILITY_BASE, observabilityLogsID, observabilityPanelsID, - OBSERVABILITY_BASE, SAVED_OBJECTS, } from '../../../common/constants/shared'; import DSLService from '../../services/requests/dsl'; @@ -23,15 +22,7 @@ import PPLService from '../../services/requests/ppl'; import { CustomPanelTable } from './custom_panel_table'; import { CustomPanelView } from './custom_panel_view'; import { CustomPanelViewSO } from './custom_panel_view_so'; -import { - createPanel, - createPanelSample, - createPanelWithVizs, - deletePanel, - fetchPanels, - newPanelTemplate, - uuidRx, -} from './redux/panel_slice'; +import { createPanelSample, uuidRx } from './redux/panel_slice'; import { REDIRECT_TAB, TAB_CREATED_TYPE, TAB_ID_TXT_PFX } from '../../../common/constants/explorer'; import { init as initFields } from '../event_analytics/redux/slices/field_slice'; import { init as initPatterns } from '../event_analytics/redux/slices/patterns_slice'; @@ -186,7 +177,7 @@ export const Home = ({ }} /> { const isSavedObject = !!props.match.params.id.match(uuidRx); diff --git a/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx b/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx index dc70ba7fd9..f7d7de6720 100644 --- a/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx +++ b/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ /* eslint-disable react-hooks/exhaustive-deps */ -/* eslint-disable no-console */ import _ from 'lodash'; import React, { useEffect, useState } from 'react'; @@ -109,6 +108,7 @@ export const PanelGrid = (props: PanelGridProps) => { pplFilterValue={pplFilterValue} showFlyout={showFlyout} removeVisualization={removeVisualization} + contextMenuId="visualization" /> ) ); @@ -203,6 +203,7 @@ export const PanelGrid = (props: PanelGridProps) => { breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} cols={{ lg: 12, md: 12, sm: 12, xs: 1, xxs: 1 }} onLayoutChange={layoutChanged} + draggableHandle=".mouseGrabber" > {panelVisualizations.map((panelVisualization: VisualizationType, index) => (
{gridData[index]}
diff --git a/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx b/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx index a2fa5e02b5..f2a1cff644 100644 --- a/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx +++ b/public/components/custom_panels/panel_modules/panel_grid/panel_grid_so.tsx @@ -105,6 +105,7 @@ export const PanelGridSO = (props: PanelGridProps) => { pplFilterValue={pplFilterValue} showFlyout={showFlyout} removeVisualization={removeVisualization} + contextMenuId="visualization" /> ) ); @@ -210,6 +211,7 @@ export const PanelGridSO = (props: PanelGridProps) => { breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} cols={{ lg: 12, md: 12, sm: 12, xs: 1, xxs: 1 }} onLayoutChange={layoutChanged} + draggableHandle=".mouseGrabber" > {panelVisualizations.map((panelVisualization: VisualizationType, index) => (
{gridData[index]}
diff --git a/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap b/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap index 4ba3e3ec34..a38755e252 100644 --- a/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap +++ b/public/components/custom_panels/panel_modules/visualization_container/__tests__/__snapshots__/visualization_container.test.tsx.snap @@ -1,156 +1,169 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Visualization Container Component renders add visualization container 1`] = ` - - -
- -
- -
- -
- - -
- - -
-
-
-
- -
+
+ + +
+ +
+ + - - - - - -
- -
-
- -
- + + + +
+ + + + +
- - - - - - + className="euiLoadingChart euiLoadingChart--mono visualization-loading-chart euiLoadingChart--xLarge" + > + + + + + + +
- - - + + + `; diff --git a/public/components/custom_panels/panel_modules/visualization_container/__tests__/visualization_container.test.tsx b/public/components/custom_panels/panel_modules/visualization_container/__tests__/visualization_container.test.tsx index 9ea46f4d42..507208fad0 100644 --- a/public/components/custom_panels/panel_modules/visualization_container/__tests__/visualization_container.test.tsx +++ b/public/components/custom_panels/panel_modules/visualization_container/__tests__/visualization_container.test.tsx @@ -15,6 +15,9 @@ import { sampleSavedVisualization, samplePPLResponse, } from '../../../../../../test/panels_constants'; +import { createStore } from '@reduxjs/toolkit'; +import { rootReducer } from '../../../../../framework/redux/reducers'; +import { Provider } from 'react-redux'; describe('Visualization Container Component', () => { configure({ adapter: new Adapter() }); @@ -28,6 +31,9 @@ describe('Visualization Container Component', () => { Promise.resolve((samplePPLResponse as unknown) as HttpResponse) ); + // configure({ adapter: new Adapter() }); + const store = createStore(rootReducer); + const editMode = true; const visualizationId = 'panel_viz_9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'; const savedVisualizationId = 'oiuccXwBYVazWqOO1e06'; @@ -44,21 +50,24 @@ describe('Visualization Container Component', () => { }; const wrapper = mount( - + + + ); wrapper.update(); diff --git a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss index 0130b340fb..da61c2f65b 100644 --- a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss +++ b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.scss @@ -8,6 +8,7 @@ cursor: -webkit-grab; cursor: grab; } + & :active { cursor: -webkit-grabbing; cursor: grabbing; @@ -34,6 +35,43 @@ text-align: center; } +.metricVis { + #metricsEditInline { + /* for ... reasons (?), margin here causes + other rules to activate... providing + a more-correct display. + */ + margin: 0 0; + } + + + #aggregation__field { + .euiFormLabel { + width: 130px + } + } + + // #explorerPlotComponent { + // //overflow: hidden; + // } + // + .svg-container { + //height: 425px !important; + margin-top: -25px !important; + } + + // .main-svg { + // //margin-top: -10px; + // //height: calc(100% - 10px); + // } + // + + .main-svg { + margin-top: -10px; + //height: calc(100% - 60px); + } +} + %center-div { top: 50%; left: 50%; @@ -45,8 +83,7 @@ } .visualization-loading-chart { - margin: 0; - position: absolute; + margin: 122px 0; @extend %center-div; } diff --git a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx index a5fb524740..3d52f9eddd 100644 --- a/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx +++ b/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx @@ -25,15 +25,19 @@ import { EuiToolTip, } from '@elastic/eui'; import React, { useEffect, useMemo, useState } from 'react'; -import _ from 'lodash'; +import { isEmpty } from 'lodash'; +import { useSelector } from 'react-redux'; import { displayVisualization, + fetchVisualizationById, renderCatalogVisualization, renderSavedVisualization, } from '../../helpers/utils'; import './visualization_container.scss'; import { VizContainerError } from '../../../../../common/types/custom_panels'; +import { metricQuerySelector } from '../../../metrics/redux/slices/metrics_slice'; import { coreRefs } from '../../../../framework/core_refs'; +import { PROMQL_METRIC_SUBTYPE } from '../../../../../common/constants/shared'; /* * Visualization container - This module is a placeholder to add visualizations in react-grid-layout @@ -60,8 +64,11 @@ interface Props { editMode: boolean; visualizationId: string; savedVisualizationId: string; + inputMetaData: object; fromTime: string; toTime: string; + span?: number | string; + resolution?: string; onRefresh: boolean; pplFilterValue: string; usedInNotebooks?: boolean; @@ -70,15 +77,18 @@ interface Props { showFlyout?: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; removeVisualization?: (visualizationId: string) => void; catalogVisualization?: boolean; - spanParam?: string; + inlineEditor?: JSX.Element; } export const VisualizationContainer = ({ editMode, visualizationId, savedVisualizationId, + inputMetaData, fromTime, toTime, + span, + resolution, onRefresh, pplFilterValue, usedInNotebooks, @@ -87,7 +97,7 @@ export const VisualizationContainer = ({ showFlyout, removeVisualization, catalogVisualization, - spanParam, + inlineEditor, }: Props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [visualizationTitle, setVisualizationTitle] = useState(''); @@ -103,6 +113,7 @@ export const VisualizationContainer = ({ const [isModalVisible, setIsModalVisible] = useState(false); const [modalContent, setModalContent] = useState(<>); + const queryMetaData = useSelector(metricQuerySelector(visualizationId)); const closeModal = () => setIsModalVisible(false); const showModal = (modalType: string) => { if (modalType === 'catalogModal') @@ -193,11 +204,10 @@ export const VisualizationContainer = ({ , ]; - const showModelPanel = [ + const showPPLQueryPanel = [ { closeActionsMenu(); showModal('catalogModal'); @@ -207,43 +217,61 @@ export const VisualizationContainer = ({ , ]; - if (usedInNotebooks) { - popoverPanel = catalogVisualization ? [showModelPanel] : [popoverPanel[0]]; + if (visualizationMetaData?.subType === PROMQL_METRIC_SUBTYPE) { + popoverPanel = [showPPLQueryPanel]; + } else if (usedInNotebooks) { + popoverPanel = [popoverPanel[0]]; } + const fetchVisualization = async () => { + return savedVisualizationId + ? await fetchVisualizationById(http, savedVisualizationId, setIsError) + : inputMetaData; + }; + const loadVisaulization = async () => { - if (catalogVisualization) - await renderCatalogVisualization({ + const visualization = await fetchVisualization(); + setVisualizationMetaData(visualization); + + if (!visualization && !savedVisualizationId) return; + + if (visualization.subType === PROMQL_METRIC_SUBTYPE) { + renderCatalogVisualization({ + visualization, http, pplService, - catalogSource: savedVisualizationId, + catalogSource: visualizationId, startTime: fromTime, endTime: toTime, + span, + resolution, filterQuery: pplFilterValue, - spanParam, setVisualizationTitle, setVisualizationType, setVisualizationData, setVisualizationMetaData, setIsLoading, setIsError, + queryMetaData, }); - else - await renderSavedVisualization( + } else + await renderSavedVisualization({ + visualization, http, pplService, savedVisualizationId, - fromTime, - toTime, + startTime: fromTime, + endTime: toTime, pplFilterValue, - spanParam, + span, + resolution, setVisualizationTitle, setVisualizationType, setVisualizationData, setVisualizationMetaData, setIsLoading, - setIsError - ); + setIsError, + }); }; const memoisedVisualizationBox = useMemo( @@ -251,7 +279,7 @@ export const VisualizationContainer = ({
{isLoading ? ( - ) : !_.isEmpty(isError) ? ( + ) : !isEmpty(isError) ? (
@@ -281,18 +309,21 @@ export const VisualizationContainer = ({ useEffect(() => { loadVisaulization(); - }, [onRefresh]); + }, [onRefresh, inputMetaData, span, resolution, fromTime, toTime]); + + const metricVisCssClassName = catalogVisualization ? 'metricVis' : ''; return ( <> -
+
+ {inlineEditor}
{memoisedVisualizationBox} diff --git a/public/components/custom_panels/redux/panel_slice.ts b/public/components/custom_panels/redux/panel_slice.ts index 1bad8f4523..82492a09ee 100644 --- a/public/components/custom_panels/redux/panel_slice.ts +++ b/public/components/custom_panels/redux/panel_slice.ts @@ -4,31 +4,24 @@ */ import { createSelector, createSlice } from '@reduxjs/toolkit'; -import { async, concat, from, Observable, of } from 'rxjs'; -import { map, mergeMap, tap, toArray } from 'rxjs/operators'; -import { forEach, last } from 'lodash'; +import { concat, from, Observable, of } from 'rxjs'; +import { map, mergeMap, toArray } from 'rxjs/operators'; +import { forEach } from 'lodash'; import { + createDemoPanel, CUSTOM_PANELS_API_PREFIX, CUSTOM_PANELS_SAVED_OBJECT_TYPE, - CUSTOM_PANEL_SLICE, - createDemoPanel, + samplePanelName, } from '../../../../common/constants/custom_panels'; import { CustomPanelListType, CustomPanelType, ObservabilityPanelAttrs, PanelType, - VisualizationType, } from '../../../../common/types/custom_panels'; import { coreRefs } from '../../../framework/core_refs'; import { SavedObject, SimpleSavedObject } from '../../../../../../src/core/public'; -import { isNameValid } from '../helpers/utils'; -import { samplePanelName } from '../../../../common/constants/custom_panels'; -import { - addMultipleVisualizations, - addVisualizationPanel, -} from '../helpers/add_visualization_helper'; -import { useToast } from '../../../../public/components/common/toast'; +import { addVisualizationPanel } from '../helpers/add_visualization_helper'; interface InitialState { id: string; @@ -84,8 +77,6 @@ const normalizedPanel = (panel: CustomPanelType): CustomPanelType => ({ export const selectPanelList = (rootState): CustomPanelType[] => rootState.customPanel.panelList; -const {setToast} = useToast(); - /* ** ASYNC DISPATCH FUNCTIONS */ @@ -100,7 +91,12 @@ const fetchObservabilityPanels$ = () => of(coreRefs.http.get(`${CUSTOM_PANELS_API_PREFIX}/panels`)).pipe( mergeMap((res) => res), mergeMap((res) => res.panels as ObservabilityPanelAttrs[]), - map((p: ObservabilityPanelAttrs) => ({ ...p, title: p.name, savedObject: false })) + map((p: ObservabilityPanelAttrs) => ({ + ...p, + title: p.name, + savedObject: false, + type: 'observability-savedObject', + })) ); // Fetches all saved Custom Panels @@ -141,19 +137,25 @@ export const uuidRx = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a export const isUuid = (id) => !!id.match(uuidRx); -export const updatePanel = (panel: CustomPanelType, successMsg: string, failureMsg: string) => async (dispatch, getState) => { +export const updatePanel = ( + panel: CustomPanelType, + successMsg: string, + failureMsg: string +) => async (dispatch, getState) => { + const { toasts } = coreRefs; + try { if (isUuid(panel.id)) await updateSavedObjectPanel(panel); else await updateLegacyPanel(panel); if (successMsg) { - setToast(successMsg) + toasts!.add(successMsg); } dispatch(setPanel(panel)); const panelList = getState().customPanel.panelList.map((p) => (p.id === panel.id ? panel : p)); dispatch(setPanelList(panelList)); - } catch (e) { + } catch (e) { if (failureMsg) { - setToast(failureMsg, 'danger') + toasts!.addDanger(failureMsg); } console.error(e); } @@ -172,20 +174,39 @@ export const addVizToPanels = (panels, vizId) => async (dispatch, getState) => { }); }; -export const addMultipleVizToPanels = (panels, vizIds) => async (dispatch, getState) => { - forEach(panels, (oldPanel) => { - const panel = getState().customPanel.panelList.find((p) => p.id === oldPanel.panel.id); +export const addMultipleVizToPanels = async (panels, vizIds) => { + await Promise.all( + panels.map(async ({ panel: { id } }) => { + const soPanel = await savedObjectPanelsClient.get(id); + const oldPanel = savedObjectToCustomPanel(soPanel); - const allVisualizations = panel!.visualizations; + const allVisualizations = oldPanel!.visualizations; - const visualizationsWithNewPanel = addMultipleVisualizations(vizIds, allVisualizations); + let visualizationsWithNewPanel = allVisualizations; - const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; - dispatch(updatePanel(updatedPanel, '', '')); - }); + forEach(vizIds, (vizId) => { + visualizationsWithNewPanel = addVisualizationPanel( + vizId, + undefined, + visualizationsWithNewPanel + ); + }); + + const updatedPanel = { ...oldPanel, visualizations: visualizationsWithNewPanel }; + + if (isUuid(updatedPanel.id)) { + await updateSavedObjectPanel(updatedPanel); + } else { + await updateLegacyPanel(updatedPanel); + } + }) + ); }; -export const replaceVizInPanel = (oldPanel, oldVizId, vizId, newVisualizationTitle) => async (dispatch, getState) => { +export const replaceVizInPanel = (oldPanel, oldVizId, vizId, newVisualizationTitle) => async ( + dispatch, + getState +) => { const panel = getState().customPanel.panelList.find((p) => p.id === oldPanel.id); const allVisualizations = panel!.visualizations; @@ -193,8 +214,14 @@ export const replaceVizInPanel = (oldPanel, oldVizId, vizId, newVisualizationTit const visualizationsWithNewPanel = addVisualizationPanel(vizId, oldVizId, allVisualizations); const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; - - dispatch(updatePanel(updatedPanel, `Visualization ${newVisualizationTitle} successfully added!`, `Error in adding ${newVisualizationTitle} visualization to the panel`)); + + dispatch( + updatePanel( + updatedPanel, + `Visualization ${newVisualizationTitle} successfully added!`, + `Error in adding ${newVisualizationTitle} visualization to the panel` + ) + ); }; const deletePanelSO = (customPanelIdList: string[]) => { @@ -211,6 +238,8 @@ const deleteLegacyPanels = (customPanelIdList: string[]) => { }; export const deletePanels = (panelsToDelete: CustomPanelType[]) => async (dispatch, getState) => { + const { toasts } = coreRefs; + const toastMessage = `Observability Dashboard${ panelsToDelete.length > 1 ? 's' : ' ' + panelsToDelete[0].title } successfully deleted!`; @@ -222,28 +251,27 @@ export const deletePanels = (panelsToDelete: CustomPanelType[]) => async (dispat (p) => !ids.includes(p.id) ); dispatch(setPanelList(panelList)); - setToast(toastMessage); + toasts!.add(toastMessage); } catch (e) { - setToast( - 'Error deleting Observability Dashboards, please make sure you have the correct permission.', - 'danger' + toasts!.addDanger( + 'Error deleting Observability Dashboards, please make sure you have the correct permission.' ); console.error(e); } }; export const createPanel = (panel) => async (dispatch, getState) => { + const { toasts } = coreRefs; try { const newSOPanel = await savedObjectPanelsClient.create(panel); const newPanel = savedObjectToCustomPanel(newSOPanel); const panelList = getState().customPanel.panelList; dispatch(setPanelList([...panelList, newPanel])); - setToast(`Observability Dashboard "${newPanel.title}" successfully created!`); + toasts!.add(`Observability Dashboard "${newPanel.title}" successfully created!`); window.location.replace(`#/${newPanel.id}`); } catch (e) { - setToast( - 'Error occurred while creating Observability Dashboard, please make sure you have the correct permission.', - 'danger' + toasts!.addDanger( + 'Error occurred while creating Observability Dashboard, please make sure you have the correct permission.' ); console.error(e); } @@ -263,6 +291,8 @@ export const createPanelSample = (vizIds) => async (dispatch, getState) => { }; export const clonePanel = (panel, newPanelName) => async (dispatch, getState) => { + const { toasts } = coreRefs; + try { const { id, ...panelCopy } = { ...panel, @@ -277,12 +307,11 @@ export const clonePanel = (panel, newPanelName) => async (dispatch, getState) => const panelList = getState().customPanel.panelList; dispatch(setPanelList([...panelList, newPanel])); dispatch(setPanel(newPanel)); - setToast(`Observability Dashboard "${newPanel.title}" successfully created!`); + toasts!.add(`Observability Dashboard "${newPanel.title}" successfully created!`); window.location.replace(`#/${newPanel.id}`); } catch (e) { - setToast( - 'Error cloning Observability Dashboard, please make sure you have the correct permission.', - 'danger' + toasts!.addDanger( + 'Error cloning Observability Dashboard, please make sure you have the correct permission.' ); console.error(e); } @@ -315,7 +344,13 @@ export const renameCustomPanel = (editedCustomPanelName: string, id: string) => ) => { const panel = getState().customPanel.panelList.find((p) => p.id === id); const updatedPanel = { ...panel, title: editedCustomPanelName }; - dispatch(updatePanel(updatedPanel, `Operational Panel successfully renamed into "${editedCustomPanelName}"`, 'Error renaming Operational Panel, please make sure you have the correct permission.')) + dispatch( + updatePanel( + updatedPanel, + `Operational Panel successfully renamed into "${editedCustomPanelName}"`, + 'Error renaming Operational Panel, please make sure you have the correct permission.' + ) + ); }; /* @@ -323,6 +358,8 @@ export const renameCustomPanel = (editedCustomPanelName: string, id: string) => */ const savedObjectToCustomPanel = (so: SimpleSavedObject): CustomPanelType => ({ id: so.id, + type: so.type, + objectId: so.type + ':' + so.id, ...so.attributes, savedObject: true, }); diff --git a/public/components/event_analytics/explorer/explorer.tsx b/public/components/event_analytics/explorer/explorer.tsx index 1f4806b3c0..fd88697b32 100644 --- a/public/components/event_analytics/explorer/explorer.tsx +++ b/public/components/event_analytics/explorer/explorer.tsx @@ -10,15 +10,15 @@ import { EuiFlexItem, EuiLink, EuiLoadingSpinner, + EuiPage, + EuiPageBody, + EuiPageSideBar, EuiPanel, EuiSpacer, + EuiSplitPanel, EuiTabbedContent, EuiTabbedContentTab, EuiText, - EuiPage, - EuiSplitPanel, - EuiPageSideBar, - EuiPageBody, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import _, { isEmpty, isEqual, reduce } from 'lodash'; @@ -57,6 +57,7 @@ import { import { LIVE_END_TIME, LIVE_OPTIONS, + PPL_METRIC_SUBTYPE, PPL_NEWLINE_REGEX, } from '../../../../common/constants/shared'; import { QueryManager } from '../../../../common/query_manager'; @@ -70,8 +71,8 @@ import { import { buildQuery, getIndexPatternFromRawQuery, - uiSettingsService, getSavingCommonParams, + uiSettingsService, } from '../../../../common/utils'; import { PPLDataFetcher } from '../../../services/data_fetchers/ppl/ppl_data_fetcher'; import { getSavedObjectsClient } from '../../../services/saved_objects/saved_object_client/client_factory'; @@ -91,8 +92,8 @@ import { selectSearchMetaData } from '../../event_analytics/redux/slices/search_ import { getVizContainerProps } from '../../visualizations/charts/helpers'; import { TabContext, useFetchEvents, useFetchPatterns, useFetchVisualizations } from '../hooks'; import { - selectCountDistribution, render as updateCountDistribution, + selectCountDistribution, } from '../redux/slices/count_distribution_slice'; import { selectFields, updateFields } from '../redux/slices/field_slice'; import { selectQueryResult } from '../redux/slices/query_result_slice'; @@ -102,8 +103,8 @@ import { selectExplorerVisualization } from '../redux/slices/visualization_slice import { change as changeVisualizationConfig, change as changeVizConfig, - selectVisualizationConfig, change as updateVizConfig, + selectVisualizationConfig, } from '../redux/slices/viualization_config_slice'; import { formatError, getDefaultVisConfig } from '../utils'; import { getContentTabTitle, getDateRange } from '../utils/utils'; @@ -120,10 +121,12 @@ import { DataSourceSelection } from './datasources/datasources_selection'; import { initialTabId } from '../../../framework/redux/store/shared_state'; import { ObservabilitySideBar } from './sidebar/observability_sidebar'; import { ExplorerSavedObjectLoader } from '../../../services/saved_objects/saved_object_loaders/explorer_saved_object_loader'; +import { processMetricsData } from '../../custom_panels/helpers/utils'; import { DEFAULT_DATA_SOURCE_TYPE, QUERY_LANGUAGE, } from '../../../../common/constants/data_sources'; +import { findMinInterval } from '../../common/query_utils'; export const Explorer = ({ pplService, @@ -206,8 +209,10 @@ export const Explorer = ({ explorerSearchMeta.lang || QUERY_LANGUAGE.SQL ) || {}; const SearchBar = ui?.SearchBar || Search; - const isDefaultDataSourceType = - explorerSearchMeta.datasources?.[0]?.type === DEFAULT_DATA_SOURCE_TYPE; + const isDefaultDataSourceType = [DEFAULT_DATA_SOURCE_TYPE, 'prometheus'].includes( + explorerSearchMeta.datasources?.[0]?.type + ); + const selectedIntervalRef = useRef<{ text: string; value: string; @@ -233,25 +238,7 @@ export const Explorer = ({ liveTailNameRef.current = liveTailName; const findAutoInterval = (start: string = '', end: string = '') => { - const momentStart = dateMath.parse(start)!; - const momentEnd = dateMath.parse(end, { roundUp: true })!; - const diffSeconds = momentEnd.unix() - momentStart.unix(); - let minInterval = 'y'; - - // less than 1 second - if (diffSeconds <= 1) minInterval = 'ms'; - // less than 2 minutes - else if (diffSeconds <= 60 * 2) minInterval = 's'; - // less than 2 hours - else if (diffSeconds <= 3600 * 2) minInterval = 'm'; - // less than 2 days - else if (diffSeconds <= 86400 * 2) minInterval = 'h'; - // less than 1 month - else if (diffSeconds <= 86400 * 31) minInterval = 'd'; - // less than 3 months - else if (diffSeconds <= 86400 * 93) minInterval = 'w'; - // less than 1 year - else if (diffSeconds <= 86400 * 366) minInterval = 'M'; + const minInterval = findMinInterval(start, end); setTimeIntervalOptions([ { text: 'Auto', value: 'auto_' + minInterval }, @@ -608,17 +595,22 @@ export const Explorer = ({ isQueryRunning, ]); + const visualizationSettings = !isEmpty(userVizConfigs[curVisId]) + ? { ...userVizConfigs[curVisId] } + : { + dataConfig: getDefaultVisConfig(queryManager.queryParser().parse(tempQuery).getStats()), + }; + const visualizations: IVisualizationContainerProps = useMemo(() => { return getVizContainerProps({ vizId: curVisId, rawVizData: explorerVisualizations, query, indexFields: explorerFields, - userConfigs: !isEmpty(userVizConfigs[curVisId]) - ? { ...userVizConfigs[curVisId] } - : { - dataConfig: getDefaultVisConfig(queryManager.queryParser().parse(tempQuery).getStats()), - }, + userConfigs: { + ...visualizationSettings, + ...processMetricsData(explorerData.schema, visualizationSettings), + }, appData: { fromApp: appLogEvents }, explorer: { explorerData, explorerFields, query, http, pplService }, }); @@ -740,7 +732,7 @@ export const Explorer = ({ ); } } else { - if (isTabHasObjID && isObjTypeMatchVis) { + if (isTabHasObjID && isObjTypeMatchVis && subType !== PPL_METRIC_SUBTYPE) { soClient = new SaveAsCurrentVisualization( { tabId, history, notifications, showPermissionErrorToast }, { batch, dispatch, changeQuery, updateTabName }, diff --git a/public/components/event_analytics/explorer/save_panel/save_panel.tsx b/public/components/event_analytics/explorer/save_panel/save_panel.tsx index 51c8b34c92..642adb6d49 100644 --- a/public/components/event_analytics/explorer/save_panel/save_panel.tsx +++ b/public/components/event_analytics/explorer/save_panel/save_panel.tsx @@ -20,6 +20,7 @@ import { fetchPanels, selectPanelList, } from '../../../../../public/components/custom_panels/redux/panel_slice'; +import { PPL_METRIC_SUBTYPE } from '../../../../../common/constants/shared'; interface ISavedPanelProps { selectedOptions: any; @@ -64,7 +65,7 @@ export const SavePanel = ({ const onToggleChange = (e: { target: { checked: React.SetStateAction } }) => { setChecked(e.target.checked); if (e.target.checked) { - setSubType('metric'); + setSubType(PPL_METRIC_SUBTYPE); } else { setSubType('visualization'); } diff --git a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap index 28c6cd8128..fe7ab15c31 100644 --- a/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap +++ b/public/components/event_analytics/explorer/sidebar/__tests__/__snapshots__/sidebar.test.tsx.snap @@ -701,7 +701,6 @@ exports[`Siderbar component Renders sidebar component 1`] = ` "unselectedFields": Array [], } } - tabId="DEFAULT_INDEX_PATTERNS" > @@ -1312,14 +1309,13 @@ exports[`Siderbar component Renders sidebar component 1`] = ` aria-label="inspect" className="dscSidebarField__actionButton" iconType="inspect" - isDisabled={true} onClick={[Function]} size="xs" >