diff --git a/.cypress/integration/8_metrics_analytics.spec.js b/.cypress/integration/8_metrics_analytics.spec.js index 36eb6cea42..255a354341 100644 --- a/.cypress/integration/8_metrics_analytics.spec.js +++ b/.cypress/integration/8_metrics_analytics.spec.js @@ -13,250 +13,304 @@ import { TESTING_PANEL, } from '../utils/metrics_constants'; import { suppressResizeObserverIssue, COMMAND_TIMEOUT_LONG } from '../utils/constants'; -import { - landOnPanels, - clearQuerySearchBoxText, -} from '../utils/event_analytics/helpers'; - -const moveToMetricsHome = () => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-metrics#/`); - cy.wait(delay * 3); -}; - -const moveToEventsExplorer = () => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-logs#/explorer`); - cy.wait(delay * 3); -}; +import { landOnPanels, clearQuerySearchBoxText } from '../utils/event_analytics/helpers'; -const moveToEventsHome = () => { - cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-logs#/`); - cy.wait(delay * 3); -}; - -describe('Creating custom metrics', () => { +describe('Metrics Analytics', () => { beforeEach(() => { - moveToEventsExplorer(); - clearQuerySearchBoxText('searchAutocompleteTextArea'); - suppressResizeObserverIssue(); + eraseSavedObjectMetrics(); }); - it('Create custom metric in event analytics and check it in events home', () => { - cy.get('[id^=autocomplete-textarea]').focus().type(PPL_METRICS[0], { - delay: 50, + describe('Creating custom metrics', () => { + beforeEach(() => { + moveToEventsExplorer(); + clearQuerySearchBoxText('searchAutocompleteTextArea'); + suppressResizeObserverIssue(); }); - cy.get('.euiButton__text').contains('Refresh').trigger('mouseover').click(); - cy.wait(delay); - suppressResizeObserverIssue(); - cy.get('button[id="main-content-vis"]').contains('Visualizations').trigger('mouseover').click(); - cy.wait(delay * 2); - cy.get('[data-test-subj="comboBoxToggleListButton"]').click(); - cy.get('[data-test-subj="comboBoxSearchInput"]').focus().type(VIS_TYPE_LINE, { force: true }); - cy.get('[data-test-subj="eventExplorer__saveManagementPopover"]').click({ force: true }); - cy.get('[data-test-subj="eventExplorer__querySaveName"]') - .focus() - .type(PPL_METRICS_NAMES[0], { force: true }); - cy.get('[data-test-subj="eventExplorer__metricSaveName"]').click({ force: true }); - cy.wait(1000); - cy.get('[data-test-subj="eventExplorer__querySaveConfirm"]', { - timeout: COMMAND_TIMEOUT_LONG, - }).click(); - cy.wait(delay); - cy.get('.euiToastHeader__title').contains('successfully').should('exist'); - moveToEventsHome(); - cy.get('[data-test-subj="eventHome__savedQueryTableName"]') - .first() - .contains(PPL_METRICS_NAMES[0]); - }); - it('Check for new metrics under recently created netrics', () => { - cy.get('[id^=autocomplete-textarea]').focus().type(PPL_METRICS[1], { - delay: 50, + it('Create custom metric in event analytics and check it in events home', () => { + createCustomMetric({ testMetricIndex: 0 }); + moveToEventsHome(); + cy.get('[data-test-subj="eventHome__savedQueryTableName"]') + .first() + .contains(PPL_METRICS_NAMES[metricIndex]); }); - cy.get('.euiButton__text').contains('Refresh').trigger('mouseover').click(); - cy.wait(delay); - suppressResizeObserverIssue(); - cy.get('button[id="main-content-vis"]').contains('Visualizations').trigger('mouseover').click(); - cy.wait(delay * 2); - cy.get('[data-test-subj="comboBoxInput"]').click(); - cy.get('.euiComboBoxOption__content').contains('Time series').click({ force: true }); - - cy.get('[data-test-subj="eventExplorer__saveManagementPopover"]').click({ force: true }); - cy.get('[data-test-subj="eventExplorer__querySaveName"]') - .focus() - .type(PPL_METRICS_NAMES[1], { force: true }); - cy.get('[data-test-subj="eventExplorer__metricSaveName"]').click({ force: true }); - cy.wait(1000); - cy.get('[data-test-subj="eventExplorer__querySaveConfirm"]', { - timeout: COMMAND_TIMEOUT_LONG, - }).click(); - cy.wait(delay); - cy.get('.euiToastHeader__title').contains('successfully').should('exist'); - moveToMetricsHome(); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[1]) - .should('exist'); }); -}); -describe('Search for metrics in search bar', () => { - beforeEach(() => { - moveToMetricsHome(); - suppressResizeObserverIssue(); + describe('Listing custom metrics', () => { + it('Check for new metrics under available metrics', () => { + createSavedObjectMetric({ testMetricIndex: 1 }); + + moveToMetricsHome(); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[1]) + .should('exist'); + }); }); - it('Search for metrics in search bar from available metrics', () => { - cy.get('[data-test-subj="metricsSearch"]').type('metric', { - delay: 50, + describe('Sidebar Actions', () => { + beforeEach(() => { + moveToMetricsHome(); + createSavedObjectMetric({ testMetricIndex: 0 }); + createSavedObjectMetric({ testMetricIndex: 1 }); + suppressResizeObserverIssue(); }); - cy.wait(delay); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[0]) - .should('exist'); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[1]) - .should('exist'); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains('go_memstats_alloc_bytes') - .should('not.exist'); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains('go_threads') - .should('not.exist'); - }); -}); + describe('Search for metrics in search bar', () => { + it('Search for metrics in search bar from available metrics', () => { + cy.get('[data-test-subj="metricsSearch"]').type('metric', { wait: 50 }); -describe('Select and unselect metrics in sidebar', () => { - it('Select and unselect metrics in sidebar', () => { - moveToMetricsHome(); - suppressResizeObserverIssue(); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[0]) - .trigger('mouseover') - .click(); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[1]) - .trigger('mouseover') - .click(); - cy.wait(50); - cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') - .contains(PPL_METRICS_NAMES[0]) - .should('exist'); - cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') - .contains(PPL_METRICS_NAMES[1]) - .should('exist'); - cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') - .contains(PPL_METRICS_NAMES[0]) - .trigger('mouseover') - .click(); - cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') - .contains(PPL_METRICS_NAMES[1]) - .trigger('mouseover') - .click(); - cy.wait(50); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[0]) - .trigger('mouseover') - .should('exist'); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[1]) - .trigger('mouseover') - .should('exist'); - }); -}); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[0]) + .should('exist'); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[1]) + .should('exist'); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains('go_memstats_alloc_bytes') + .should('not.exist'); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains('go_threads') + .should('not.exist'); + }); + }); -describe('Test Metric Visualizations', () => { - beforeEach(() => { - moveToMetricsHome(); - suppressResizeObserverIssue(); - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[0]) - .trigger('mouseover') - .click(); - }); + describe('Select and unselect metrics in sidebar', () => { + it('Select and unselect metrics in sidebar', () => { + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[0]) + .trigger('mouseover') + .click(); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[1]) + .trigger('mouseover') + .click(); + cy.wait(50); + cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') + .contains(PPL_METRICS_NAMES[0]) + .should('exist'); + cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') + .contains(PPL_METRICS_NAMES[1]) + .should('exist'); + cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') + .contains(PPL_METRICS_NAMES[0]) + .trigger('mouseover') + .click(); + cy.get('[data-test-subj="metricsListItems_selectedMetrics"]') + .contains(PPL_METRICS_NAMES[1]) + .trigger('mouseover') + .click(); + cy.wait(50); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[0]) + .trigger('mouseover') + .should('exist'); + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[1]) + .trigger('mouseover') + .should('exist'); + }); + }); - it('Resize a Metric visualization in edit mode', () => { - cy.get('[data-test-subj="metrics__editView"]') - .contains('Edit view') - .trigger('mouseover') - .click(); - cy.wait(delay); - cy.get('.react-resizable-handle-se') - // .eq(1) - .trigger('mousedown', { which: 1 }) - .trigger('mousemove', { clientX: 2000, clientY: 800 }) - .trigger('mouseup', { force: true }); - cy.wait(delay); - cy.get('[data-test-subj="metrics__saveView"]').trigger('mouseover').click(); - cy.wait(delay * 3); - cy.get('div.react-grid-layout>div').invoke('height').should('match', new RegExp('630')); - cy.wait(delay); - }); + describe('Test Metric Visualizations', () => { + beforeEach(() => { + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[0]) + .trigger('mouseover') + .click(); + }); - it('Drag and drop a Metric visualization in edit mode', () => { - cy.get('[data-test-subj="metricsListItems_recentlyCreated"]') - .contains(PPL_METRICS_NAMES[1]) - .trigger('mouseover') - .click(); - cy.get('[data-test-subj="metrics__editView"]') - .contains('Edit view') - .trigger('mouseover') - .click(); - cy.wait(delay); - cy.get('h5') - .contains(PPL_METRICS_NAMES[0]) - .trigger('mousedown', { which: 1, force: true }) - .trigger('mousemove', { clientX: 415, clientY: 500 }) - .trigger('mouseup', { force: true }); - cy.wait(delay); - cy.get('[data-test-subj="metrics__saveView"]') - .trigger('mouseover') - .click({ force: true }) - .then(() => { + it.only('Resize a Metric visualization in edit mode', () => { + cy.get('[data-test-subj="metrics__editView"]') + .contains('Edit view') + .trigger('mouseover') + .click(); + cy.wait(delay); + cy.get('.react-resizable-handle-se') + // .eq(1) + .trigger('mousedown', { which: 1 }) + .trigger('mousemove', { clientX: 2000, clientY: 800 }) + .trigger('mouseup', { force: true }); + cy.wait(delay); + cy.get('[data-test-subj="metrics__saveView"]').trigger('mouseover').click(); cy.wait(delay * 3); - cy.get('div.react-grid-layout>div') - .eq(1) - .invoke('attr', 'style') - .should('match', new RegExp('(.*)transform: translate((.*)10px)(.*)')); + cy.get('div.react-grid-layout>div').invoke('height').should('match', new RegExp('790')); cy.wait(delay); }); - }); - it('Change date filter of the Metrics home page', () => { - cy.get('.euiButtonEmpty[data-test-subj="superDatePickerToggleQuickMenuButton"]').click({ - force: true, + it('Drag and drop a Metric visualization in edit mode', () => { + cy.get('[data-test-subj="metricsListItems_availableMetrics"]') + .contains(PPL_METRICS_NAMES[1]) + .trigger('mouseover') + .click(); + cy.get('[data-test-subj="metrics__editView"]') + .contains('Edit view') + .trigger('mouseover') + .click(); + cy.wait(delay); + cy.get('h5') + .contains(PPL_METRICS_NAMES[0]) + .trigger('mousedown', { which: 1, force: true }) + .trigger('mousemove', { clientX: 415, clientY: 500 }) + .trigger('mouseup', { force: true }); + cy.wait(delay); + cy.get('[data-test-subj="metrics__saveView"]') + .trigger('mouseover') + .click({ force: true }) + .then(() => { + cy.wait(delay * 3); + cy.get('div.react-grid-layout>div') + .eq(1) + .invoke('attr', 'style') + .should('match', new RegExp('(.*)transform: translate((.*)10px)(.*)')); + cy.wait(delay); + }); + }); + + it('Change date filter of the Metrics home page', () => { + cy.get('.euiButtonEmpty[data-test-subj="superDatePickerToggleQuickMenuButton"]').click({ + force: true, + }); + cy.get('.euiLink').contains('This year').trigger('mouseover').click(); + cy.wait(delay * 2); + cy.get('.euiSuperDatePicker__prettyFormat[data-test-subj="superDatePickerShowDatesButton"]') + .contains('This year') + .should('exist'); + cy.wait(delay); + }); + + it('Saves metrics to an existing panel', () => { + landOnPanels(); + cy.get('[data-test-subj="customPanels__createNewPanels"]').click(); + cy.get('input.euiFieldText').type(TESTING_PANEL); + cy.get('.euiButton__text', { timeout: COMMAND_TIMEOUT_LONG }) + .contains(/^Create$/) + .click(); + cy.wait(delay * 3); + moveToMetricsHome(); + cy.get('[data-test-subj="metrics__saveManagementPopover"]').trigger('mouseover').click(); + cy.get('[data-test-subj="comboBoxSearchInput"]') + .focus() + .type(TESTING_PANEL, { force: true }); + cy.get('[data-test-subj="metrics__SaveConfirm"]').click({ force: true }); + cy.get('.euiToastHeader__title').contains('successfully').should('exist'); + }); }); - cy.get('.euiLink').contains('This year').trigger('mouseover').click(); - cy.wait(delay * 2); - cy.get('.euiSuperDatePicker__prettyFormat[data-test-subj="superDatePickerShowDatesButton"]') - .contains('This year') - .should('exist'); - cy.wait(delay); - }); - it('Saves metrics to an existing panel', () => { - landOnPanels(); - cy.get('[data-test-subj="customPanels__createNewPanels"]').click(); - cy.get('input.euiFieldText').type(TESTING_PANEL); - cy.get('.euiButton__text', { timeout: COMMAND_TIMEOUT_LONG }) - .contains(/^Create$/) - .click(); - cy.wait(delay * 3); - moveToMetricsHome(); - cy.get('[data-test-subj="metrics__saveManagementPopover"]').trigger('mouseover').click(); - cy.get('[data-test-subj="comboBoxSearchInput"]').focus().type(TESTING_PANEL, { force: true }); - cy.get('[data-test-subj="metrics__SaveConfirm"]').click({ force: true }); - cy.get('.euiToastHeader__title').contains('successfully').should('exist'); + describe('Has working breadcrumbs', () => { + it('Redirect to correct page on breadcrumb click', () => { + cy.get('[data-test-subj="metricsSearch"]').should('exist'); + cy.get('.euiTitle').contains('Metrics').should('exist'); + cy.get('.euiBreadcrumb[href="observability-logs#/"]').click(), + { timeout: COMMAND_TIMEOUT_LONG }; + cy.get('.euiTitle').contains('Logs').should('exist'); + }); + }); }); }); -describe('Has working breadcrumbs', () => { - it('Redirect to correct page on breadcrumb click', () => { - moveToMetricsHome(); - suppressResizeObserverIssue(); - cy.get('[data-test-subj="metricsSearch"]').should('exist'); - cy.get('.euiTitle').contains('Metrics').should('exist'); - cy.get('.euiBreadcrumb[href="observability-logs#/"]').click(), - { timeout: COMMAND_TIMEOUT_LONG }; - cy.get('.euiTitle').contains('Logs').should('exist'); +const moveToMetricsHome = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-metrics#/`); + cy.wait(delay * 3); +}; + +const moveToEventsExplorer = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-logs#/explorer`); + cy.wait(delay * 3); +}; + +const moveToEventsHome = () => { + cy.visit(`${Cypress.env('opensearchDashboards')}/app/observability-logs#/`); + cy.wait(delay * 3); +}; + +const createCustomMetric = ({ testMetricIndex }) => { + cy.get('[id^=autocomplete-textarea]').focus().type(PPL_METRICS[metricIndex], { + delay: 50, }); -}); + cy.get('.euiButton__text').contains('Refresh').trigger('mouseover').click(); + cy.wait(delay); + suppressResizeObserverIssue(); + cy.get('button[id="main-content-vis"]').contains('Visualizations').trigger('mouseover').click(); + cy.wait(delay * 2); + cy.get('[data-test-subj="comboBoxToggleListButton"]').click(); + cy.get('[data-test-subj="comboBoxSearchInput"]').focus().type(VIS_TYPE_LINE, { force: true }); + cy.get('[data-test-subj="eventExplorer__saveManagementPopover"]').click({ force: true }); + cy.get('[data-test-subj="eventExplorer__querySaveName"]') + .focus() + .type(PPL_METRICS_NAMES[metricIndex], { force: true }); + cy.get('[data-test-subj="eventExplorer__metricSaveName"]').click({ force: true }); + cy.wait(1000); + cy.get('[data-test-subj="eventExplorer__querySaveConfirm"]', { + timeout: COMMAND_TIMEOUT_LONG, + }).click(); + cy.wait(delay); + cy.get('.euiToastHeader__title').contains('successfully').should('exist'); +}; + +const createSavedObjectMetric = ({ testMetricIndex }) => { + return cy + .request({ + method: 'POST', + failOnStatusCode: false, + url: 'api/saved_objects/observability-visualization', + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + body: { + attributes: { + title: PPL_METRICS_NAMES[testMetricIndex], + description: '', + version: 1, + createdTimeMs: new Date().getTime(), + savedVisualization: { + query: PPL_METRICS[testMetricIndex], + selected_date_range: { + start: 'now-15m', + end: 'now', + text: '', + }, + selected_timestamp: { + name: 'timestamp', + type: 'timestamp', + }, + selected_fields: { + tokens: [], + text: '', + }, + name: PPL_METRICS_NAMES[testMetricIndex], + description: '', + type: 'line', + sub_type: 'metric', + }, + }, + }, + }) + .then((response) => response.body); +}; + +const eraseSavedObjectMetrics = () => { + return cy + .request({ + method: 'get', + failOnStatusCode: false, + url: 'api/saved_objects/_find?type=observability-visualization', + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + }) + .then((response) => { + response.body.saved_objects.map((soMetric) => { + cy.request({ + method: 'DELETE', + failOnStatusCode: false, + url: `api/saved_objects/observability-visualization/${soMetric.id}`, + headers: { + 'content-type': 'application/json;charset=UTF-8', + 'osd-xsrf': true, + }, + }); + }); + }); +}; diff --git a/public/components/custom_panels/helpers/utils.tsx b/public/components/custom_panels/helpers/utils.tsx index 7bbcafd400..ec864cb548 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -6,7 +6,7 @@ import dateMath from '@elastic/datemath'; import { ShortDate } from '@elastic/eui'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; -import _, { isEmpty } from 'lodash'; +import _, { castArray, forEach, isEmpty } from 'lodash'; import { Moment } from 'moment-timezone'; import React from 'react'; import { Layout } from 'react-grid-layout'; @@ -30,6 +30,7 @@ import { SavedObjectsActions } from '../../../services/saved_objects/saved_objec import { ObservabilitySavedVisualization } from '../../../services/saved_objects/saved_object_client/types'; import { getDefaultVisConfig } from '../../event_analytics/utils'; import { Visualization } from '../../visualizations/visualization'; +import { MetricType } from '../../../../common/types/metrics'; /* * "Utils" This file contains different reused functions in operational panels @@ -51,14 +52,23 @@ export const isNameValid = (name: string) => { }; // DateTime convertor to required format -export const convertDateTime = (datetime: string, isStart = true, formatted = true) => { +export const convertDateTime = ( + datetime: string, + isStart = true, + formatted = true, + isMetrics: boolean = false +) => { let returnTime: undefined | Moment; if (isStart) { returnTime = dateMath.parse(datetime); } else { returnTime = dateMath.parse(datetime, { roundUp: true }); } - + if (isMetrics) { + const myDate = new Date(returnTime._d); // Your timezone! + const epochTime = myDate.getTime() / 1000.0; + return Math.round(epochTime); + } if (formatted) return returnTime!.utc().format(PPL_DATE_FORMAT); return returnTime; }; @@ -202,14 +212,19 @@ export const getQueryResponse = ( setIsLoading: React.Dispatch>, setIsError: React.Dispatch>, filterQuery = '', - timestampField = 'timestamp' + timestampField = 'timestamp', + metricVisualization = false ) => { setIsLoading(true); setIsError({} as VizContainerError); let finalQuery = ''; try { - finalQuery = queryAccumulator(query, timestampField, startTime, endTime, filterQuery); + if (!metricVisualization) { + finalQuery = queryAccumulator(query, timestampField, startTime, endTime, filterQuery); + } else { + finalQuery = query; + } } catch (error) { const errorMessage = 'Issue in building final query'; setIsError({ errorMessage }); @@ -311,37 +326,89 @@ const createCatalogVisualizationMetaData = ( }; }; +const updateCatalogVisualizationQuery = ({ + catalogSourceName, + catalogTableName, + aggregation, + attributesGroupBy, + startTime, + endTime, + spanParam, +}: { + catalogSourceName: string; + catalogTableName: string; + aggregation: string; + attributesGroupBy: string[]; + startTime: string; + endTime: string; + spanParam: string | undefined; +}) => { + const attributesGroupString = attributesGroupBy.toString(); + const startEpochTime = convertDateTime(startTime, true, false, true); + const endEpochTime = convertDateTime(endTime, false, false, true); + const promQuery = + attributesGroupBy.length === 0 + ? catalogTableName + : `${aggregation} by(${attributesGroupString}) (${catalogTableName})`; + + return `source = ${catalogSourceName}.query_range('${promQuery}', ${startEpochTime}, ${endEpochTime}, '${spanParam}')`; +}; + // Creates a catalogVisualization for a runtime catalog based PPL query and runs getQueryResponse -export const renderCatalogVisualization = async ( - http: CoreStart['http'], - pplService: PPLService, - catalogSource: 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>, - spanResolution?: string -) => { +export const renderCatalogVisualization = async ({ + http, + pplService, + catalogSource, + startTime, + endTime, + filterQuery, + spanParam, + setVisualizationTitle, + setVisualizationType, + setVisualizationData, + setVisualizationMetaData, + setIsLoading, + setIsError, + spanResolution, + queryMetaData, +}: { + http: CoreStart['http']; + pplService: PPLService; + catalogSource: 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>; + spanResolution?: string; + queryMetaData?: MetricType; +}) => { setIsLoading(true); setIsError({} as VizContainerError); const visualizationType = 'line'; const visualizationTimeField = '@timestamp'; - let visualizationQuery = `source = ${catalogSource} | stats avg(@value) by span(${visualizationTimeField},1h)`; - if (spanParam !== undefined) { - visualizationQuery = updateQuerySpanInterval( - visualizationQuery, - visualizationTimeField, - spanParam - ); - } + 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, + }); const visualizationMetaData = createCatalogVisualizationMetaData( catalogSource, @@ -349,6 +416,15 @@ export const renderCatalogVisualization = async ( visualizationType, visualizationTimeField ); + + visualizationMetaData.user_configs = { + layoutConfig: { + height: 390, + margin: { t: 5 }, + legend: { orientation: 'h', yanchor: 'top', x: 0.0, y: -0.4 }, + }, + }; + setVisualizationTitle(catalogSource); setVisualizationType(visualizationType); @@ -364,7 +440,8 @@ export const renderCatalogVisualization = async ( setIsLoading, setIsError, filterQuery, - visualizationTimeField + visualizationTimeField, + true ); }; @@ -397,9 +474,7 @@ 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 - ? JSON.parse(visualization.savedVisualization.user_configs) - : {}, + user_configs: visualization.savedVisualization.user_configs || {}, sub_type: visualization.savedVisualization.hasOwnProperty('sub_type') ? visualization.savedVisualization.sub_type : '', @@ -462,16 +537,43 @@ export const isPPLFilterValid = ( return true; }; +export const processMetricsData = (schema: any, dataConfig: any) => { + if (isEmpty(schema)) return {}; + if ( + schema.length === 3 && + schema.every((schemaField) => ['@labels', '@value', '@timestamp'].includes(schemaField.name)) + ) { + return prepareMetricsData(schema, dataConfig); + } + return {}; +}; + +export const prepareMetricsData = (schema: any, dataConfig: any) => { + const metricBreakdown: any[] = []; + const metricSeries: any[] = []; + const metricDimension: any[] = []; + + forEach(schema, (field) => { + if (field.name === '@timestamp') + metricDimension.push({ name: '@timestamp', label: '@timestamp' }); + if (field.name === '@labels') metricBreakdown.push({ name: '@labels', customLabel: '@labels' }); + if (field.name === '@value') metricSeries.push({ name: '@value', label: '@value' }); + }); + + return { + breakdowns: metricBreakdown, + series: metricSeries, + dimensions: metricDimension, + span: {}, + }; +}; + // Renders visualization in the vizualization container component export const displayVisualization = (metaData: any, data: any, type: string) => { if (metaData === undefined || isEmpty(metaData)) { return <>; } - if (metaData.user_configs !== undefined && metaData.user_configs !== '') { - metaData.user_configs = JSON.parse(metaData.user_configs); - } - const dataConfig = { ...(metaData.user_configs?.dataConfig || {}) }; const hasBreakdowns = !_.isEmpty(dataConfig.breakdowns); const realTimeParsedStats = { @@ -487,13 +589,16 @@ export const displayVisualization = (metaData: any, data: any, type: string) => ); } - const finalDataConfig = { + let finalDataConfig = { ...dataConfig, ...realTimeParsedStats, dimensions: finalDimensions, breakdowns, }; + // add metric specific overriding + finalDataConfig = { ...finalDataConfig, ...processMetricsData(data.schema, finalDataConfig) }; + const mixedUserConfigs = { availabilityConfig: { ...(metaData.user_configs?.availabilityConfig || {}), 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 e74e485089..a5fb524740 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 @@ -26,8 +26,6 @@ import { } from '@elastic/eui'; import React, { useEffect, useMemo, useState } from 'react'; import _ from 'lodash'; -import { CoreStart } from '../../../../../../../src/core/public'; -import PPLService from '../../../../services/requests/ppl'; import { displayVisualization, renderCatalogVisualization, @@ -35,6 +33,7 @@ import { } from '../../helpers/utils'; import './visualization_container.scss'; import { VizContainerError } from '../../../../../common/types/custom_panels'; +import { coreRefs } from '../../../../framework/core_refs'; /* * Visualization container - This module is a placeholder to add visualizations in react-grid-layout @@ -58,11 +57,9 @@ import { VizContainerError } from '../../../../../common/types/custom_panels'; */ interface Props { - http: CoreStart['http']; editMode: boolean; visualizationId: string; savedVisualizationId: string; - pplService: PPLService; fromTime: string; toTime: string; onRefresh: boolean; @@ -77,11 +74,9 @@ interface Props { } export const VisualizationContainer = ({ - http, editMode, visualizationId, savedVisualizationId, - pplService, fromTime, toTime, onRefresh, @@ -103,6 +98,7 @@ export const VisualizationContainer = ({ const [isError, setIsError] = useState({} as VizContainerError); const onActionsMenuClick = () => setIsPopoverOpen((currPopoverOpen) => !currPopoverOpen); const closeActionsMenu = () => setIsPopoverOpen(false); + const { http, pplService } = coreRefs; const [isModalVisible, setIsModalVisible] = useState(false); const [modalContent, setModalContent] = useState(<>); @@ -217,21 +213,21 @@ export const VisualizationContainer = ({ const loadVisaulization = async () => { if (catalogVisualization) - await renderCatalogVisualization( + await renderCatalogVisualization({ http, pplService, - savedVisualizationId, - fromTime, - toTime, - pplFilterValue, + catalogSource: savedVisualizationId, + startTime: fromTime, + endTime: toTime, + filterQuery: pplFilterValue, spanParam, setVisualizationTitle, setVisualizationType, setVisualizationData, setVisualizationMetaData, setIsLoading, - setIsError - ); + setIsError, + }); else await renderSavedVisualization( http, diff --git a/public/components/metrics/redux/slices/metrics_slice.ts b/public/components/metrics/redux/slices/metrics_slice.ts index 630fdce962..e323ba628d 100644 --- a/public/components/metrics/redux/slices/metrics_slice.ts +++ b/public/components/metrics/redux/slices/metrics_slice.ts @@ -185,7 +185,10 @@ export const { export const availableMetricsSelector = (state) => state.metrics.metrics .filter((metric) => !state.metrics.selected.includes(metric.id)) - .filter((metric) => state.metrics.search === '' || metric.name.includes(state.metrics.search)); + .filter( + (metric) => + state.metrics.search === '' || metric.name.match(new RegExp(state.metrics.search, 'i')) + ); export const selectedMetricsSelector = (state) => state.metrics.selected.map((id) => state.metrics.metrics.find((metric) => metric.id === id)); diff --git a/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap b/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap index 1cecd70e49..5cc2fee086 100644 --- a/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap +++ b/public/components/metrics/view/__tests__/__snapshots__/metrics_grid.test.tsx.snap @@ -224,7 +224,6 @@ exports[`Metrics Grid Component renders Metrics Grid Component 1`] = ` editActionType="save" editMode={false} endTime="now" - http={[MockFunction]} moveToEvents={[MockFunction]} onRefresh={true} panelVisualizations={ @@ -258,12 +257,6 @@ exports[`Metrics Grid Component renders Metrics Grid Component 1`] = ` }, ] } - pplService={ - PPLService { - "fetch": [Function], - "http": [MockFunction], - } - } setEditActionType={ [MockFunction] { "calls": Array [ @@ -680,17 +673,10 @@ exports[`Metrics Grid Component renders Metrics Grid Component 1`] = ` { configure({ adapter: new Adapter() }); const store = createStore(rootReducer); + const core = coreStartMock; it('renders Metrics Grid Component', async () => { - httpClientMock.get = jest.fn(() => Promise.resolve((sampleMetric as unknown) as HttpResponse)); - httpClientMock.post = jest.fn(() => - Promise.resolve((samplePPLResponse as unknown) as HttpResponse) - ); - - const http = httpClientMock; - const core = coreStartMock; const panelVisualizations = sampleMetricsVisualizations; const setPanelVisualizations = jest.fn(); const editMode = false; - const pplService = new PPLService(httpClientMock); const startTime = 'now-30m'; const endTime = 'now'; const onEditClick = jest.fn(); @@ -45,15 +34,28 @@ describe('Metrics Grid Component', () => { const spanParam = '1h'; const setEditActionType = jest.fn(); + coreRefs.pplService = new PPLService(httpClientMock); + coreRefs.pplService.fetch = jest.fn(() => + Promise.resolve({ + data: { + datarows: [], + schema: [ + { name: '@timestamp', type: 'timestamp' }, + { name: '@value', type: 'number' }, + { name: '@labels', type: 'string' }, + ], + }, + then: () => Promise.resolve(), + }) + ); + const wrapper = mount( >; editMode: boolean; - pplService: PPLService; startTime: string; endTime: string; moveToEvents: (savedVisualizationId: string) => any; @@ -38,12 +35,10 @@ interface MetricsGridProps { } export const MetricsGrid = ({ - http, chrome, panelVisualizations, setPanelVisualizations, editMode, - pplService, startTime, endTime, moveToEvents, @@ -75,11 +70,9 @@ export const MetricsGrid = ({ const gridDataComps = panelVisualizations.map((panelVisualization: MetricType, index) => ( { const { @@ -36,7 +38,7 @@ export const Line = ({ visualizations, layout, config }: any) => { const { data: { explorer: { - explorerData: { jsonData }, + explorerData: { schema, jsonData }, }, userConfigs: { dataConfig: { @@ -81,6 +83,32 @@ export const Line = ({ visualizations, layout, config }: any) => { colorTheme.find((colorSelected) => colorSelected.name.name === field)?.color) || PLOTLY_COLOR[index % PLOTLY_COLOR.length]; + const checkIfMetrics = (metricsSchema: any) => { + if (isEmpty(metricsSchema)) return false; + + if ( + metricsSchema.length === 3 && + metricsSchema.every((schemaField) => + ['@labels', '@value', '@timestamp'].includes(schemaField.name) + ) + ) { + return true; + } + return false; + }; + + const preprocessMetricsJsonData = (json): any => { + const data: any[] = []; + _.forEach(json, (row) => { + const record: any = {}; + record['@labels'] = JSON.parse(row['@labels']); + record['@timestamp'] = JSON.parse(row['@timestamp']); + record['@value'] = JSON.parse(row['@value']); + data.push(record); + }); + return data; + }; + const addStylesToTraces = (traces, traceStyles) => { const { fillOpacity: opac, @@ -123,12 +151,18 @@ export const Line = ({ visualizations, layout, config }: any) => { }; let lines = useMemo(() => { - const visConfig = { + let visConfig = { dimensions, series, breakdowns, span, }; + + visConfig = { + ...visConfig, + ...processMetricsData(schema, visConfig), + }; + const traceStyles = { fillOpacity, tooltipMode, @@ -137,19 +171,42 @@ export const Line = ({ visualizations, layout, config }: any) => { lineWidth, markerSize, }; + const traceStylesForMetrics = { + fillOpacity: 0, + tooltipMode, + tooltipText, + lineShape, + lineWidth: 3, + markerSize, + }; const lineSpecficMetaData = { x_coordinate: 'x', y_coordinate: 'y', }; - return addStylesToTraces( - transformPreprocessedDataToTraces( - preprocessJsonData(jsonData, visConfig), - visConfig, - lineSpecficMetaData - ), - traceStyles - ); + if (checkIfMetrics(schema)) { + const formattedMetricsJson = preprocessMetricsJsonData(jsonData); + return addStylesToTraces( + formattedMetricsJson?.map((trace) => { + return { + ...trace, + x: trace['@timestamp'], + y: trace['@value'], + name: JSON.stringify(trace['@labels']), + }; + }), + traceStylesForMetrics + ); + } else { + return addStylesToTraces( + transformPreprocessedDataToTraces( + preprocessJsonData(jsonData, visConfig), + visConfig, + lineSpecficMetaData + ), + traceStyles + ); + } }, [chartStyles, jsonData, dimensions, series, span, breakdowns, panelOptions, tooltipOptions]); const mergedLayout = useMemo(() => { @@ -162,11 +219,15 @@ export const Line = ({ visualizations, layout, config }: any) => { }, }; + const { legend: layoutConfigLegend, ...layoutConfigGeneral } = layoutConfig; + return { ...layout, + ...layoutConfigGeneral, title: panelOptions.title || layoutConfig.layout?.title || '', legend: { ...layout.legend, + ...layoutConfigLegend, orientation: legendPosition, ...(legendSize && { font: { diff --git a/public/components/visualizations/charts/shared/common.ts b/public/components/visualizations/charts/shared/common.ts index 50790511e9..13eafcd30e 100644 --- a/public/components/visualizations/charts/shared/common.ts +++ b/public/components/visualizations/charts/shared/common.ts @@ -89,7 +89,6 @@ export const preprocessJsonData = ( const backtickRemovedEntry = { ...removeBackTick(entry), }; - forEach(series, (sr) => { let tabularVizData: IIntermediateMapping = { value: 0, diff --git a/test/__mocks__/coreMocks.ts b/test/__mocks__/coreMocks.ts index 101c27a127..45615411f1 100644 --- a/test/__mocks__/coreMocks.ts +++ b/test/__mocks__/coreMocks.ts @@ -7,6 +7,7 @@ import { of } from 'rxjs'; import { CoreStart } from '../../../../src/core/public'; import { coreMock } from '../../../../src/core/public/mocks'; import httpClientMock from './httpClientMock'; +import PPLService from '../../public/services/requests/ppl'; const coreStart = coreMock.createStart(); coreStart.savedObjects.client.find = jest.fn(() => Promise.resolve({ savedObjects: [] })) as any; @@ -15,6 +16,7 @@ coreStart.savedObjects.client.find = jest.fn(() => Promise.resolve({ savedObject const coreStartMock = ({ ...coreStart, http: httpClientMock, + pplService: new PPLService(httpClientMock), } as unknown) as CoreStart; export { coreStartMock };