diff --git a/.cypress/integration/metrics_analytics_test/metrics_analytics.spec.js b/.cypress/integration/metrics_analytics_test/metrics_analytics.spec.js index d7898ba5ad..c00e2b7045 100644 --- a/.cypress/integration/metrics_analytics_test/metrics_analytics.spec.js +++ b/.cypress/integration/metrics_analytics_test/metrics_analytics.spec.js @@ -281,7 +281,7 @@ const createSavedObjectMetric = ({ testMetricIndex }) => { name: PPL_METRICS_NAMES[testMetricIndex], description: '', type: 'line', - sub_type: 'metric', + subType: 'metric', }, }, }, diff --git a/.cypress/integration/panels_test/panels.spec.ts b/.cypress/integration/panels_test/panels.spec.ts index 3750c2e02e..94fc0ac0ca 100644 --- a/.cypress/integration/panels_test/panels.spec.ts +++ b/.cypress/integration/panels_test/panels.spec.ts @@ -17,7 +17,7 @@ import { } from '../../utils/panel_constants'; describe('Panels testing with Sample Data', () => { - suppressResizeObserverIssue();//needs to be in file once + suppressResizeObserverIssue(); //needs to be in file once before(() => { cy.visit(`${Cypress.env('opensearchDashboards')}/app/home#/tutorial_directory/sampleData`); @@ -30,7 +30,7 @@ describe('Panels testing with Sample Data', () => { beforeEach(() => { eraseTestPanels(); eraseSavedVisualizations(); - }) + }); after(() => { eraseTestPanels(); @@ -54,8 +54,8 @@ describe('Panels testing with Sample Data', () => { .click({ force: true }); cy.get('[data-test-subj="eventExplorer__saveManagementPopover"]') .trigger('mouseover') - .click({force: true}); - cy.wait(delay*5); //Wont save as correct name without wait + .click({ force: true }); + cy.wait(delay * 5); //Wont save as correct name without wait cy.get('[data-test-subj="eventExplorer__querySaveName"]') .focus() .type(PPL_VISUALIZATIONS_NAMES[0]); @@ -151,7 +151,7 @@ describe('Panels testing with Sample Data', () => { it('Searches panels', () => { createLegacyPanel('Legacy Named'); createSavedObjectPanel('Saved Object'); - cy.wait(delay);//Needed so the panel appears on the dashboard page + cy.wait(delay); //Needed so the panel appears on the dashboard page cy.reload(); cy.get('input[data-test-subj="operationalPanelSearchBar"]') .focus() @@ -203,7 +203,7 @@ describe('Panels testing with Sample Data', () => { it('Deletes the panel', () => { createSavedObjectPanel(); - cy.get('a[data-test-subj="breadcrumb last"]').click();//refresh so panel appears + cy.get('a[data-test-subj="breadcrumb last"]').click(); //refresh so panel appears cy.get('input[data-test-subj="checkboxSelectAll"]').click(); openActionsDropdown(); cy.get('button[data-test-subj="deleteContextMenuItem"]').click({ force: true }); @@ -251,7 +251,7 @@ describe('Panels testing with Sample Data', () => { beforeEach(() => { const test_name = `test_${new Date().getTime()}`; createSavedObjectPanel(test_name).as('thePanel'); - cy.then(function (){ + cy.then(function () { moveToThePanel(this.thePanel.id); }); }); @@ -264,19 +264,23 @@ describe('Panels testing with Sample Data', () => { it('Redirects to correct page on breadcrumb click', () => { cy.get('a[data-test-subj="breadcrumb last"]').click(); - cy.then(function (){ - cy.get('h1[data-test-subj="panelNameHeader"]').contains(this.thePanel.attributes.title).should('exist'); + cy.then(function () { + cy.get('h1[data-test-subj="panelNameHeader"]') + .contains(this.thePanel.attributes.title) + .should('exist'); }); }); it('Duplicate the open panel', () => { cy.get('button[data-test-subj="panelActionContextMenu"]').click(); cy.get('button[data-test-subj="duplicatePanelContextMenuItem"]').click(); - cy.then(function (){ - cy.get(`input.euiFieldText[value="${this.thePanel.attributes.title} (copy)"]`).should('exist'); + cy.then(function () { + cy.get(`input.euiFieldText[value="${this.thePanel.attributes.title} (copy)"]`).should( + 'exist' + ); }); cy.get('button[data-test-subj="runModalButton"]').click(); - cy.then(function (){ + cy.then(function () { cy.get('h1[data-test-subj="panelNameHeader"]') .contains(this.thePanel.attributes.title + ' (copy)') .should('exist'); @@ -284,9 +288,11 @@ describe('Panels testing with Sample Data', () => { }); it('Rename the open panel', () => { - cy.then(function (){ + cy.then(function () { cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click(); cy.get('button[data-test-subj="panelActionContextMenu"]').click(); cy.get('button[data-test-subj="renamePanelContextMenuItem"]').click(); @@ -295,15 +301,19 @@ describe('Panels testing with Sample Data', () => { .clear({ force: true }) .focus() .type('Renamed Panel'); - }); + }); cy.get('button[data-test-subj="runModalButton"]').click(); cy.get('h1[data-test-subj="panelNameHeader"]').contains('Renamed Panel').should('exist'); }); it('Change date filter of the panel', () => { - cy.get('.euiButtonEmpty[data-test-subj="superDatePickerToggleQuickMenuButton"]').click({force: true}); - cy.wait(delay);//flyout won't open sometimes without - cy.get('button[data-test-subj="superDatePickerCommonlyUsed_This_year"]').click({force: true}); + cy.get('.euiButtonEmpty[data-test-subj="superDatePickerToggleQuickMenuButton"]').click({ + force: true, + }); + cy.wait(delay); //flyout won't open sometimes without + cy.get('button[data-test-subj="superDatePickerCommonlyUsed_This_year"]').click({ + force: true, + }); cy.get('button[data-test-subj="superDatePickerShowDatesButton"]') .contains('This year') .should('exist'); @@ -320,7 +330,7 @@ describe('Panels testing with Sample Data', () => { cy.get('select').select(PPL_VISUALIZATIONS_NAMES[0]); cy.get('button[aria-label="refreshPreview"]').trigger('mouseover').click(); cy.get('.plot-container').should('exist'); - cy.get('button[data-test-subj="addFlyoutButton"]').click({force: true}); + cy.get('button[data-test-subj="addFlyoutButton"]').click({ force: true }); cy.get('.euiToastHeader__title').contains('successfully').should('exist'); }); @@ -335,7 +345,7 @@ describe('Panels testing with Sample Data', () => { cy.get('select').select(PPL_VISUALIZATIONS_NAMES[1]); cy.get('button[aria-label="refreshPreview"]').trigger('mouseover').click(); cy.get('.plot-container').should('exist'); - cy.get('button[data-test-subj="addFlyoutButton"]').click({force: true}); + cy.get('button[data-test-subj="addFlyoutButton"]').click({ force: true }); cy.get('.euiToastHeader__title').contains('successfully').should('exist'); }); @@ -350,11 +360,15 @@ describe('Panels testing with Sample Data', () => { addVisualizationsToPanel(this.thePanel, [this.vis1.id]); moveToThePanel(this.thePanel.id); cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click(); }); - cy.get('.euiButtonEmpty[data-test-subj="superDatePickerToggleQuickMenuButton"]').click({force: true,}); + cy.get('.euiButtonEmpty[data-test-subj="superDatePickerToggleQuickMenuButton"]').click({ + force: true, + }); cy.get('[data-test-subj="superDatePickerQuickMenu"') .first() .within(() => { @@ -365,11 +379,11 @@ describe('Panels testing with Sample Data', () => { cy.get('[data-test-subj="searchAutocompleteTextArea"]') .trigger('mouseover') - .click({force: true}) - .wait(delay*5) + .click({ force: true }) + .wait(delay * 5) .focus() .type(PPL_FILTER); - cy.get('button[data-test-subj="superDatePickerApplyTimeButton"]').click({force: true}); + cy.get('button[data-test-subj="superDatePickerApplyTimeButton"]').click({ force: true }); cy.get('.euiButton__text').contains('Refresh').trigger('mouseover').click(); cy.get('.xtick').should('contain', 'Munich Airport'); cy.get('.xtick').contains('Zurich Airport').should('not.exist'); @@ -389,7 +403,9 @@ describe('Panels testing with Sample Data', () => { addVisualizationsToPanel(this.thePanel, [this.vis1.id]); moveToThePanel(this.thePanel.id); cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click(); }); @@ -419,7 +435,9 @@ describe('Panels testing with Sample Data', () => { addVisualizationsToPanel(this.thePanel, [this.vis1.id]); moveToThePanel(this.thePanel.id); cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click(); }); @@ -446,7 +464,9 @@ describe('Panels testing with Sample Data', () => { addVisualizationsToPanel(this.thePanel, [this.vis1.id]); moveToThePanel(this.thePanel.id); cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click(); }); @@ -470,7 +490,9 @@ describe('Panels testing with Sample Data', () => { addVisualizationsToPanel(this.thePanel, [this.vis1.id]); moveToThePanel(this.thePanel.id); cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click(); }); @@ -503,15 +525,17 @@ describe('Panels testing with Sample Data', () => { PPL_VISUALIZATIONS[2], PPL_VISUALIZATION_CONFIGS[2] ).as('vis2'); - + cy.then(function () { addVisualizationsToPanel(this.thePanel, [this.vis1.id]); moveToThePanel(this.thePanel.id); cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click({ force: true }); }); - + cy.get('button[aria-label="actionMenuButton"]').eq(0).click(); cy.get('button[data-test-subj="replaceVizContextMenuItem"]').click(); cy.get('select').select(PPL_VISUALIZATIONS_NAMES[2]); @@ -540,18 +564,20 @@ describe('Panels testing with Sample Data', () => { cy.get('[data-test-subj="eventExplorer__saveManagementPopover"]') .trigger('mouseover') .click(); - - cy.then(function () { - cy.get('[data-test-subj="eventExplorer__querySaveComboBox"]').type(this.thePanel.attributes.title); - cy.get(`input[value="${this.thePanel.attributes.title}"]`).trigger('mouseover').click(); - }); + + cy.then(function () { + cy.get('[data-test-subj="eventExplorer__querySaveComboBox"]').type( + this.thePanel.attributes.title + ); + cy.get(`input[value="${this.thePanel.attributes.title}"]`).trigger('mouseover').click(); + }); cy.get('[data-test-subj="eventExplorer__querySaveName"]') .focus() .type(PPL_VISUALIZATIONS_NAMES[2]); cy.get('[data-test-subj="eventExplorer__querySaveConfirm"]').trigger('mouseover').click(); cy.get('.euiToastHeader__title').contains('successfully').should('exist'); - + cy.then(function () { moveToThePanel(this.thePanel.id); }); @@ -572,7 +598,9 @@ describe('Panels testing with Sample Data', () => { addVisualizationsToPanel(this.thePanel, [this.vis1.id]); moveToThePanel(this.thePanel.id); cy.get('[data-test-subj="breadcrumb"]').click({ force: true }); - cy.get('input[data-test-subj="operationalPanelSearchBar"]').focus().type(this.thePanel.attributes.title); + cy.get('input[data-test-subj="operationalPanelSearchBar"]') + .focus() + .type(this.thePanel.attributes.title); cy.get('a.euiLink').contains(this.thePanel.attributes.title).click(); }); @@ -813,7 +841,7 @@ const createVisualization = (newName, query, vizConfig) => { description: '', type: 'bar', user_configs: vizConfig, - sub_type: 'visualization', + subType: 'visualization', }, }, }, 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/common/utils/__tests__/visualization_helpers.test.tsx b/common/utils/__tests__/visualization_helpers.test.tsx new file mode 100644 index 0000000000..6c7f9f8577 --- /dev/null +++ b/common/utils/__tests__/visualization_helpers.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getUserConfigFrom } from '../visualization_helpers'; + +describe('Utils helper functions', () => { + describe('getUserConfigFrom', () => { + it('should return empty object from empty input', () => { + expect(getUserConfigFrom(undefined)).toEqual({}); + expect(getUserConfigFrom('')).toEqual({}); + expect(getUserConfigFrom({})).toEqual({}); + }); + it('should get object from user_configs json', () => { + const container = { user_configs: '{ "key": "value" }' }; + expect(getUserConfigFrom(container)).toEqual({ key: 'value' }); + }); + it('should get object from userConfigs', () => { + const container = { userConfigs: '{ "key": "value" }' }; + expect(getUserConfigFrom(container)).toEqual({ key: 'value' }); + }); + }); +}); diff --git a/common/utils/visualization_helpers.ts b/common/utils/visualization_helpers.ts new file mode 100644 index 0000000000..8a93cd7af3 --- /dev/null +++ b/common/utils/visualization_helpers.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isEmpty, isString } from 'lodash'; + +/* The file contains helper functions for visualizaitons operations + * getUserConfigFrom - returns input objects' user_configs or userConfigs, JSON parsed if necessary + */ + +export const getUserConfigFrom = (container: unknown): object => { + const config = container?.user_configs || container?.userConfigs || {}; + + if (isEmpty(config)) return {}; + + if (isString(config)) return JSON.parse(config); + else return {}; +}; diff --git a/package.json b/package.json index 2e040938a8..3929fcd37d 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "cypress": "^12.8.1", "cypress-watch-and-reload": "^1.10.6", "eslint": "^6.8.0", - "husky": "6.0.0", + "husky": "^8.0.3", "jest-dom": "^4.0.0", "lint-staged": "^13.1.0", "ts-jest": "^29.1.0" @@ -82,4 +82,4 @@ "node_modules/*", "target/*" ] -} +} \ No newline at end of file diff --git a/public/components/application_analytics/helpers/utils.tsx b/public/components/application_analytics/helpers/utils.tsx index 08ba77f32e..67ac27fb7d 100644 --- a/public/components/application_analytics/helpers/utils.tsx +++ b/public/components/application_analytics/helpers/utils.tsx @@ -16,6 +16,7 @@ import { NEW_SELECTED_QUERY_TAB, TAB_CREATED_TYPE } from '../../../../common/con import { SPAN_REGEX } from '../../../../common/constants/shared'; import { VisualizationType } from '../../../../common/types/custom_panels'; import { IField } from '../../../../common/types/explorer'; +import { getUserConfigFrom } from '../../../../common/utils/visualization_helpers'; import { preprocessQuery } from '../../common/query_utils'; import { fetchVisualizationById } from '../../../components/custom_panels/helpers/utils'; import { @@ -218,7 +219,11 @@ 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) : {}; + + // resolved mismatched use of user_configs/userConfigs. This fall-back + // silently loads legacy configs. They will be stored in new userConfigs upon save. + const userConfigs = getUserConfigFrom(visData); + // 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/__tests__/query_utils.test.tsx b/public/components/common/query_utils/__tests__/query_utils.test.tsx index 1e185316ee..2db7db97d3 100644 --- a/public/components/common/query_utils/__tests__/query_utils.test.tsx +++ b/public/components/common/query_utils/__tests__/query_utils.test.tsx @@ -4,7 +4,12 @@ */ import React from 'react'; -import { parsePromQLIntoKeywords } from '../'; +import { + findMinInterval, + parsePromQLIntoKeywords, + preprocessMetricQuery, + updateCatalogVisualizationQuery, +} from '../'; describe('Query Utils', () => { describe('parsePromQLIntoKeywords', () => { @@ -20,7 +25,6 @@ describe('Query Utils', () => { }); test('should parse simple ppl query_range into keywords', () => { - console.log('should parse simple ppl query_range into keywords'); const query = "source = test_catalog.query_range('metric')"; const keywords = parsePromQLIntoKeywords(query); expect(keywords).toEqual({ @@ -32,7 +36,6 @@ describe('Query Utils', () => { }); test('should parse promql into keywords', () => { - console.log('should parse promql into keywords'); const query = "source = test_catalog.query_range('count by(one,two) (metric)')"; const keywords = parsePromQLIntoKeywords(query); expect(keywords).toEqual({ @@ -43,4 +46,56 @@ describe('Query Utils', () => { }); }); }); + describe('findMinInterval by moment strings', () => { + it.each([ + ['now-3y', 'y'], + ['now-23M', 'w'], + ['now-4M', 'w'], + ['now-3w', 'd'], + ['now-40h', 'h'], // less than 2 days + ['now-119m', 'm'], + ['now-59s', 's'], + ['now-900ms', 'ms'], + ])("when input is '{0}' expect span '{3}'", (start, span) => { + const minInterval = findMinInterval(start, 'now'); + expect(minInterval).toEqual(span); + }); + }); + describe('Metric Query processors', () => { + const defaultQueryMetaData = { + catalogSourceName: 'my_catalog', + catalogTableName: 'metricName', + aggregation: 'avg', + attributesGroupBy: [], + start: 'now-1m', + end: 'now', + span: '1', + resolution: 'h', + }; + describe('updateCatalogVisualizationQuery', () => { + it('should build plain promQL series query', () => { + const query = updateCatalogVisualizationQuery(defaultQueryMetaData); + expect(query).toMatch(/avg \(metricName\)/); + }); + it('should build promQL with attributes grouping', () => { + const query = updateCatalogVisualizationQuery({ + ...defaultQueryMetaData, + attributesGroupBy: ['label1', 'label2'], + }); + expect(query).toMatch(/avg by\(label1,label2\) \(metricName\)/); + }); + }); + describe('preprocessMetricQuery', () => { + it('should set timestamps and default resolution', () => { + const [startDate, endDate] = ['2023-11-11', '2023-12-11']; + const [start, end] = [1699660800, 1702252800]; // 2023-11-11 to 2023-12-11 + const query = preprocessMetricQuery({ + metaData: { queryMetaData: defaultQueryMetaData }, + startTime: startDate, + endTime: endDate, + }); + expect(query).toMatch(new RegExp(`, ${start}, ${end}, '1d'`)); + }); + }); + }); }); diff --git a/public/components/common/query_utils/index.ts b/public/components/common/query_utils/index.ts index 9f4024bb31..489afad130 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, @@ -35,6 +35,28 @@ 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 2 year + else if (diffSeconds <= 86400 * 366 * 2) minInterval = 'w'; + + return minInterval; +}; + export const convertDateTime = ( datetime: string, isStart = true, @@ -56,6 +78,37 @@ export const convertDateTime = ( return returnTime; }; +export const updateCatalogVisualizationQuery = ({ + catalogSourceName, + catalogTableName, + aggregation, + attributesGroupBy, + start, + end, + span = '1', + resolution = 'h', +}: { + catalogSourceName: string; + catalogTableName: string; + aggregation: string; + attributesGroupBy: string[]; + start: string; + end: string; + span: string; + resolution: string; +}) => { + 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; +}; + const PROMQL_DEFAULT_AGGREGATION = 'avg'; const PROMQL_CATALOG_ONLY_INDEX = /^(?[^|\s]+)\.(?\S+)$/i; const PROMQL_INDEX_REGEX = /(?[^|\s]+)\.query_range\('(?.+?)'/i; @@ -128,13 +181,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 +203,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/__tests__/__snapshots__/search.test.tsx.snap b/public/components/common/search/__tests__/__snapshots__/search.test.tsx.snap new file mode 100644 index 0000000000..c227af5246 --- /dev/null +++ b/public/components/common/search/__tests__/__snapshots__/search.test.tsx.snap @@ -0,0 +1,910 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Explorer Search component renders basic component 1`] = ` + + +
+ +
+ +
+ + PPL + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="smallContextMenuExample" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + + + +
+
+
+
+
+ +
+ +
+ + +