From 9860e25a88833dc1e8d42c6464651e822213b43f Mon Sep 17 00:00:00 2001 From: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> Date: Fri, 8 Jul 2022 16:19:21 +0100 Subject: [PATCH 01/21] [Security Solution][Detections] fixes rules management table selection after auto refresh (#135533) ## Summary - fixes https://github.com/elastic/kibana/issues/135297 - pauses rules table auto-refresh if any rule selected - removes pausing auto-refresh when performing bulk actions, as it not needed anymore - clear selection of all rules when bulk duplicate performed, as newly created rules can change existing selection ### Before https://user-images.githubusercontent.com/92328789/177141742-af2e7df5-9522-49af-ae20-563173632196.mov ### After https://user-images.githubusercontent.com/92328789/177143675-a6515dc7-a2b4-466f-80e0-7912f2f9f417.mov ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Release note fixes losing selection of items on rules management table during auto-refresh --- .../detection_rules/prebuilt_rules.spec.ts | 1 - .../rules_table_auto_refresh.spec.ts | 126 ++++++++++++++++++ .../detection_rules/sorting.spec.ts | 21 --- .../cypress/screens/alerts_detection_rules.ts | 8 ++ .../cypress/tasks/alerts_detection_rules.ts | 35 +++++ .../utility_bar/utility_bar_section.tsx | 11 +- .../all/bulk_actions/use_bulk_actions.tsx | 23 +--- .../__mocks__/rules_table_context.tsx | 1 + .../all/rules_table/rules_table_context.tsx | 38 +++++- .../rules/all/utility_bar.test.tsx | 59 +++++--- .../rules/all/utility_bar.tsx | 22 ++- 11 files changed, 278 insertions(+), 67 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_table_auto_refresh.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index 389b601f415e2..397162f69d490 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -92,7 +92,6 @@ describe('Prebuilt rules', () => { waitForRuleToChangeStatus(); cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); - selectAllRules(); disableSelectedRules(); waitForRuleToChangeStatus(); cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'false'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_table_auto_refresh.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_table_auto_refresh.spec.ts new file mode 100644 index 0000000000000..ca5ce2b2bfebd --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/rules_table_auto_refresh.spec.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + RULE_CHECKBOX, + REFRESH_RULES_STATUS, + REFRESH_SETTINGS_SWITCH, + REFRESH_SETTINGS_SELECTION_NOTE, +} from '../../screens/alerts_detection_rules'; +import { + changeRowsPerPageTo, + checkAutoRefresh, + waitForRulesTableToBeLoaded, + selectAllRules, + openRefreshSettingsPopover, + clearAllRuleSelection, + selectNumberOfRules, + mockGlobalClock, + disableAutoRefresh, + checkAutoRefreshIsDisabled, + checkAutoRefreshIsEnabled, +} from '../../tasks/alerts_detection_rules'; +import { login, visit } from '../../tasks/login'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; +import { createCustomRule } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { getNewRule } from '../../objects/rule'; + +const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; + +describe('Alerts detection rules table auto-refresh', () => { + before(() => { + cleanKibana(); + login(); + for (let i = 1; i < 7; i += 1) { + createCustomRule({ ...getNewRule(), name: `Test rule ${i}` }, `${i}`); + } + }); + + it('Auto refreshes rules', () => { + visit(DETECTIONS_RULE_MANAGEMENT_URL); + + mockGlobalClock(); + waitForRulesTableToBeLoaded(); + + // ensure rules have rendered. As there is no user interaction in this test, + // rules were not rendered before test completes + cy.get(RULE_CHECKBOX).should('have.length', 6); + + // mock 1 minute passing to make sure refresh is conducted + checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'be.visible'); + + cy.contains(REFRESH_RULES_STATUS, 'Updated now'); + }); + + it('should prevent table from rules refetch if any rule selected', () => { + visit(DETECTIONS_RULE_MANAGEMENT_URL); + + mockGlobalClock(); + waitForRulesTableToBeLoaded(); + + selectNumberOfRules(1); + + // mock 1 minute passing to make sure refresh is not conducted + checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'not.exist'); + + // ensure rule is still selected + cy.get(RULE_CHECKBOX).first().should('be.checked'); + + cy.contains(REFRESH_RULES_STATUS, 'Updated 1 minute ago'); + }); + + it('should disable auto refresh when any rule selected and enable it after rules unselected', () => { + visit(DETECTIONS_RULE_MANAGEMENT_URL); + waitForRulesTableToBeLoaded(); + changeRowsPerPageTo(5); + + // check refresh settings if it's enabled before selecting + openRefreshSettingsPopover(); + checkAutoRefreshIsEnabled(); + + selectAllRules(); + + // auto refresh should be disabled after rules selected + openRefreshSettingsPopover(); + checkAutoRefreshIsDisabled(); + + // if any rule selected, refresh switch should be disabled and help note to users should displayed + cy.get(REFRESH_SETTINGS_SWITCH).should('be.disabled'); + cy.contains( + REFRESH_SETTINGS_SELECTION_NOTE, + 'Note: Refresh is disabled while there is an active selection.' + ); + + clearAllRuleSelection(); + + // after all rules unselected, auto refresh should renew + openRefreshSettingsPopover(); + checkAutoRefreshIsEnabled(); + }); + + it('should not enable auto refresh after rules were unselected if auto refresh was disabled', () => { + visit(DETECTIONS_RULE_MANAGEMENT_URL); + waitForRulesTableToBeLoaded(); + changeRowsPerPageTo(5); + + openRefreshSettingsPopover(); + disableAutoRefresh(); + + selectAllRules(); + + openRefreshSettingsPopover(); + checkAutoRefreshIsDisabled(); + + clearAllRuleSelection(); + + // after all rules unselected, auto refresh should still be disabled + openRefreshSettingsPopover(); + checkAutoRefreshIsDisabled(); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index c91af7e44cccb..c5cacc511c600 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -18,7 +18,6 @@ import { import { enableRule, changeRowsPerPageTo, - checkAutoRefresh, goToPage, sortByEnabledRules, waitForRulesTableToBeLoaded, @@ -36,8 +35,6 @@ import { getNewThresholdRule, } from '../../objects/rule'; -const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; - describe('Alerts detection rules', () => { before(() => { cleanKibana(); @@ -100,22 +97,4 @@ describe('Alerts detection rules', () => { .find(SECOND_PAGE_SELECTOR) .should('have.class', 'euiPaginationButton-isActive'); }); - - it('Auto refreshes rules', () => { - /** - * Ran into the error: timer created with setInterval() but cleared with cancelAnimationFrame() - * There are no cancelAnimationFrames in the codebase that are used to clear a setInterval so - * explicitly set the below overrides. see https://docs.cypress.io/api/commands/clock#Function-names - */ - - visit(DETECTIONS_RULE_MANAGEMENT_URL); - - cy.clock(Date.now(), ['setInterval', 'clearInterval', 'Date']); - - waitForRulesTableToBeLoaded(); - - // mock 1 minute passing to make sure refresh - // is conducted - checkAutoRefresh(DEFAULT_RULE_REFRESH_INTERVAL_VALUE, 'be.visible'); - }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 2e186d95f176c..096e64cae45d2 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -130,3 +130,11 @@ export const RULES_TAGS_POPOVER_WRAPPER = '[data-test-subj="tagsDisplayPopoverWr export const RULES_TAGS_FILTER_BTN = '[data-test-subj="tags-filter-popover-button"]'; export const SELECTED_RULES_NUMBER_LABEL = '[data-test-subj="selectedRules"]'; + +export const REFRESH_SETTINGS_POPOVER = '[data-test-subj="refreshSettings-popover"]'; + +export const REFRESH_SETTINGS_SWITCH = '[data-test-subj="refreshSettingsSwitch"]'; + +export const REFRESH_SETTINGS_SELECTION_NOTE = '[data-test-subj="refreshSettingsSelectionNote"]'; + +export const REFRESH_RULES_STATUS = '[data-test-subj="refreshRulesStatus"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 80d5bf54ca6c4..75c44f5e91e54 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -48,6 +48,8 @@ import { RULES_TAGS_POPOVER_WRAPPER, INTEGRATIONS_POPOVER, SELECTED_RULES_NUMBER_LABEL, + REFRESH_SETTINGS_POPOVER, + REFRESH_SETTINGS_SWITCH, } from '../screens/alerts_detection_rules'; import { ALL_ACTIONS } from '../screens/rule_details'; import { LOADING_INDICATOR } from '../screens/security_header'; @@ -195,6 +197,11 @@ export const selectAllRules = () => { cy.get(SELECT_ALL_RULES_BTN).contains('Clear'); }; +export const clearAllRuleSelection = () => { + cy.get(SELECT_ALL_RULES_BTN).contains('Clear').click(); + cy.get(SELECT_ALL_RULES_BTN).contains('Select all'); +}; + export const confirmRulesDelete = () => { cy.get(RULES_DELETE_CONFIRMATION_MODAL).should('be.visible'); cy.get(MODAL_CONFIRMATION_BTN).click(); @@ -297,3 +304,31 @@ export const testAllTagsBadges = (tags: string[]) => { export const testMultipleSelectedRulesLabel = (rulesCount: number) => { cy.get(SELECTED_RULES_NUMBER_LABEL).should('have.text', `Selected ${rulesCount} rules`); }; + +export const openRefreshSettingsPopover = () => { + cy.get(REFRESH_SETTINGS_POPOVER).click(); + cy.get(REFRESH_SETTINGS_SWITCH).should('be.visible'); +}; + +export const checkAutoRefreshIsDisabled = () => { + cy.get(REFRESH_SETTINGS_SWITCH).should('have.attr', 'aria-checked', 'false'); +}; + +export const checkAutoRefreshIsEnabled = () => { + cy.get(REFRESH_SETTINGS_SWITCH).should('have.attr', 'aria-checked', 'true'); +}; + +export const disableAutoRefresh = () => { + cy.get(REFRESH_SETTINGS_SWITCH).click(); + checkAutoRefreshIsDisabled(); +}; + +export const mockGlobalClock = () => { + /** + * Ran into the error: timer created with setInterval() but cleared with cancelAnimationFrame() + * There are no cancelAnimationFrames in the codebase that are used to clear a setInterval so + * explicitly set the below overrides. see https://docs.cypress.io/api/commands/clock#Function-names + */ + + cy.clock(Date.now(), ['setInterval', 'clearInterval', 'Date']); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx index c84219cc63488..dc966516c8373 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar_section.tsx @@ -11,10 +11,15 @@ import { BarSection, BarSectionProps } from './styles'; export interface UtilityBarSectionProps extends BarSectionProps { children: React.ReactNode; + dataTestSubj?: string; } -export const UtilityBarSection = React.memo<UtilityBarSectionProps>(({ grow, children }) => ( - <BarSection grow={grow}>{children}</BarSection> -)); +export const UtilityBarSection = React.memo<UtilityBarSectionProps>( + ({ grow, children, dataTestSubj }) => ( + <BarSection grow={grow} data-test-subj={dataTestSubj}> + {children} + </BarSection> + ) +); UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx index 1c31961690430..a9da49b6a9316 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/bulk_actions/use_bulk_actions.tsx @@ -88,8 +88,8 @@ export const useBulkActions = ({ ); const { - state: { isAllSelected, rules, loadingRuleIds, selectedRuleIds, isRefreshOn }, - actions: { setLoadingRules, setIsRefreshOn }, + state: { isAllSelected, rules, loadingRuleIds, selectedRuleIds }, + actions: { setLoadingRules, clearRulesSelection }, } = rulesTableContext; return useCallback( @@ -107,7 +107,6 @@ export const useBulkActions = ({ const handleEnableAction = async () => { startTransaction({ name: BULK_RULE_ACTIONS.ENABLE }); - setIsRefreshOn(false); closePopover(); const disabledRules = selectedRules.filter(({ enabled }) => !enabled); @@ -130,12 +129,10 @@ export const useBulkActions = ({ search: isAllSelected ? { query: filterQuery } : { ids: ruleIds }, }); updateRulesCache(res?.attributes?.results?.updated ?? []); - setIsRefreshOn(isRefreshOn); }; const handleDisableActions = async () => { startTransaction({ name: BULK_RULE_ACTIONS.DISABLE }); - setIsRefreshOn(false); closePopover(); const enabledIds = selectedRules.filter(({ enabled }) => enabled).map(({ id }) => id); @@ -148,12 +145,10 @@ export const useBulkActions = ({ search: isAllSelected ? { query: filterQuery } : { ids: enabledIds }, }); updateRulesCache(res?.attributes?.results?.updated ?? []); - setIsRefreshOn(isRefreshOn); }; const handleDuplicateAction = async () => { startTransaction({ name: BULK_RULE_ACTIONS.DUPLICATE }); - setIsRefreshOn(false); closePopover(); await executeRulesBulkAction({ @@ -164,17 +159,15 @@ export const useBulkActions = ({ search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }, }); invalidateRules(); - setIsRefreshOn(isRefreshOn); + clearRulesSelection(); }; const handleDeleteAction = async () => { - setIsRefreshOn(false); closePopover(); if (isAllSelected) { // User has cancelled deletion if ((await confirmDeletion()) === false) { - setIsRefreshOn(isRefreshOn); return; } } @@ -188,11 +181,9 @@ export const useBulkActions = ({ search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }, }); invalidateRules(); - setIsRefreshOn(isRefreshOn); }; const handleExportAction = async () => { - setIsRefreshOn(false); closePopover(); startTransaction({ name: BULK_RULE_ACTIONS.EXPORT }); @@ -203,7 +194,6 @@ export const useBulkActions = ({ toasts, search: isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }, }); - setIsRefreshOn(isRefreshOn); }; const handleBulkEdit = (bulkEditActionType: BulkActionEditType) => async () => { @@ -211,7 +201,6 @@ export const useBulkActions = ({ let isBulkEditFinished = false; // disabling auto-refresh so user's selected rules won't disappear after table refresh - setIsRefreshOn(false); closePopover(); const customSelectedRuleIds = selectedRules @@ -220,13 +209,11 @@ export const useBulkActions = ({ // User has cancelled edit action or there are no custom rules to proceed if ((await confirmBulkEdit()) === false) { - setIsRefreshOn(isRefreshOn); return; } const editPayload = await completeBulkEditForm(bulkEditActionType); if (editPayload == null) { - setIsRefreshOn(isRefreshOn); return; } @@ -288,7 +275,6 @@ export const useBulkActions = ({ isBulkEditFinished = true; updateRulesCache(res?.attributes?.results?.updated ?? []); - setIsRefreshOn(isRefreshOn); if (getIsMounted()) { await resolveTagsRefetch(bulkEditActionType); } @@ -453,7 +439,6 @@ export const useBulkActions = ({ filterQuery, invalidateRules, confirmDeletion, - setIsRefreshOn, confirmBulkEdit, completeBulkEditForm, queryClient, @@ -461,7 +446,7 @@ export const useBulkActions = ({ getIsMounted, resolveTagsRefetch, updateRulesCache, - isRefreshOn, + clearRulesSelection, ] ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx index a6d3aeb35d2a8..86fcdcfe8b3f7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/__mocks__/rules_table_context.tsx @@ -51,6 +51,7 @@ export const useRulesTableContextMock = { setPerPage: jest.fn(), setSelectedRuleIds: jest.fn(), setSortingOptions: jest.fn(), + clearRulesSelection: jest.fn(), }, }), }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx index 9647d2fd528b1..243e70683875f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table/rules_table_context.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + useRef, +} from 'react'; import { DEFAULT_RULES_TABLE_REFRESH_SETTING } from '../../../../../../../common/constants'; import { invariant } from '../../../../../../../common/utils/invariant'; import { useKibana, useUiSetting$ } from '../../../../../../common/lib/kibana'; @@ -131,6 +139,10 @@ export interface RulesTableActions { setPerPage: React.Dispatch<React.SetStateAction<number>>; setSelectedRuleIds: React.Dispatch<React.SetStateAction<string[]>>; setSortingOptions: React.Dispatch<React.SetStateAction<SortingOptions>>; + /** + * clears rules selection on a page + */ + clearRulesSelection: () => void; } export interface RulesTableContextType { @@ -171,6 +183,7 @@ export const RulesTableContextProvider = ({ const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(DEFAULT_RULES_PER_PAGE); const [selectedRuleIds, setSelectedRuleIds] = useState<string[]>([]); + const autoRefreshBeforePause = useRef<boolean | null>(null); const toggleInMemorySorting = useCallback( (value: boolean) => { @@ -201,6 +214,26 @@ export const RulesTableContextProvider = ({ setIsAllSelected(false); }, []); + const clearRulesSelection = useCallback(() => { + setSelectedRuleIds([]); + setIsAllSelected(false); + }, []); + + useEffect(() => { + // pause table auto refresh when any of rule selected + // store current auto refresh value, to use it later, when all rules selection will be cleared + if (selectedRuleIds.length > 0) { + setIsRefreshOn(false); + if (autoRefreshBeforePause.current == null) { + autoRefreshBeforePause.current = isRefreshOn; + } + } else { + // if no rules selected, update auto refresh value, with previously stored value + setIsRefreshOn(autoRefreshBeforePause.current ?? isRefreshOn); + autoRefreshBeforePause.current = null; + } + }, [selectedRuleIds, isRefreshOn]); + // Fetch rules const { data: { rules, total } = { rules: [], total: 0 }, @@ -265,6 +298,7 @@ export const RulesTableContextProvider = ({ setPerPage, setSelectedRuleIds, setSortingOptions, + clearRulesSelection, }, }), [ @@ -289,7 +323,9 @@ export const RulesTableContextProvider = ({ selectedRuleIds, sortingOptions, toggleInMemorySorting, + setSelectedRuleIds, total, + clearRulesSelection, ] ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx index dbe26402e677a..91f4a0b06e71d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.test.tsx @@ -7,20 +7,15 @@ import React from 'react'; import { mount } from 'enzyme'; -import { ThemeProvider } from 'styled-components'; import { waitFor } from '@testing-library/react'; import { AllRulesUtilityBar } from './utility_bar'; -import { getMockTheme } from '../../../../../common/lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { euiBreakpoints: { l: '1200px' }, euiSizeM: '10px' }, -}); +import { TestProviders } from '../../../../../common/mock'; describe('AllRules', () => { it('renders AllRulesUtilityBar total rules and selected rules', () => { const wrapper = mount( - <ThemeProvider theme={mockTheme}> + <TestProviders> <AllRulesUtilityBar canBulkEdit onRefresh={jest.fn()} @@ -31,7 +26,7 @@ describe('AllRules', () => { onRefreshSwitch={jest.fn()} hasBulkActions /> - </ThemeProvider> + </TestProviders> ); expect(wrapper.find('[data-test-subj="showingRules"]').at(0).text()).toEqual('Showing 4 rules'); @@ -42,7 +37,7 @@ describe('AllRules', () => { it('does not render total selected and bulk actions when "hasBulkActions" is false', () => { const wrapper = mount( - <ThemeProvider theme={mockTheme}> + <TestProviders> <AllRulesUtilityBar canBulkEdit onRefresh={jest.fn()} @@ -53,7 +48,7 @@ describe('AllRules', () => { onRefreshSwitch={jest.fn()} hasBulkActions={false} /> - </ThemeProvider> + </TestProviders> ); expect(wrapper.find('[data-test-subj="showingRules"]').exists()).toBeFalsy(); @@ -65,7 +60,7 @@ describe('AllRules', () => { it('renders utility actions if user has permissions', () => { const wrapper = mount( - <ThemeProvider theme={mockTheme}> + <TestProviders> <AllRulesUtilityBar canBulkEdit onRefresh={jest.fn()} @@ -76,7 +71,7 @@ describe('AllRules', () => { onRefreshSwitch={jest.fn()} hasBulkActions /> - </ThemeProvider> + </TestProviders> ); expect(wrapper.find('[data-test-subj="bulkActions"]').exists()).toBeTruthy(); @@ -84,7 +79,7 @@ describe('AllRules', () => { it('renders no utility actions if user has no permissions', () => { const wrapper = mount( - <ThemeProvider theme={mockTheme}> + <TestProviders> <AllRulesUtilityBar canBulkEdit={false} onRefresh={jest.fn()} @@ -95,7 +90,7 @@ describe('AllRules', () => { onRefreshSwitch={jest.fn()} hasBulkActions /> - </ThemeProvider> + </TestProviders> ); expect(wrapper.find('[data-test-subj="bulkActions"]').exists()).toBeFalsy(); @@ -104,7 +99,7 @@ describe('AllRules', () => { it('invokes refresh on refresh action click', () => { const mockRefresh = jest.fn(); const wrapper = mount( - <ThemeProvider theme={mockTheme}> + <TestProviders> <AllRulesUtilityBar canBulkEdit onRefresh={mockRefresh} @@ -115,7 +110,7 @@ describe('AllRules', () => { onRefreshSwitch={jest.fn()} hasBulkActions /> - </ThemeProvider> + </TestProviders> ); wrapper.find('[data-test-subj="refreshRulesAction"] button').at(0).simulate('click'); @@ -123,21 +118,21 @@ describe('AllRules', () => { expect(mockRefresh).toHaveBeenCalled(); }); - it('invokes onRefreshSwitch when auto refresh switch is clicked', async () => { + it('invokes onRefreshSwitch when auto refresh switch is clicked if there are not selected items', async () => { const mockSwitch = jest.fn(); const wrapper = mount( - <ThemeProvider theme={mockTheme}> + <TestProviders> <AllRulesUtilityBar canBulkEdit onRefresh={jest.fn()} paginationTotal={4} - numberSelectedItems={1} + numberSelectedItems={0} onGetBulkItemsPopoverContent={jest.fn()} isAutoRefreshOn={true} onRefreshSwitch={mockSwitch} hasBulkActions /> - </ThemeProvider> + </TestProviders> ); await waitFor(() => { @@ -146,4 +141,28 @@ describe('AllRules', () => { expect(mockSwitch).toHaveBeenCalledTimes(1); }); }); + + it('does not invokes onRefreshSwitch when auto refresh switch is clicked if there are selected items', async () => { + const mockSwitch = jest.fn(); + const wrapper = mount( + <TestProviders> + <AllRulesUtilityBar + canBulkEdit + onRefresh={jest.fn()} + paginationTotal={4} + numberSelectedItems={1} + onGetBulkItemsPopoverContent={jest.fn()} + isAutoRefreshOn={true} + onRefreshSwitch={mockSwitch} + hasBulkActions + /> + </TestProviders> + ); + + await waitFor(() => { + wrapper.find('[data-test-subj="refreshSettings"] button').first().simulate('click'); + wrapper.find('[data-test-subj="refreshSettingsSwitch"] button').first().simulate('click'); + expect(mockSwitch).not.toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx index 5513f70c42297..4e2f18ea7832d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/utility_bar.tsx @@ -11,8 +11,11 @@ import { EuiSwitch, EuiSwitchEvent, EuiContextMenuPanelDescriptor, + EuiTextColor, + EuiSpacer, } from '@elastic/eui'; import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { UtilityBar, @@ -59,6 +62,7 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>( }) => { const { timelines } = useKibana().services; const rulesTableContext = useRulesTableContextOptional(); + const isAnyRuleSelected = numberSelectedItems > 0; const handleGetBulkItemsPopoverContent = useCallback( (closePopover: () => void): JSX.Element | null => { @@ -96,12 +100,26 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>( checked={isAutoRefreshOn ?? false} onChange={handleAutoRefreshSwitch(closePopover)} compressed + disabled={isAnyRuleSelected} data-test-subj="refreshSettingsSwitch" />, + ...(isAnyRuleSelected + ? [ + <div key="refreshSettingsSelectionNote"> + <EuiSpacer size="s" /> + <EuiTextColor color="subdued" data-test-subj="refreshSettingsSelectionNote"> + <FormattedMessage + id="xpack.securitySolution.detectionEngine.rules.refreshRulePopoverSelectionHelpText" + defaultMessage="Note: Refresh is disabled while there is an active selection." + /> + </EuiTextColor> + </div>, + ] + : []), ]} /> ), - [isAutoRefreshOn, handleAutoRefreshSwitch] + [isAutoRefreshOn, handleAutoRefreshSwitch, isAnyRuleSelected] ); return ( @@ -186,7 +204,7 @@ export const AllRulesUtilityBar = React.memo<AllRulesUtilityBarProps>( )} </UtilityBarSection> {rulesTableContext && ( - <UtilityBarSection> + <UtilityBarSection dataTestSubj="refreshRulesStatus"> {timelines.getLastUpdated({ showUpdating: rulesTableContext.state.isFetching, updatedAt: rulesTableContext.state.lastUpdated, From fc2c3ec10f549c3fcd96250429b2da934f909f46 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov <kuznetsov.yaroslav.yk@gmail.com> Date: Fri, 8 Jul 2022 18:26:55 +0300 Subject: [PATCH 02/21] [Data] Fixed Date histogram bounds calculation doesn't update "now" (#135899) * Fixed the problem with date histogram bounds calculation. * Update src/plugins/data/server/search/aggs/aggs_service.ts Co-authored-by: Anton Dosov <dosantappdev@gmail.com> Co-authored-by: Anton Dosov <dosantappdev@gmail.com> --- src/plugins/data/common/index.ts | 1 + .../data/common/search/aggs/aggs_service.ts | 9 ++++----- src/plugins/data/public/plugin.ts | 1 - .../public/search/aggs/aggs_service.test.ts | 2 +- .../data/public/search/aggs/aggs_service.ts | 20 +++++++++++++------ .../data/public/search/search_service.ts | 6 +++--- .../data/server/search/aggs/aggs_service.ts | 15 ++++++++++++-- 7 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 478d6d23d7b96..1a929df039ec0 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -28,6 +28,7 @@ export * from './search'; export type { RefreshInterval, TimeRangeBounds, + TimeRange, GetConfigFn, SavedQuery, SavedQueryAttributes, diff --git a/src/plugins/data/common/search/aggs/aggs_service.ts b/src/plugins/data/common/search/aggs/aggs_service.ts index f88138d04f31a..a367baf5a5372 100644 --- a/src/plugins/data/common/search/aggs/aggs_service.ts +++ b/src/plugins/data/common/search/aggs/aggs_service.ts @@ -9,8 +9,7 @@ import { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common'; -import { CalculateBoundsOptions } from '../../query'; -import { UI_SETTINGS, AggTypesDependencies, calculateBounds } from '../..'; +import { UI_SETTINGS, AggTypesDependencies } from '../..'; import { GetConfigFn } from '../../types'; import { AggConfigs, @@ -42,7 +41,7 @@ export interface AggsCommonStartDependencies { getIndexPattern(id: string): Promise<DataView>; getConfig: GetConfigFn; fieldFormats: FieldFormatsStartCommon; - calculateBoundsOptions: CalculateBoundsOptions; + calculateBounds: AggTypesDependencies['calculateBounds']; } /** @@ -75,13 +74,13 @@ export class AggsCommonService { public start({ getConfig, fieldFormats, - calculateBoundsOptions, + calculateBounds, }: AggsCommonStartDependencies): AggsCommonStart { const aggTypesStart = this.aggTypesRegistry.start({ getConfig, getFieldFormatsStart: () => fieldFormats, aggExecutionContext: this.aggExecutionContext, - calculateBounds: (timeRange) => calculateBounds(timeRange, calculateBoundsOptions), + calculateBounds, }); return { diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index d0a603b52b921..22ebf1c9d8036 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -137,7 +137,6 @@ export class DataPublicPlugin fieldFormats, indexPatterns: dataViews, screenshotMode, - nowProvider: this.nowProvider, }); setSearchService(search); diff --git a/src/plugins/data/public/search/aggs/aggs_service.test.ts b/src/plugins/data/public/search/aggs/aggs_service.test.ts index c391c023b2d27..40cc3590a32e2 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.test.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.test.ts @@ -34,11 +34,11 @@ describe('AggsService - public', () => { setupDeps = { registerFunction: expressionsPluginMock.createSetupContract().registerFunction, uiSettings, + nowProvider: createNowProviderMock(), }; startDeps = { fieldFormats: fieldFormatsServiceMock.createStartContract(), indexPatterns: dataPluginMock.createStartContract().indexPatterns, - nowProvider: createNowProviderMock(), }; }); diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index 640bb954561a1..5a830b6cc154c 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -17,6 +17,7 @@ import { AggsCommonStartDependencies, AggsCommonService, } from '../../../common/search/aggs'; +import { calculateBounds, TimeRange } from '../../../common'; import type { AggsSetup, AggsStart } from './types'; import type { NowProviderInternalContract } from '../../now_provider'; @@ -49,13 +50,13 @@ export function createGetConfig( export interface AggsSetupDependencies { uiSettings: IUiSettingsClient; registerFunction: ExpressionsServiceSetup['registerFunction']; + nowProvider: NowProviderInternalContract; } /** @internal */ export interface AggsStartDependencies { fieldFormats: FieldFormatsStart; indexPatterns: DataViewsContract; - nowProvider: NowProviderInternalContract; } /** @@ -69,8 +70,17 @@ export class AggsService { }); private getConfig?: AggsCommonStartDependencies['getConfig']; private subscriptions: Subscription[] = []; + private nowProvider!: NowProviderInternalContract; + + /** + * NowGetter uses window.location, so we must have a separate implementation + * of calculateBounds on the client and the server. + */ + private calculateBounds = (timeRange: TimeRange) => + calculateBounds(timeRange, { forceNow: this.nowProvider.get() }); - public setup({ registerFunction, uiSettings }: AggsSetupDependencies): AggsSetup { + public setup({ registerFunction, uiSettings, nowProvider }: AggsSetupDependencies): AggsSetup { + this.nowProvider = nowProvider; this.getConfig = createGetConfig(uiSettings, aggsRequiredUiSettings, this.subscriptions); return this.aggsCommonService.setup({ @@ -78,14 +88,12 @@ export class AggsService { }); } - public start({ indexPatterns, fieldFormats, nowProvider }: AggsStartDependencies): AggsStart { + public start({ indexPatterns, fieldFormats }: AggsStartDependencies): AggsStart { const { calculateAutoTimeExpression, types, createAggConfigs } = this.aggsCommonService.start({ getConfig: this.getConfig!, getIndexPattern: indexPatterns.get, + calculateBounds: this.calculateBounds, fieldFormats, - calculateBoundsOptions: { - forceNow: nowProvider.get(), - }, }); return { diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index a945e3d8a5479..eabc101d7a237 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -89,7 +89,6 @@ export interface SearchServiceStartDependencies { fieldFormats: FieldFormatsStart; indexPatterns: DataViewsContract; screenshotMode: ScreenshotModePluginStart; - nowProvider: NowProviderInternalContract; } export class SearchService implements Plugin<ISearchSetup, ISearchStart> { @@ -191,6 +190,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { const aggs = this.aggsService.setup({ uiSettings, registerFunction: expressions.registerFunction, + nowProvider, }); if (this.initializerContext.config.get().search.aggs.shardDelay.enabled) { @@ -223,7 +223,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { public start( { http, theme, uiSettings, chrome, application }: CoreStart, - { fieldFormats, indexPatterns, screenshotMode, nowProvider }: SearchServiceStartDependencies + { fieldFormats, indexPatterns, screenshotMode }: SearchServiceStartDependencies ): ISearchStart { const search = ((request, options = {}) => { return this.searchInterceptor.search(request, options); @@ -232,7 +232,7 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> { const loadingCount$ = new BehaviorSubject(0); http.addLoadingCountSource(loadingCount$); - const aggs = this.aggsService.start({ fieldFormats, indexPatterns, nowProvider }); + const aggs = this.aggsService.start({ fieldFormats, indexPatterns }); const searchSourceDependencies: SearchSourceDependencies = { aggs, diff --git a/src/plugins/data/server/search/aggs/aggs_service.ts b/src/plugins/data/server/search/aggs/aggs_service.ts index 57f9ccebee23c..ab4fdbb57aa4d 100644 --- a/src/plugins/data/server/search/aggs/aggs_service.ts +++ b/src/plugins/data/server/search/aggs/aggs_service.ts @@ -17,7 +17,12 @@ import type { import { ExpressionsServiceSetup } from '@kbn/expressions-plugin/common'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; -import { AggsCommonService, aggsRequiredUiSettings } from '../../../common'; +import { + calculateBounds, + AggsCommonService, + aggsRequiredUiSettings, + TimeRange, +} from '../../../common'; import { AggsSetup, AggsStart } from './types'; /** @internal */ @@ -48,6 +53,12 @@ async function getConfigFn(uiSettingsClient: IUiSettingsClient) { export class AggsService { private readonly aggsCommonService = new AggsCommonService({ shouldDetectTimeZone: false }); + /** + * getForceNow uses window.location on the client, so we must have a + * separate implementation of calculateBounds on the server. + */ + private calculateBounds = (timeRange: TimeRange) => calculateBounds(timeRange); + public setup({ registerFunction }: AggsSetupDependencies): AggsSetup { return this.aggsCommonService.setup({ registerFunction, @@ -65,10 +76,10 @@ export class AggsService { this.aggsCommonService.start({ getConfig: await getConfigFn(uiSettingsClient), fieldFormats: await fieldFormats.fieldFormatServiceFactory(uiSettingsClient), - calculateBoundsOptions: {}, getIndexPattern: ( await indexPatterns.dataViewsServiceFactory(savedObjectsClient, elasticsearchClient) ).get, + calculateBounds: this.calculateBounds, }); return { From eb6e6477b2a6d99048d92917136a1918eae87ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Fri, 8 Jul 2022 17:43:10 +0200 Subject: [PATCH 03/21] Added a gif module to be able to import gif files (#136015) --- packages/kbn-ambient-ui-types/index.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/kbn-ambient-ui-types/index.d.ts b/packages/kbn-ambient-ui-types/index.d.ts index 3d5cb05778829..7f2f3cf1d0089 100644 --- a/packages/kbn-ambient-ui-types/index.d.ts +++ b/packages/kbn-ambient-ui-types/index.d.ts @@ -24,6 +24,12 @@ declare module '*.svg' { export default content; } +declare module '*.gif' { + const content: string; + // eslint-disable-next-line import/no-default-export + export default content; +} + declare module '*.mdx' { let MDXComponent: (props: any) => JSX.Element; // eslint-disable-next-line import/no-default-export From 9dbf147d6673ecce8cbbe591f7216db0c4378aee Mon Sep 17 00:00:00 2001 From: Lisa Cawley <lcawley@elastic.co> Date: Fri, 8 Jul 2022 09:01:49 -0700 Subject: [PATCH 04/21] Add openAPI specifications for get comments API (#135921) --- .../api/cases/cases-api-get-comments.asciidoc | 32 +- .../plugins/cases/docs/openapi/bundled.json | 285 ++++++++++++++++++ .../plugins/cases/docs/openapi/bundled.yaml | 180 +++++++++++ .../examples/get_comment_response.yaml | 19 ++ .../components/parameters/comment_id.yaml | 6 +- .../cases/docs/openapi/entrypoint.yaml | 8 +- .../paths/api@cases@{caseid}@comments.yaml | 27 ++ ...i@cases@{caseid}@comments@{commentid}.yaml | 27 ++ ...{spaceid}@api@cases@{caseid}@comments.yaml | 28 ++ ...i@cases@{caseid}@comments@{commentid}.yaml | 28 ++ 10 files changed, 618 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/cases/docs/openapi/components/examples/get_comment_response.yaml diff --git a/docs/api/cases/cases-api-get-comments.asciidoc b/docs/api/cases/cases-api-get-comments.asciidoc index 103731cd04dd7..58c4c32acfa15 100644 --- a/docs/api/cases/cases-api-get-comments.asciidoc +++ b/docs/api/cases/cases-api-get-comments.asciidoc @@ -47,12 +47,12 @@ default space is used. === {api-examples-title} -Retrieves comment ID `71ec1870-725b-11ea-a0b2-c51ea50a58e2` from case ID -`a18b38a0-71b0-11ea-a0b2-c51ea50a58e2`: +Retrieves comment ID `8048b460-fe2b-11ec-b15d-779a7c8bbcc3` from case ID +`ecbf8a20-fe2a-11ec-b15d-779a7c8bbcc3`: [source,sh] -------------------------------------------------- -GET api/cases/a18b38a0-71b0-11ea-a0b2-c51ea50a58e2/comments/71ec1870-725b-11ea-a0b2-c51ea50a58e2 +GET api/cases/ecbf8a20-fe2a-11ec-b15d-779a7c8bbcc3/comments/8048b460-fe2b-11ec-b15d-779a7c8bbcc3 -------------------------------------------------- // KIBANA @@ -61,20 +61,20 @@ The API returns the requested comment JSON object. For example: [source,json] -------------------------------------------------- { - "id":"8acb3a80-ab0a-11ec-985f-97e55adae8b9", - "version":"Wzc5NzYsM10=", - "comment":"Start operation bubblegum immediately! And chew fast!", + "id":"8048b460-fe2b-11ec-b15d-779a7c8bbcc3", + "version":"WzIzLDFd", "type":"user", "owner":"cases", - "created_at":"2022-03-24T00:37:10.832Z", - "created_by": { - "email": "classified@hms.oo.gov.uk", - "full_name": "Classified", - "username": "M" - }, - "pushed_at": null, - "pushed_by": null, - "updated_at": null, - "updated_by": null + "comment":"A new comment", + "created_at":"2022-07-07T19:32:13.104Z", + "created_by":{ + "email":null, + "full_name":null, + "username":"elastic" + }, + "pushed_at":null, + "pushed_by":null, + "updated_at":null, + "updated_by":null } -------------------------------------------------- \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index 24cdc4a35303b..a5a62a2d5615d 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -3313,6 +3313,129 @@ } ] }, + "get": { + "summary": "Retrieves all the comments from a case.", + "description": "You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases with the comments you're seeking.\n", + "tags": [ + "cases", + "kibana" + ], + "deprecated": true, + "parameters": [ + { + "$ref": "#/components/parameters/case_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/alert_comment_response_properties" + }, + { + "$ref": "#/components/schemas/user_comment_response_properties" + } + ] + } + } + }, + "examples": {} + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/api/cases/{caseId}/comments/{commentId}": { + "delete": { + "summary": "Deletes a comment or alert from a case.", + "description": "You must have `all` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/case_id" + }, + { + "$ref": "#/components/parameters/comment_id" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "get": { + "summary": "Retrieves a comment from a case.", + "description": "You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases with the comments you're seeking.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/case_id" + }, + { + "$ref": "#/components/parameters/comment_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/alert_comment_response_properties" + }, + { + "$ref": "#/components/schemas/user_comment_response_properties" + } + ] + }, + "examples": { + "getCaseCommentResponse": { + "$ref": "#/components/examples/get_comment_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, "servers": [ { "url": "https://localhost:5601" @@ -6645,6 +6768,138 @@ } ] }, + "get": { + "summary": "Retrieves all the comments from a case.", + "description": "You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases with the comments you're seeking.\n", + "deprecated": true, + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/case_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/alert_comment_response_properties" + }, + { + "$ref": "#/components/schemas/user_comment_response_properties" + } + ] + } + } + }, + "examples": {} + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/cases/{caseId}/comments/{commentId}": { + "delete": { + "summary": "Deletes a comment or alert from a case.", + "description": "You must have `all` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/case_id" + }, + { + "$ref": "#/components/parameters/comment_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "get": { + "summary": "Retrieves a comment from a case.", + "description": "You must have `read` privileges for the **Cases** feature in the **Management**, **Observability**, or **Security*** section of the Kibana feature privileges, depending on the owner of the cases with the comments you're seeking.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/case_id" + }, + { + "$ref": "#/components/parameters/comment_id" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/alert_comment_response_properties" + }, + { + "$ref": "#/components/schemas/user_comment_response_properties" + } + ] + }, + "examples": { + "getCaseCommentResponse": { + "$ref": "#/components/examples/get_comment_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, "servers": [ { "url": "https://localhost:5601" @@ -6736,6 +6991,16 @@ "example": "9c235210-6834-11ea-a78c-6ffb38a34414" } }, + "comment_id": { + "in": "path", + "name": "commentId", + "description": "The identifier for the comment. To retrieve comment IDs, use the get case or find cases APIs.\n", + "required": true, + "schema": { + "type": "string", + "example": "71ec1870-725b-11ea-a0b2-c51ea50a58e2" + } + }, "space_id": { "in": "path", "name": "spaceId", @@ -7581,6 +7846,26 @@ }, "external_service": null } + }, + "get_comment_response": { + "summary": "A single user comment retrieved from a case", + "value": { + "id": "8048b460-fe2b-11ec-b15d-779a7c8bbcc3", + "version": "WzIzLDFd", + "type": "user", + "owner": "cases", + "comment": "A new comment", + "created_at": "2022-07-07T19:32:13.104Z", + "created_by": { + "email": null, + "full_name": null, + "username": "elastic" + }, + "pushed_at": null, + "pushed_by": null, + "updated_at": null, + "updated_by": null + } } } }, diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index bf9f7823d0950..07831d6e70a50 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -2751,6 +2751,81 @@ paths: $ref: '#/components/examples/update_comment_response' servers: - url: https://localhost:5601 + get: + summary: Retrieves all the comments from a case. + description: > + You must have `read` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security** section of the Kibana + feature privileges, depending on the owner of the cases with the + comments you're seeking. + tags: + - cases + - kibana + deprecated: true + parameters: + - $ref: '#/components/parameters/case_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + anyOf: + - $ref: '#/components/schemas/alert_comment_response_properties' + - $ref: '#/components/schemas/user_comment_response_properties' + examples: {} + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /api/cases/{caseId}/comments/{commentId}: + delete: + summary: Deletes a comment or alert from a case. + description: > + You must have `all` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security** section of the Kibana + feature privileges, depending on the owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/case_id' + - $ref: '#/components/parameters/comment_id' + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + get: + summary: Retrieves a comment from a case. + description: > + You must have `read` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security** section of the Kibana + feature privileges, depending on the owner of the cases with the + comments you're seeking. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/case_id' + - $ref: '#/components/parameters/comment_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + oneOf: + - $ref: '#/components/schemas/alert_comment_response_properties' + - $ref: '#/components/schemas/user_comment_response_properties' + examples: + getCaseCommentResponse: + $ref: '#/components/examples/get_comment_response' + servers: + - url: https://localhost:5601 servers: - url: https://localhost:5601 /s/{spaceId}/api/cases: @@ -5507,6 +5582,84 @@ paths: $ref: '#/components/examples/update_comment_response' servers: - url: https://localhost:5601 + get: + summary: Retrieves all the comments from a case. + description: > + You must have `read` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security** section of the Kibana + feature privileges, depending on the owner of the cases with the + comments you're seeking. + deprecated: true + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/case_id' + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + anyOf: + - $ref: '#/components/schemas/alert_comment_response_properties' + - $ref: '#/components/schemas/user_comment_response_properties' + examples: {} + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/cases/{caseId}/comments/{commentId}: + delete: + summary: Deletes a comment or alert from a case. + description: > + You must have `all` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security** section of the Kibana + feature privileges, depending on the owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/case_id' + - $ref: '#/components/parameters/comment_id' + - $ref: '#/components/parameters/space_id' + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + get: + summary: Retrieves a comment from a case. + description: > + You must have `read` privileges for the **Cases** feature in the + **Management**, **Observability**, or **Security*** section of the + Kibana feature privileges, depending on the owner of the cases with the + comments you're seeking. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/case_id' + - $ref: '#/components/parameters/comment_id' + - $ref: '#/components/parameters/space_id' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + oneOf: + - $ref: '#/components/schemas/alert_comment_response_properties' + - $ref: '#/components/schemas/user_comment_response_properties' + examples: + getCaseCommentResponse: + $ref: '#/components/examples/get_comment_response' + servers: + - url: https://localhost:5601 servers: - url: https://localhost:5601 components: @@ -5576,6 +5729,16 @@ components: schema: type: string example: 9c235210-6834-11ea-a78c-6ffb38a34414 + comment_id: + in: path + name: commentId + description: > + The identifier for the comment. To retrieve comment IDs, use the get + case or find cases APIs. + required: true + schema: + type: string + example: 71ec1870-725b-11ea-a0b2-c51ea50a58e2 space_id: in: path name: spaceId @@ -6278,6 +6441,23 @@ components: type: .none fields: null external_service: null + get_comment_response: + summary: A single user comment retrieved from a case + value: + id: 8048b460-fe2b-11ec-b15d-779a7c8bbcc3 + version: WzIzLDFd + type: user + owner: cases + comment: A new comment + created_at: '2022-07-07T19:32:13.104Z' + created_by: + email: null + full_name: null + username: elastic + pushed_at: null + pushed_by: null + updated_at: null + updated_by: null security: - basicAuth: [] - apiKeyAuth: [] diff --git a/x-pack/plugins/cases/docs/openapi/components/examples/get_comment_response.yaml b/x-pack/plugins/cases/docs/openapi/components/examples/get_comment_response.yaml new file mode 100644 index 0000000000000..dd2baedd8eda3 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/examples/get_comment_response.yaml @@ -0,0 +1,19 @@ +summary: A single user comment retrieved from a case +value: + { + "id":"8048b460-fe2b-11ec-b15d-779a7c8bbcc3", + "version":"WzIzLDFd", + "type":"user", + "owner":"cases", + "comment":"A new comment", + "created_at":"2022-07-07T19:32:13.104Z", + "created_by":{ + "email":null, + "full_name":null, + "username":"elastic" + }, + "pushed_at":null, + "pushed_by":null, + "updated_at":null, + "updated_by":null + } \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/parameters/comment_id.yaml b/x-pack/plugins/cases/docs/openapi/components/parameters/comment_id.yaml index 41c25d8a03dc5..a46f47569e8d2 100644 --- a/x-pack/plugins/cases/docs/openapi/components/parameters/comment_id.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/parameters/comment_id.yaml @@ -1,7 +1,9 @@ in: path name: commentId -description: The identifier for the comment. To retrieve comment IDs, use the get case or find cases APIs. If it is not specified, all comments are deleted. -required: false +description: > + The identifier for the comment. To retrieve comment IDs, use the get case or + find cases APIs. +required: true schema: type: string example: '71ec1870-725b-11ea-a0b2-c51ea50a58e2' \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml index c476e67c7ad6d..18332efda8be4 100644 --- a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml @@ -41,8 +41,8 @@ paths: # $ref: 'paths/api@cases@{caseid}@alerts.yaml' '/api/cases/{caseId}/comments': $ref: 'paths/api@cases@{caseid}@comments.yaml' -# '/api/cases/{caseId}/comments/{commentId}': -# $ref: 'paths/api@cases@{caseid}@comments@{commentid}.yaml' + '/api/cases/{caseId}/comments/{commentId}': + $ref: 'paths/api@cases@{caseid}@comments@{commentid}.yaml' # '/api/cases/{caseId}/connector/{connectorId}/_push': # $ref: 'paths/api@cases@{caseid}@connector@{connectorid}@_push.yaml' # '/api/cases/{caseId}/user_actions': @@ -72,8 +72,8 @@ paths: # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@alerts.yaml' '/s/{spaceId}/api/cases/{caseId}/comments': $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments.yaml' -# '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': -# $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' + '/s/{spaceId}/api/cases/{caseId}/comments/{commentId}': + $ref: 'paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml' # '/s/{spaceId}/api/cases/{caseId}/connector/{connectorId}/_push': # $ref: 'paths/s@{spaceid}@api@cases@{caseid}@connector@{connectorid}@_push.yaml' # '/s/{spaceId}/api/cases/{caseId}/user_actions': diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments.yaml index 8e719ad40f669..15fa137fa64b4 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments.yaml @@ -91,5 +91,32 @@ patch: servers: - url: https://localhost:5601 +get: + summary: Retrieves all the comments from a case. + description: > + You must have `read` privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security** section of the Kibana feature privileges, + depending on the owner of the cases with the comments you're seeking. + tags: + - cases + - kibana + deprecated: true + parameters: + - $ref: ../components/parameters/case_id.yaml + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + anyOf: + - $ref: '../components/schemas/alert_comment_response_properties.yaml' + - $ref: '../components/schemas/user_comment_response_properties.yaml' + examples: {} + servers: + - url: https://localhost:5601 + servers: - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments@{commentid}.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments@{commentid}.yaml index a89edd5247472..0b167d3e8d25c 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments@{commentid}.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@{caseid}@comments@{commentid}.yaml @@ -17,5 +17,32 @@ delete: servers: - url: https://localhost:5601 +get: + summary: Retrieves a comment from a case. + description: > + You must have `read` privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security** section of the Kibana feature privileges, + depending on the owner of the cases with the comments you're seeking. + tags: + - cases + - kibana + parameters: + - $ref: '../components/parameters/case_id.yaml' + - $ref: '../components/parameters/comment_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + oneOf: + - $ref: '../components/schemas/alert_comment_response_properties.yaml' + - $ref: '../components/schemas/user_comment_response_properties.yaml' + examples: + getCaseCommentResponse: + $ref: '../components/examples/get_comment_response.yaml' + servers: + - url: https://localhost:5601 + servers: - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments.yaml index 0e1960bdce513..dc07c62c38c50 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments.yaml @@ -94,5 +94,33 @@ patch: servers: - url: https://localhost:5601 +get: + summary: Retrieves all the comments from a case. + description: > + You must have `read` privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security** section of the Kibana feature privileges, + depending on the owner of the cases with the comments you're seeking. + deprecated: true + tags: + - cases + - kibana + parameters: + - $ref: '../components/parameters/case_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + anyOf: + - $ref: '../components/schemas/alert_comment_response_properties.yaml' + - $ref: '../components/schemas/user_comment_response_properties.yaml' + examples: {} + servers: + - url: https://localhost:5601 + servers: - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml index 4970fa9ec7be2..c9ad642bdd559 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@{caseid}@comments@{commentid}.yaml @@ -18,5 +18,33 @@ delete: servers: - url: https://localhost:5601 +get: + summary: Retrieves a comment from a case. + description: > + You must have `read` privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security*** section of the Kibana feature privileges, + depending on the owner of the cases with the comments you're seeking. + tags: + - cases + - kibana + parameters: + - $ref: '../components/parameters/case_id.yaml' + - $ref: '../components/parameters/comment_id.yaml' + - $ref: '../components/parameters/space_id.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + oneOf: + - $ref: '../components/schemas/alert_comment_response_properties.yaml' + - $ref: '../components/schemas/user_comment_response_properties.yaml' + examples: + getCaseCommentResponse: + $ref: '../components/examples/get_comment_response.yaml' + servers: + - url: https://localhost:5601 + servers: - url: https://localhost:5601 \ No newline at end of file From f5e87246475171083e99e96ff3052c19151a2d0c Mon Sep 17 00:00:00 2001 From: Mark Hopkin <mark.hopkin@elastic.co> Date: Fri, 8 Jul 2022 17:34:38 +0100 Subject: [PATCH 05/21] [Fleet] Display package verification status (#135928) * add attributes.type to fleet error * return verification key ID as part of status response * show verification status of installed integrations * badge on installed integrations tab * show unverified status on overview page * Do now show labels on available packages list * show release label and verificatiomn status * update stories * self review * fix badge label * update openapi * add unit tests for verification function * review comments --- .../plugins/fleet/common/openapi/bundled.json | 3 + .../plugins/fleet/common/openapi/bundled.yaml | 2 + .../schemas/fleet_status_response.yaml | 2 + .../plugins/fleet/common/types/models/epm.ts | 2 + .../fleet/common/types/rest_spec/error.ts | 16 +++ .../common/types/rest_spec/fleet_setup.ts | 1 + .../fleet/common/types/rest_spec/index.ts | 15 +- .../integrations/layouts/default.tsx | 73 ++++++---- .../epm/components/package_card.stories.tsx | 60 +++----- .../sections/epm/components/package_card.tsx | 29 +++- .../epm/components/package_list_grid.tsx | 12 +- .../epm/screens/detail/overview/overview.tsx | 31 +++- .../epm/screens/home/available_packages.tsx | 14 +- .../sections/epm/screens/home/index.tsx | 59 ++++++-- .../epm/screens/home/installed_packages.tsx | 70 ++++++--- .../fleet/public/hooks/use_fleet_status.tsx | 2 + x-pack/plugins/fleet/public/services/index.ts | 2 +- .../services/package_verification.test.ts | 135 ++++++++++++++++++ .../public/services/package_verification.ts | 26 ++++ .../plugins/fleet/server/errors/handlers.ts | 2 +- x-pack/plugins/fleet/server/errors/index.ts | 7 +- .../fleet/server/routes/setup/handlers.ts | 7 + .../epm/packages/package_verification.ts | 8 ++ 23 files changed, 453 insertions(+), 125 deletions(-) create mode 100644 x-pack/plugins/fleet/common/types/rest_spec/error.ts create mode 100644 x-pack/plugins/fleet/public/services/package_verification.test.ts create mode 100644 x-pack/plugins/fleet/public/services/package_verification.ts diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 0b32651376f95..53e1b9f46568d 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -4082,6 +4082,9 @@ "encrypted_saved_object_encryption_key_required" ] } + }, + "package_verification_key_id": { + "type": "string" } }, "required": [ diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index d2044f9e631ce..20fe96ac4cd2d 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -2557,6 +2557,8 @@ components: type: string enum: - encrypted_saved_object_encryption_key_required + package_verification_key_id: + type: string required: - isReady - missing_requirements diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_status_response.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_status_response.yaml index cf0f0f60084da..8bb00fdce58d3 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_status_response.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_status_response.yaml @@ -18,6 +18,8 @@ properties: type: string enum: - 'encrypted_saved_object_encryption_key_required' + package_verification_key_id: + type: string required: - isReady - missing_requirements diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index e177bcba0ebb2..2148f2ff4555c 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -406,6 +406,8 @@ export interface IntegrationCardItem { id: string; categories: string[]; fromIntegrations?: string; + isUnverified?: boolean; + showLabels?: boolean; } export type PackageVerificationStatus = 'verified' | 'unverified' | 'unknown'; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/error.ts b/x-pack/plugins/fleet/common/types/rest_spec/error.ts new file mode 100644 index 0000000000000..c02f9cdb4db6c --- /dev/null +++ b/x-pack/plugins/fleet/common/types/rest_spec/error.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type FleetErrorType = 'verification_failed'; + +export interface FleetErrorResponse { + message: string; + statusCode: number; + attributes?: { + type?: FleetErrorType; + }; +} diff --git a/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts index 5204b7bfbdbd1..33929bd92c8b1 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/fleet_setup.ts @@ -16,4 +16,5 @@ export interface GetFleetStatusResponse { 'security_required' | 'tls_required' | 'api_keys' | 'fleet_admin_user' | 'fleet_server' >; missing_optional_features: Array<'encrypted_saved_object_encryption_key_required'>; + package_verification_key_id?: string; } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/index.ts b/x-pack/plugins/fleet/common/types/rest_spec/index.ts index 78b9f09f7f9f8..989e65209bf9d 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/index.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/index.ts @@ -5,15 +5,16 @@ * 2.0. */ +export * from './agent_policy'; +export * from './agent'; +export * from './app'; export * from './common'; -export * from './package_policy'; export * from './data_stream'; -export * from './agent'; -export * from './agent_policy'; -export * from './fleet_setup'; -export * from './epm'; +export * from './download_sources'; export * from './enrollment_api_key'; +export * from './epm'; +export * from './error'; +export * from './fleet_setup'; export * from './output'; +export * from './package_policy'; export * from './settings'; -export * from './app'; -export * from './download_sources'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx index 2554a8b730938..24fdfee190512 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx @@ -5,21 +5,61 @@ * 2.0. */ import React, { memo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import styled from 'styled-components'; + import { useLink } from '../../../hooks'; import type { Section } from '../sections'; import { WithHeaderLayout } from '.'; +const TabBadge = styled(EuiBadge)` + padding: 0 1px; + margin-left: 4px; +`; + +const TabTitle: React.FC<{ title: JSX.Element; hasWarning: boolean }> = memo( + ({ title, hasWarning }) => { + return ( + <> + {title} + {hasWarning && <TabBadge color="warning" iconType="alert" />} + </> + ); + } +); interface Props { section?: Section; children?: React.ReactNode; + sectionsWithWarning?: Section[]; } -export const DefaultLayout: React.FunctionComponent<Props> = memo(({ section, children }) => { +export const DefaultLayout: React.FC<Props> = memo(({ section, children, sectionsWithWarning }) => { const { getHref } = useLink(); + const tabs = [ + { + name: ( + <FormattedMessage + id="xpack.fleet.appNavigation.integrationsAllLinkText" + defaultMessage="Browse integrations" + /> + ), + section: 'browse' as Section, + href: getHref('integrations_all'), + }, + { + name: ( + <FormattedMessage + id="xpack.fleet.appNavigation.integrationsInstalledLinkText" + defaultMessage="Installed integrations" + /> + ), + section: 'manage' as Section, + href: getHref('integrations_installed'), + }, + ]; return ( <WithHeaderLayout @@ -48,28 +88,13 @@ export const DefaultLayout: React.FunctionComponent<Props> = memo(({ section, ch </EuiFlexItem> </EuiFlexGroup> } - tabs={[ - { - name: ( - <FormattedMessage - id="xpack.fleet.appNavigation.integrationsAllLinkText" - defaultMessage="Browse integrations" - /> - ), - isSelected: section === 'browse', - href: getHref('integrations_all'), - }, - { - name: ( - <FormattedMessage - id="xpack.fleet.appNavigation.integrationsInstalledLinkText" - defaultMessage="Installed integrations" - /> - ), - isSelected: section === 'manage', - href: getHref('integrations_installed'), - }, - ]} + tabs={tabs.map((tab) => ({ + name: ( + <TabTitle title={tab.name} hasWarning={!!sectionsWithWarning?.includes(tab.section)} /> + ), + href: tab.href, + isSelected: section === tab.section, + }))} > {children} </WithHeaderLayout> diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx index e62044b31333a..dee9dfbe4d42b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx @@ -7,10 +7,6 @@ import React from 'react'; -import type { SavedObject } from '@kbn/core/public'; - -import type { Installation } from '../../../../../../common'; - import type { PackageCardProps } from './package_card'; import { PackageCard } from './package_card'; @@ -19,7 +15,7 @@ export default { description: 'A card representing a package available in Fleet', }; -type Args = Omit<PackageCardProps, 'status'> & { width: number }; +type Args = PackageCardProps & { width: number }; const args: Args = { width: 280, @@ -33,6 +29,7 @@ const args: Args = { icons: [], integration: '', categories: ['foobar'], + isUnverified: false, }; const argTypes = { @@ -42,48 +39,23 @@ const argTypes = { options: ['ga', 'beta', 'experimental'], }, }, + isUnverified: { + control: 'boolean', + }, }; -export const NotInstalled = ({ width, ...props }: Args) => ( +export const AvailablePackage = ({ width, ...props }: Args) => ( <div style={{ width }}> - {/* - // @ts-ignore */} - <PackageCard {...props} status="not_installed" /> + <PackageCard {...props} showLabels={false} /> </div> ); +AvailablePackage.args = args; +AvailablePackage.argTypes = argTypes; -export const Installed = ({ width, ...props }: Args) => { - const savedObject: SavedObject<Installation> = { - id: props.id, - // @ts-expect-error - type: props.type || '', - attributes: { - name: props.name, - version: props.version, - install_version: props.version, - es_index_patterns: {}, - installed_kibana: [], - installed_kibana_space_id: 'default', - installed_es: [], - install_status: 'installed', - install_source: 'registry', - install_started_at: '2020-01-01T00:00:00.000Z', - keep_policies_up_to_date: false, - verification_status: 'unknown', - }, - references: [], - }; - - return ( - <div style={{ width }}> - {/* - // @ts-ignore */} - <PackageCard {...props} status="installed" savedObject={savedObject} /> - </div> - ); -}; - -NotInstalled.args = args; -NotInstalled.argTypes = argTypes; -Installed.args = args; -Installed.argTypes = argTypes; +export const InstalledPackage = ({ width, ...props }: Args) => ( + <div style={{ width }}> + <PackageCard {...props} showLabels={true} /> + </div> +); +InstalledPackage.args = args; +InstalledPackage.argTypes = argTypes; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index 16296c928c710..fa96da7ea6ace 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -7,10 +7,12 @@ import React from 'react'; import styled from 'styled-components'; -import { EuiCard, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiBadge, EuiCard, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; +import { FormattedMessage } from '@kbn/i18n-react'; + import { CardIcon } from '../../../../../components/package_icon'; import type { IntegrationCardItem } from '../../../../../../common/types/models/epm'; @@ -38,6 +40,8 @@ export function PackageCard({ release, id, fromIntegrations, + isUnverified, + showLabels = true, }: PackageCardProps) { let releaseBadge: React.ReactNode | null = null; @@ -52,6 +56,24 @@ export function PackageCard({ ); } + let verifiedBadge: React.ReactNode | null = null; + + if (isUnverified && showLabels) { + verifiedBadge = ( + <EuiFlexItem grow={false}> + <EuiSpacer size="xs" /> + <span> + <EuiBadge color="warning"> + <FormattedMessage + id="xpack.fleet.packageCard.unverifiedLabel" + defaultMessage="Unverified" + /> + </EuiBadge> + </span> + </EuiFlexItem> + ); + } + const { application } = useStartServices(); const onCardClick = () => { @@ -88,7 +110,10 @@ export function PackageCard({ } onClick={onCardClick} > - {releaseBadge} + <EuiFlexGroup gutterSize="xs"> + {verifiedBadge} + {releaseBadge} + </EuiFlexGroup> </Card> </TrackApplicationView> ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx index d9e258f70f902..09e28e649b84e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx @@ -39,6 +39,7 @@ export interface Props { onSearchChange: (search: string) => void; showMissingIntegrationMessage?: boolean; callout?: JSX.Element | null; + showCardLabels?: boolean; } export const PackageListGrid: FunctionComponent<Props> = ({ @@ -52,6 +53,7 @@ export const PackageListGrid: FunctionComponent<Props> = ({ showMissingIntegrationMessage = false, featuredList = null, callout, + showCardLabels = true, }) => { const [searchTerm, setSearchTerm] = useState(initialSearch || ''); const localSearchRef = useLocalSearch(list); @@ -104,6 +106,7 @@ export const PackageListGrid: FunctionComponent<Props> = ({ <GridColumn list={filteredList} showMissingIntegrationMessage={showMissingIntegrationMessage} + showCardLabels={showCardLabels} /> ); } @@ -180,16 +183,21 @@ function ControlsColumn({ controls, title, sticky }: ControlsColumnProps) { interface GridColumnProps { list: IntegrationCardItem[]; showMissingIntegrationMessage?: boolean; + showCardLabels?: boolean; } -function GridColumn({ list, showMissingIntegrationMessage = false }: GridColumnProps) { +function GridColumn({ + list, + showMissingIntegrationMessage = false, + showCardLabels = false, +}: GridColumnProps) { return ( <EuiFlexGrid gutterSize="l" columns={3}> {list.length ? ( list.map((item) => { return ( <EuiFlexItem key={item.id}> - <PackageCard {...item} /> + <PackageCard {...item} showLabels={showCardLabels} /> </EuiFlexItem> ); }) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx index 0b477fee2ba77..5433244d5c2c6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx @@ -6,8 +6,12 @@ */ import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useFleetStatus } from '../../../../../../../hooks'; +import { isPackageUnverified } from '../../../../../../../services'; import type { PackageInfo, RegistryPolicyTemplate } from '../../../../../types'; import { Screenshots } from './screenshots'; @@ -26,16 +30,39 @@ const LeftColumn = styled(EuiFlexItem)` } `; +const UnverifiedCallout = () => ( + <> + <EuiCallOut + title={i18n.translate('xpack.fleet.epm.verificationWarningCalloutTitle', { + defaultMessage: 'This integration is not verified', + })} + iconType="alert" + color="warning" + > + <p> + <FormattedMessage + id="xpack.fleet.epm.verificationWarningCalloutIntroText" + defaultMessage="One or more of the installed integrations contains an unsigned package with unknown authenticity." + // TODO: add documentation link + /> + </p> + </EuiCallOut> + <EuiSpacer size="l" /> + </> +); + export const OverviewPage: React.FC<Props> = memo(({ packageInfo, integrationInfo }) => { const screenshots = useMemo( () => integrationInfo?.screenshots || packageInfo.screenshots || [], [integrationInfo, packageInfo.screenshots] ); - + const { packageVerificationKeyId } = useFleetStatus(); + const isUnverified = isPackageUnverified(packageInfo, packageVerificationKeyId); return ( <EuiFlexGroup alignItems="flexStart"> <LeftColumn grow={2} /> <EuiFlexItem grow={9} className="eui-textBreakWord"> + {isUnverified && <UnverifiedCallout />} {packageInfo.readme ? ( <Readme readmePath={integrationInfo?.readme || packageInfo.readme} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index 095a38cdcd92f..af3faff04243d 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -6,7 +6,7 @@ */ import type { FunctionComponent } from 'react'; -import React, { memo, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useLocation, useHistory, useParams } from 'react-router-dom'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; @@ -38,7 +38,7 @@ import { useLink, } from '../../../../hooks'; import { doesPackageHaveIntegrations } from '../../../../services'; -import type { PackageList } from '../../../../types'; +import type { GetPackagesResponse, PackageList } from '../../../../types'; import { PackageListGrid } from '../../components/package_list_grid'; import type { PackageListItem } from '../../../../types'; @@ -180,7 +180,10 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => { // TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http` // or `location` to load data. Ideally, we'll split this into "connected" and "pure" components. -export const AvailablePackages: React.FC = memo(() => { +export const AvailablePackages: React.FC<{ + allPackages?: GetPackagesResponse | null; + isLoading: boolean; +}> = ({ allPackages, isLoading }) => { const [preference, setPreference] = useState<IntegrationPreferenceType>('recommended'); useBreadcrumbs('integrations_all'); @@ -238,7 +241,7 @@ export const AvailablePackages: React.FC = memo(() => { ]; const cards: IntegrationCardItem[] = eprAndCustomPackages.map((item) => { - return mapToCard(getAbsolutePath, getHref, item); + return mapToCard({ getAbsolutePath, getHref, item }); }); cards.sort((a, b) => { @@ -382,6 +385,7 @@ export const AvailablePackages: React.FC = memo(() => { onSearchChange={setSearchTerm} showMissingIntegrationMessage callout={noEprCallout} + showCardLabels={false} /> ); -}); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 4322f434ddc70..93d226f885119 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -5,21 +5,28 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { useMemo } from 'react'; import { Switch, Route } from 'react-router-dom'; import type { IntegrationCategory } from '@kbn/custom-integrations-plugin/common'; import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common'; +import { installationStatuses } from '../../../../../../../common/constants'; + import type { DynamicPage, DynamicPagePathValues, StaticPage } from '../../../../constants'; import { INTEGRATIONS_ROUTING_PATHS, INTEGRATIONS_SEARCH_QUERYPARAM } from '../../../../constants'; import { DefaultLayout } from '../../../../layouts'; +import { isPackageUnverified } from '../../../../services'; import type { PackageListItem } from '../../../../types'; import type { IntegrationCardItem } from '../../../../../../../common/types/models'; +import { useGetPackages } from '../../../../hooks'; + +import type { Section } from '../../..'; + import type { CategoryFacet } from './category_facets'; import { InstalledPackages } from './installed_packages'; import { AvailablePackages } from './available_packages'; @@ -43,21 +50,29 @@ export const categoryExists = (category: string, categories: CategoryFacet[]) => return categories.some((c) => c.id === category); }; -export const mapToCard = ( - getAbsolutePath: (p: string) => string, - getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => string, - item: CustomIntegration | PackageListItem, - selectedCategory?: string -): IntegrationCardItem => { +export const mapToCard = ({ + getAbsolutePath, + getHref, + item, + packageVerificationKeyId, + selectedCategory, +}: { + getAbsolutePath: (p: string) => string; + getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => string; + item: CustomIntegration | PackageListItem; + packageVerificationKeyId?: string; + selectedCategory?: string; +}): IntegrationCardItem => { let uiInternalPathUrl; + let isUnverified = false; if (item.type === 'ui_link') { uiInternalPathUrl = item.uiExternalLink || getAbsolutePath(item.uiInternalPath); } else { let urlVersion = item.version; - if ('savedObject' in item) { urlVersion = item.savedObject.attributes.version || item.version; + isUnverified = isPackageUnverified(item, packageVerificationKeyId); } const url = getHref('integration_details_overview', { @@ -87,22 +102,38 @@ export const mapToCard = ( version: 'version' in item ? item.version || '' : '', release, categories: ((item.categories || []) as string[]).filter((c: string) => !!c), + isUnverified, }; }; -export const EPMHomePage: React.FC = memo(() => { +export const EPMHomePage: React.FC = () => { + const { data: allPackages, isLoading } = useGetPackages({ + experimental: true, + }); + + const installedPackages = useMemo( + () => + (allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed), + [allPackages?.response] + ); + + const atLeastOneUnverifiedPackageInstalled = installedPackages.some( + (pkg) => 'savedObject' in pkg && pkg.savedObject.attributes.verification_status === 'unverified' + ); + + const sectionsWithWarning = (atLeastOneUnverifiedPackageInstalled ? ['manage'] : []) as Section[]; return ( <Switch> <Route path={INTEGRATIONS_ROUTING_PATHS.integrations_installed}> - <DefaultLayout section="manage"> - <InstalledPackages /> + <DefaultLayout section="manage" sectionsWithWarning={sectionsWithWarning}> + <InstalledPackages installedPackages={installedPackages} isLoading={isLoading} /> </DefaultLayout> </Route> <Route path={INTEGRATIONS_ROUTING_PATHS.integrations_all}> - <DefaultLayout section="browse"> - <AvailablePackages /> + <DefaultLayout section="browse" sectionsWithWarning={sectionsWithWarning}> + <AvailablePackages allPackages={allPackages} isLoading={isLoading} /> </DefaultLayout> </Route> </Switch> ); -}); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx index 19de166cebc16..776aaaafd5a1e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { useLocation, useHistory, useParams } from 'react-router-dom'; import semverLt from 'semver/functions/lt'; import { i18n } from '@kbn/i18n'; @@ -13,11 +13,12 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; -import { installationStatuses } from '../../../../../../../common/constants'; import { pagePathGetters } from '../../../../constants'; -import { useGetPackages, useBreadcrumbs, useLink, useStartServices } from '../../../../hooks'; +import { useBreadcrumbs, useLink, useStartServices, useFleetStatus } from '../../../../hooks'; import { PackageListGrid } from '../../components/package_list_grid'; +import type { PackageListItem } from '../../../../types'; + import type { CategoryFacet } from './category_facets'; import { CategoryFacets } from './category_facets'; @@ -37,7 +38,7 @@ const AnnouncementLink = () => { ); }; -const Callout = () => ( +const InstalledIntegrationsInfoCallout = () => ( <EuiCallOut title={i18n.translate('xpack.fleet.epmList.availableCalloutTitle', { defaultMessage: 'Only installed Elastic Agent Integrations are displayed.', @@ -56,14 +57,33 @@ const Callout = () => ( </EuiCallOut> ); +const VerificationWarningCallout = () => ( + <EuiCallOut + title={i18n.translate('xpack.fleet.epmList.verificationWarningCalloutTitle', { + defaultMessage: 'Some installed integrations are not verified', + })} + iconType="alert" + color="warning" + > + <p> + <FormattedMessage + id="xpack.fleet.epmList.verificationWarningCalloutIntroText" + defaultMessage="One or more of the installed integrations contains an unsigned package with unknown authenticity." + // TODO: add documentation link + /> + </p> + </EuiCallOut> +); + // TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http` // or `location` to load data. Ideally, we'll split this into "connected" and "pure" components. -export const InstalledPackages: React.FC = memo(() => { +export const InstalledPackages: React.FC<{ + installedPackages: PackageListItem[]; + isLoading: boolean; +}> = ({ installedPackages, isLoading }) => { useBreadcrumbs('integrations_installed'); - const { data: allPackages, isLoading } = useGetPackages({ - experimental: true, - }); + const { packageVerificationKeyId } = useFleetStatus(); const { getHref, getAbsolutePath } = useLink(); @@ -93,26 +113,20 @@ export const InstalledPackages: React.FC = memo(() => { ); } - const allInstalledPackages = useMemo( - () => - (allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed), - [allPackages?.response] - ); - const updatablePackages = useMemo( () => - allInstalledPackages.filter( + installedPackages.filter( (item) => 'savedObject' in item && semverLt(item.savedObject.attributes.version, item.version) ), - [allInstalledPackages] + [installedPackages] ); const categories: CategoryFacet[] = useMemo( () => [ { ...INSTALLED_CATEGORY, - count: allInstalledPackages.length, + count: installedPackages.length, }, { id: 'updates_available', @@ -122,7 +136,7 @@ export const InstalledPackages: React.FC = memo(() => { }), }, ], - [allInstalledPackages.length, updatablePackages.length] + [installedPackages.length, updatablePackages.length] ); if (!categoryExists(selectedCategory, categories)) { @@ -142,10 +156,22 @@ export const InstalledPackages: React.FC = memo(() => { ); const cards = ( - selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages - ).map((item) => mapToCard(getAbsolutePath, getHref, item, selectedCategory || 'installed')); + selectedCategory === 'updates_available' ? updatablePackages : installedPackages + ).map((item) => + mapToCard({ + getAbsolutePath, + getHref, + item, + selectedCategory: selectedCategory || 'installed', + packageVerificationKeyId, + }) + ); - const callout = selectedCategory === 'updates_available' ? null : <Callout />; + const CalloutComponent = cards.some((c) => c.isUnverified) + ? VerificationWarningCallout + : InstalledIntegrationsInfoCallout; + const callout = + selectedCategory === 'updates_available' || isLoading ? null : <CalloutComponent />; return ( <PackageListGrid @@ -155,4 +181,4 @@ export const InstalledPackages: React.FC = memo(() => { list={cards} /> ); -}); +}; diff --git a/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx b/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx index 7319cd9f07be7..29fa37f3b7270 100644 --- a/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_fleet_status.tsx @@ -19,6 +19,7 @@ interface FleetStatusState { error?: Error; missingRequirements?: GetFleetStatusResponse['missing_requirements']; missingOptionalFeatures?: GetFleetStatusResponse['missing_optional_features']; + packageVerificationKeyId?: GetFleetStatusResponse['package_verification_key_id']; } interface FleetStatus extends FleetStatusState { @@ -58,6 +59,7 @@ export const FleetStatusProvider: React.FC = ({ children }) => { isReady: res.data?.isReady ?? false, missingRequirements: res.data?.missing_requirements, missingOptionalFeatures: res.data?.missing_optional_features, + packageVerificationKeyId: res.data?.package_verification_key_id, })); } catch (error) { setState((s) => ({ ...s, isLoading: false, error })); diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index 2c1bedeaef82c..094fe2b66c6c9 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -41,7 +41,7 @@ export { countValidationErrors, getStreamsForInputType, } from '../../common'; - +export * from './package_verification'; export * from './pkg_key_from_package_info'; export * from './ui_extensions'; export * from './increment_policy_name'; diff --git a/x-pack/plugins/fleet/public/services/package_verification.test.ts b/x-pack/plugins/fleet/public/services/package_verification.test.ts new file mode 100644 index 0000000000000..d611eb6cccf0a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/package_verification.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PackageVerificationStatus } from '../../common'; +import type { PackageInfo } from '../types'; + +import { ExperimentalFeaturesService, isPackageUnverified } from '.'; + +const mockGet = jest.spyOn(ExperimentalFeaturesService, 'get'); + +const createPackage = ({ + verificationStatus = 'unknown', + verificationKeyId, +}: { + verificationStatus?: PackageVerificationStatus; + verificationKeyId?: string; +} = {}): PackageInfo => ({ + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [], + // @ts-ignore + assets: {}, + savedObject: { + id: '1234', + type: 'epm-package', + references: [], + attributes: { + installed_kibana: [], + installed_es: [], + es_index_patterns: {}, + name: 'test-package', + version: '0.0.1', + install_status: 'installed', + install_version: '0.0.1', + install_started_at: new Date().toString(), + install_source: 'registry', + verification_status: verificationStatus, + ...(verificationKeyId && { verification_key_id: verificationKeyId }), + }, + }, +}); + +describe('isPackageUnverified', () => { + describe('When experimental feature is disabled', () => { + beforeEach(() => { + mockGet.mockReturnValue({ + packageVerification: false, + } as ReturnType<typeof ExperimentalFeaturesService['get']>); + }); + + it('Should return false for a package with no saved object', () => { + const noSoPkg = createPackage(); + // @ts-ignore we know pkg has savedObject but ts doesn't + delete noSoPkg.savedObject; + expect(isPackageUnverified(noSoPkg)).toEqual(false); + }); + it('Should return false for an unverified package', () => { + const unverifiedPkg = createPackage({ verificationStatus: 'unverified' }); + expect(isPackageUnverified(unverifiedPkg)).toEqual(false); + }); + it('Should return false for a verified package', () => { + const verifiedPkg = createPackage({ verificationStatus: 'verified' }); + expect(isPackageUnverified(verifiedPkg)).toEqual(false); + }); + it('Should return false for a verified package with correct key', () => { + const unverifiedPkg = createPackage({ + verificationStatus: 'verified', + verificationKeyId: '1234', + }); + expect(isPackageUnverified(unverifiedPkg, '1234')).toEqual(false); + }); + it('Should return false for a verified package with out of date key', () => { + const unverifiedPkg = createPackage({ + verificationStatus: 'verified', + verificationKeyId: '1234', + }); + expect(isPackageUnverified(unverifiedPkg, 'not_1234')).toEqual(false); + }); + it('Should return false for an unknown verification package', () => { + const unknownPkg = createPackage({ verificationStatus: 'unknown' }); + expect(isPackageUnverified(unknownPkg)).toEqual(false); + }); + }); + describe('When experimental feature is enabled', () => { + beforeEach(() => { + mockGet.mockReturnValue({ + packageVerification: true, + } as ReturnType<typeof ExperimentalFeaturesService['get']>); + }); + it('Should return false for a package with no saved object', () => { + const noSoPkg = createPackage(); + // @ts-ignore we know pkg has savedObject but ts doesn't + delete noSoPkg.savedObject; + expect(isPackageUnverified(noSoPkg)).toEqual(false); + }); + it('Should return false for a verified package', () => { + const unverifiedPkg = createPackage({ verificationStatus: 'verified' }); + expect(isPackageUnverified(unverifiedPkg)).toEqual(false); + }); + it('Should return false for an unknown verification package', () => { + const unverifiedPkg = createPackage({ verificationStatus: 'unknown' }); + expect(isPackageUnverified(unverifiedPkg)).toEqual(false); + }); + it('Should return false for a verified package with correct key', () => { + const unverifiedPkg = createPackage({ + verificationStatus: 'verified', + verificationKeyId: '1234', + }); + expect(isPackageUnverified(unverifiedPkg, '1234')).toEqual(false); + }); + + it('Should return true for an unverified package', () => { + const unverifiedPkg = createPackage({ verificationStatus: 'unverified' }); + expect(isPackageUnverified(unverifiedPkg)).toEqual(true); + }); + + it('Should return true for a verified package with out of date key', () => { + const unverifiedPkg = createPackage({ + verificationStatus: 'verified', + verificationKeyId: '1234', + }); + expect(isPackageUnverified(unverifiedPkg, 'not_1234')).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/services/package_verification.ts b/x-pack/plugins/fleet/public/services/package_verification.ts new file mode 100644 index 0000000000000..5b40a453be413 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/package_verification.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PackageInfo, PackageListItem } from '../types'; + +import { ExperimentalFeaturesService } from '.'; + +export function isPackageUnverified( + pkg: PackageInfo | PackageListItem, + packageVerificationKeyId?: string +) { + if (!('savedObject' in pkg)) return false; + + const { verification_status: verificationStatus, verification_key_id: verificationKeyId } = + pkg.savedObject.attributes; + + const { packageVerification: isPackageVerificationEnabled } = ExperimentalFeaturesService.get(); + const isKeyOutdated = !!verificationKeyId && verificationKeyId !== packageVerificationKeyId; + const isUnverified = + verificationStatus === 'unverified' || (verificationStatus === 'verified' && isKeyOutdated); + return isPackageVerificationEnabled && isUnverified; +} diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index 1eb0aed4ad690..8b5f8463c9f41 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -84,7 +84,7 @@ export function ingestErrorToResponseOptions(error: IngestErrorHandlerParams['er logger.error(error.message); return { statusCode: getHTTPResponseCode(error), - body: { message: error.message }, + body: { message: error.message, attributes: error.attributes }, }; } diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index cbfcf5733f736..ecd5bb8c670ae 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -8,13 +8,15 @@ /* eslint-disable max-classes-per-file */ import type { ElasticsearchErrorDetails } from '@kbn/es-errors'; +import type { FleetErrorType } from '../../common'; + import { isESClientError } from './utils'; export { defaultIngestErrorHandler, ingestErrorToResponseOptions } from './handlers'; export { isESClientError } from './utils'; - export class IngestManagerError extends Error { + attributes?: { type?: FleetErrorType }; constructor(message?: string, public readonly meta?: unknown) { super(message); this.name = this.constructor.name; // for stack traces @@ -34,6 +36,9 @@ export class PackageOutdatedError extends IngestManagerError {} export class PackageFailedVerificationError extends IngestManagerError { constructor(pkgKey: string) { super(`${pkgKey} failed signature verification.`); + this.attributes = { + type: 'verification_failed', + }; } } export class AgentPolicyError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index 59a3516aac83a..2823c1b3873a5 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -11,6 +11,7 @@ import { formatNonFatalErrors, setupFleet } from '../../services/setup'; import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; import type { FleetRequestHandler } from '../../types'; +import { getGpgKeyIdOrUndefined } from '../../services/epm/packages/package_verification'; export const getFleetStatusHandler: FleetRequestHandler = async (context, request, response) => { try { @@ -43,6 +44,12 @@ export const getFleetStatusHandler: FleetRequestHandler = async (context, reques missing_optional_features: missingOptionalFeatures, }; + const packageVerificationKeyId = await getGpgKeyIdOrUndefined(); + + if (packageVerificationKeyId) { + body.package_verification_key_id = packageVerificationKeyId; + } + return response.ok({ body, }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/package_verification.ts b/x-pack/plugins/fleet/server/services/epm/packages/package_verification.ts index 0e39cefaf1169..b4432e8919d0c 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/package_verification.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/package_verification.ts @@ -24,6 +24,14 @@ interface VerificationResult { let cachedKey: openpgp.Key | undefined | null = null; +export async function getGpgKeyIdOrUndefined(): Promise<string | undefined> { + const key = await getGpgKeyOrUndefined(); + + if (!key) return undefined; + + return key.getKeyID().toHex(); +} + export async function getGpgKeyOrUndefined(): Promise<openpgp.Key | undefined> { if (cachedKey !== null) return cachedKey; From aee38f15f9aff47db76a40cfe50ee29b6a3e3a1a Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Fri, 8 Jul 2022 18:49:42 +0200 Subject: [PATCH 06/21] added logic to sanitize tags on UI (#136019) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/tag_options.test.tsx | 4 +- .../components/tag_options.tsx | 5 ++- .../components/tags_add_remove.test.tsx | 40 +++++++++++++++++++ .../components/tags_add_remove.tsx | 27 ++++++++----- .../agents/agent_list_page/utils/index.ts | 1 + .../utils/sanitize_tag.test.ts | 22 ++++++++++ .../agent_list_page/utils/sanitize_tag.ts | 10 +++++ 7 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.test.ts create mode 100644 x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.ts diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.test.tsx index 584a92a60310f..d4ad6f8fe144f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.test.tsx @@ -57,7 +57,7 @@ describe('TagOptions', () => { fireEvent.click(result.getByText('Delete tag')); expect(mockBulkUpdateTags).toHaveBeenCalledWith( - 'tags:agent', + 'tags:"agent"', [], ['agent'], expect.anything(), @@ -80,7 +80,7 @@ describe('TagOptions', () => { }); expect(mockBulkUpdateTags).toHaveBeenCalledWith( - 'tags:agent', + 'tags:"agent"', ['newName'], ['agent'], expect.anything(), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.tsx index 994fb1b64880e..7e25a7a098dba 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tag_options.tsx @@ -20,6 +20,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useUpdateTags } from '../hooks'; +import { sanitizeTag } from '../utils'; interface Props { tagName: string; @@ -53,7 +54,7 @@ export const TagOptions: React.FC<Props> = ({ tagName, isTagHovered, onTagsUpdat const updateTagsHook = useUpdateTags(); const bulkUpdateTags = updateTagsHook.bulkUpdateTags; - const TAGS_QUERY = 'tags:{name}'; + const TAGS_QUERY = 'tags:"{name}"'; const handleRename = (newName?: string) => { if (newName === tagName || !newName) { @@ -127,7 +128,7 @@ export const TagOptions: React.FC<Props> = ({ tagName, isTagHovered, onTagsUpdat }} onChange={(e: ChangeEvent<HTMLInputElement>) => { const newName = e.currentTarget.value; - setUpdatedName(newName); + setUpdatedName(sanitizeTag(newName)); }} /> </EuiFlexItem> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx index 4467f8413d583..75e829d12b92e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.test.tsx @@ -103,6 +103,46 @@ describe('TagsAddRemove', () => { ); }); + it('should add new tag by removing special chars', () => { + const result = renderComponent('agent1'); + const searchInput = result.getByRole('combobox'); + + fireEvent.input(searchInput, { + target: { value: 'Tag-123: _myTag"' }, + }); + + fireEvent.click(result.getAllByText('Create a new tag "Tag-123 _myTag"')[0].closest('button')!); + + expect(mockUpdateTags).toHaveBeenCalledWith( + 'agent1', + ['tag1', 'Tag-123 _myTag'], + expect.anything(), + 'Tag created', + 'Tag creation failed' + ); + }); + + it('should limit new tag to 20 length', () => { + const result = renderComponent('agent1'); + const searchInput = result.getByRole('combobox'); + + fireEvent.input(searchInput, { + target: { value: '01234567890123456789123' }, + }); + + fireEvent.click( + result.getAllByText('Create a new tag "01234567890123456789"')[0].closest('button')! + ); + + expect(mockUpdateTags).toHaveBeenCalledWith( + 'agent1', + ['tag1', '01234567890123456789'], + expect.anything(), + 'Tag created', + 'Tag creation failed' + ); + }); + it('should add selected tag when previously unselected - bulk selection', async () => { mockBulkUpdateTags.mockImplementation(() => { selectedTags = ['tag1', 'tag2']; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx index c60c6cec19fd9..c52ad66f2e9d0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/tags_add_remove.tsx @@ -22,6 +22,8 @@ import { i18n } from '@kbn/i18n'; import { useUpdateTags } from '../hooks'; +import { sanitizeTag } from '../utils'; + import { TagOptions } from './tag_options'; interface Props { @@ -138,8 +140,9 @@ export const TagsAddRemove: React.FC<Props> = ({ defaultMessage: 'Find or create label...', }), onChange: (value: string) => { - setSearchValue(value); + setSearchValue(sanitizeTag(value)); }, + value: searchValue ?? '', }} options={labels} renderOption={renderOption} @@ -162,14 +165,20 @@ export const TagsAddRemove: React.FC<Props> = ({ ); }} > - <EuiIcon type="plus" />{' '} - <FormattedMessage - id="xpack.fleet.tagsAddRemove.createText" - defaultMessage='Create a new tag "{name}"' - values={{ - name: searchValue, - }} - /> + <EuiFlexGroup alignItems="center" gutterSize="s"> + <EuiFlexItem grow={false}> + <EuiIcon type="plus" /> + </EuiFlexItem> + <EuiFlexItem> + <FormattedMessage + id="xpack.fleet.tagsAddRemove.createText" + defaultMessage='Create a new tag "{name}"' + values={{ + name: searchValue, + }} + /> + </EuiFlexItem> + </EuiFlexGroup> </EuiButtonEmpty> } > diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/index.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/index.ts index 192b4a5593b34..a269d6a68c689 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/index.ts @@ -7,3 +7,4 @@ export * from './truncate_tag'; export * from './get_common_tags'; +export * from './sanitize_tag'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.test.ts new file mode 100644 index 0000000000000..8e07fd8e04b99 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { sanitizeTag } from './sanitize_tag'; + +describe('sanitizeTag', () => { + it('should remove special characters from tag name', () => { + expect(sanitizeTag('Tag-123: []"\'#$%^&*__')).toEqual('Tag-123 __'); + }); + + it('should limit tag to 20 length', () => { + expect(sanitizeTag('aaaa aaaa aaaa aaaa bbb')).toEqual('aaaa aaaa aaaa aaaa '); + }); + + it('should do nothing for empty tag', () => { + expect(sanitizeTag('')).toEqual(''); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.ts new file mode 100644 index 0000000000000..d0b00ea9bed19 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/utils/sanitize_tag.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function sanitizeTag(tag: string): string { + return tag.replace(/[^a-zA-Z0-9 \-_]/g, '').slice(0, 20); +} From 6c08eacdc0cf23a9ec645afec300b926cc2028ba Mon Sep 17 00:00:00 2001 From: Nick Peihl <nick.peihl@elastic.co> Date: Fri, 8 Jul 2022 12:57:16 -0400 Subject: [PATCH 07/21] [File Upload] [Maps] Reduce precision of coordinates for geo imports (#135133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reduce precision of coordinates for geo imports * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Revert to jsts@1.6.2 The jsts library does not transpile modules since 2.0. So it is not currently possible to use the newer library. * Fix yarn lockfile * Fix test Test runs on features, not feature collections. 😬 Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../geo/geojson_clean_and_validate.js | 11 +++++- .../geo/geojson_clean_and_validate.test.js | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.js b/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.js index 17b4f94b52d5f..e16374d851de8 100644 --- a/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.js +++ b/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.js @@ -8,6 +8,13 @@ import * as jsts from 'jsts'; import rewind from '@mapbox/geojson-rewind'; +// The GeoJSON specification suggests limiting coordinate precision to six decimal places +// See https://datatracker.ietf.org/doc/html/rfc7946#section-11.2 +// We can enforce rounding to six decimal places by setting the PrecisionModel scale +// scale = 10^n where n = maximum number of decimal places +const precisionModel = new jsts.geom.PrecisionModel(Math.pow(10, 6)); +const geometryPrecisionReducer = new jsts.precision.GeometryPrecisionReducer(precisionModel); +geometryPrecisionReducer.setChangePrecisionModel(true); const geoJSONReader = new jsts.io.GeoJSONReader(); const geoJSONWriter = new jsts.io.GeoJSONWriter(); @@ -36,6 +43,8 @@ export function cleanGeometry({ geometry }) { if (!geometry) { return null; } - const geometryToWrite = geometry.isSimple() || geometry.isValid() ? geometry : geometry.buffer(0); + + // GeometryPrecisionReducer will automatically clean invalid geometries + const geometryToWrite = geometryPrecisionReducer.reduce(geometry); return geoJSONWriter.write(geometryToWrite); } diff --git a/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.test.js b/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.test.js index 0f8d126251dfb..8c9000e66e811 100644 --- a/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.test.js +++ b/x-pack/plugins/file_upload/public/importer/geo/geojson_clean_and_validate.test.js @@ -102,6 +102,44 @@ describe('geo_json_clean_and_validate', () => { }); }); + it('should reduce coordinate precision', () => { + const ludicrousPrecisionGeoJson = { + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [108.28125, 61.77312286453146], + [72.0703125, 46.31658418182218], + [99.49218749999999, 22.917922936146045], + [133.2421875, 27.059125784374068], + [139.5703125, 52.908902047770255], + [108.28125, 61.77312286453146], + ], + ], + }, + }; + + expect(geoJsonCleanAndValidate(ludicrousPrecisionGeoJson)).toEqual({ + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [108.28125, 61.773123], + [72.070313, 46.316584], + [99.492187, 22.917923], + [133.242188, 27.059126], + [139.570313, 52.908902], + [108.28125, 61.773123], + ], + ], + }, + }); + }); + it('should reverse counter-clockwise winding order', () => { const counterClockwiseGeoJson = { type: 'Feature', From 87ac0fd2feb30f6d659a9aa023f816fdb0fd9b3a Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski <jon@elastic.co> Date: Fri, 8 Jul 2022 12:02:59 -0500 Subject: [PATCH 08/21] [docker] Add ubi9 image (#135868) * [docker] Add ubi9 image * update artifacts tests * cleanup * fixes * formatting --- .buildkite/pipelines/artifacts.yml | 12 +++++++++++- .buildkite/pipelines/docker_context.yml | 11 ----------- .../scripts/steps/artifacts/docker_context.sh | 4 +++- .../tasks/os_packages/create_os_package_tasks.ts | 16 +++++++++++++--- .../tasks/os_packages/docker_generator/run.ts | 9 +++++---- .../docker_generator/template_context.ts | 2 +- .../templates/dockerfile.template.ts | 4 ++-- 7 files changed, 35 insertions(+), 23 deletions(-) delete mode 100644 .buildkite/pipelines/docker_context.yml diff --git a/.buildkite/pipelines/artifacts.yml b/.buildkite/pipelines/artifacts.yml index bfe5fe190ea16..8e08c736694e8 100644 --- a/.buildkite/pipelines/artifacts.yml +++ b/.buildkite/pipelines/artifacts.yml @@ -51,7 +51,17 @@ steps: - exit_status: '*' limit: 1 - - command: KIBANA_DOCKER_CONTEXT=ubi .buildkite/scripts/steps/artifacts/docker_context.sh + - command: KIBANA_DOCKER_CONTEXT=ubi8 .buildkite/scripts/steps/artifacts/docker_context.sh + label: 'Docker Context Verification' + agents: + queue: n2-2 + timeout_in_minutes: 30 + retry: + automatic: + - exit_status: '*' + limit: 1 + + - command: KIBANA_DOCKER_CONTEXT=ubi9 .buildkite/scripts/steps/artifacts/docker_context.sh label: 'Docker Context Verification' agents: queue: n2-2 diff --git a/.buildkite/pipelines/docker_context.yml b/.buildkite/pipelines/docker_context.yml deleted file mode 100644 index f85b895e4780b..0000000000000 --- a/.buildkite/pipelines/docker_context.yml +++ /dev/null @@ -1,11 +0,0 @@ - steps: - - command: .buildkite/scripts/steps/docker_context/build.sh - label: 'Docker Build Context' - agents: - queue: n2-4 - timeout_in_minutes: 30 - key: build-docker-context - retry: - automatic: - - exit_status: '*' - limit: 1 \ No newline at end of file diff --git a/.buildkite/scripts/steps/artifacts/docker_context.sh b/.buildkite/scripts/steps/artifacts/docker_context.sh index 1195d7ad5dc38..86c4361173a08 100755 --- a/.buildkite/scripts/steps/artifacts/docker_context.sh +++ b/.buildkite/scripts/steps/artifacts/docker_context.sh @@ -19,8 +19,10 @@ if [[ "$KIBANA_DOCKER_CONTEXT" == "default" ]]; then DOCKER_CONTEXT_FILE="kibana-$FULL_VERSION-docker-build-context.tar.gz" elif [[ "$KIBANA_DOCKER_CONTEXT" == "cloud" ]]; then DOCKER_CONTEXT_FILE="kibana-cloud-$FULL_VERSION-docker-build-context.tar.gz" -elif [[ "$KIBANA_DOCKER_CONTEXT" == "ubi" ]]; then +elif [[ "$KIBANA_DOCKER_CONTEXT" == "ubi8" ]]; then DOCKER_CONTEXT_FILE="kibana-ubi8-$FULL_VERSION-docker-build-context.tar.gz" +elif [[ "$KIBANA_DOCKER_CONTEXT" == "ubi9" ]]; then + DOCKER_CONTEXT_FILE="kibana-ubi9-$FULL_VERSION-docker-build-context.tar.gz" fi tar -xf "target/$DOCKER_CONTEXT_FILE" -C "$DOCKER_BUILD_FOLDER" diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index 49967feb214d6..69a272d39f4a0 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -86,7 +86,13 @@ export const CreateDockerUBI: Task = { async run(config, log, build) { await runDockerGenerator(config, log, build, { architecture: 'x64', - baseImage: 'ubi', + baseImage: 'ubi8', + context: false, + image: true, + }); + await runDockerGenerator(config, log, build, { + architecture: 'x64', + baseImage: 'ubi9', context: false, image: true, }); @@ -124,9 +130,13 @@ export const CreateDockerContexts: Task = { image: false, dockerBuildDate, }); - await runDockerGenerator(config, log, build, { - baseImage: 'ubi', + baseImage: 'ubi8', + context: true, + image: false, + }); + await runDockerGenerator(config, log, build, { + baseImage: 'ubi9', context: true, image: false, }); diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index d8b604f00b46e..34b58e7513bb1 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -29,7 +29,7 @@ export async function runDockerGenerator( build: Build, flags: { architecture?: string; - baseImage: 'none' | 'ubi' | 'ubuntu'; + baseImage: 'none' | 'ubi9' | 'ubi8' | 'ubuntu'; context: boolean; image: boolean; ironbank?: boolean; @@ -39,11 +39,12 @@ export async function runDockerGenerator( ) { let baseImageName = ''; if (flags.baseImage === 'ubuntu') baseImageName = 'ubuntu:20.04'; - if (flags.baseImage === 'ubi') baseImageName = 'docker.elastic.co/ubi8/ubi-minimal:latest'; - const ubiVersionTag = 'ubi8'; + if (flags.baseImage === 'ubi8') baseImageName = 'docker.elastic.co/ubi8/ubi-minimal:latest'; + if (flags.baseImage === 'ubi9') baseImageName = 'docker.elastic.co/ubi9/ubi-minimal:latest'; let imageFlavor = ''; - if (flags.baseImage === 'ubi') imageFlavor += `-${ubiVersionTag}`; + if (flags.baseImage === 'ubi8') imageFlavor += `-ubi8`; + if (flags.baseImage === 'ubi9') imageFlavor += `-ubi9`; if (flags.ironbank) imageFlavor += '-ironbank'; if (flags.cloud) imageFlavor += '-cloud'; diff --git a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts index 32a551820a05b..da2d7422a03ea 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/template_context.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/template_context.ts @@ -23,7 +23,7 @@ export interface TemplateContext { dockerBuildDate: string; usePublicArtifact?: boolean; publicArtifactSubdomain: string; - baseImage: 'none' | 'ubi' | 'ubuntu'; + baseImage: 'none' | 'ubi8' | 'ubi9' | 'ubuntu'; baseImageName: string; cloud?: boolean; metricbeatTarball?: string; diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts index 63b04ed6f70b0..ca597e5c38941 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts @@ -16,8 +16,8 @@ function generator(options: TemplateContext) { const dir = options.ironbank ? 'ironbank' : 'base'; const template = readFileSync(resolve(__dirname, dir, './Dockerfile')); return Mustache.render(template.toString(), { - packageManager: options.baseImage === 'ubi' ? 'microdnf' : 'apt-get', - ubi: options.baseImage === 'ubi', + packageManager: options.baseImage.includes('ubi') ? 'microdnf' : 'apt-get', + ubi: options.baseImage.includes('ubi'), ubuntu: options.baseImage === 'ubuntu', ...options, }); From 3891aeb95fb030348807dee583e0c217a9083b7d Mon Sep 17 00:00:00 2001 From: Andrew Tate <drewctate@gmail.com> Date: Fri, 8 Jul 2022 12:07:43 -0500 Subject: [PATCH 09/21] [Chart expressions] new metric vis expression (#135461) --- .i18nrc.json | 15 +- docs/developer/plugin-list.asciidoc | 4 + packages/kbn-optimizer/limits.yml | 1 + .../expression_legacy_metric/.i18nrc.json | 6 + .../.storybook/main.js | 26 + .../expression_legacy_metric/README.md | 9 + .../common/constants.ts | 14 + .../metric_vis_function.test.ts.snap | 2 +- .../common/expression_functions/index.ts | 9 + .../metric_vis_function.test.ts | 0 .../metric_vis_function.ts | 206 ++++ .../expression_legacy_metric/common/index.ts | 25 + .../common/types/expression_functions.ts | 48 + .../common/types/expression_renderers.ts | 59 ++ .../common/types/index.ts | 10 + .../expression_legacy_metric/jest.config.js | 19 + .../expression_legacy_metric/kibana.json | 21 + .../public/__mocks__/format_service.ts | 0 .../public/__mocks__/palette_service.ts | 0 .../public/__mocks__/services.ts | 0 .../__stories__/metric_renderer.stories.tsx | 0 .../metric_component.test.tsx.snap | 2 +- .../with_auto_scale.test.tsx.snap | 0 .../public/components/metric.scss | 17 +- .../components/metric_component.test.tsx | 0 .../public/components/metric_component.tsx | 0 .../public/components/metric_value.test.tsx | 6 +- .../public/components/metric_value.tsx | 12 +- .../components/with_auto_scale.styles.ts | 0 .../components/with_auto_scale.test.tsx | 0 .../public/components/with_auto_scale.tsx | 0 .../public/expression_renderers/index.ts | 9 + .../metric_vis_renderer.tsx | 91 ++ .../public/format_service.ts | 0 .../expression_legacy_metric/public/index.ts | 13 + .../expression_legacy_metric/public/plugin.ts | 41 + .../public/services/format_service.ts | 13 + .../public/services/index.ts | 10 + .../public/services/palette_service.ts | 13 + .../public/utils/format.ts | 21 + .../public/utils/index.ts | 9 + .../public/utils/palette.ts | 41 + .../expression_legacy_metric/server/index.ts | 13 + .../expression_legacy_metric/server/plugin.ts | 40 + .../expression_legacy_metric/tsconfig.json | 23 + .../metric_vis_function.ts | 197 ++-- .../common/types/expression_functions.ts | 25 +- .../common/types/expression_renderers.ts | 34 +- .../expression_metric/kibana.json | 10 +- .../public/__mocks__/theme_service.ts | 13 + .../public/components/currency_codes.test.ts | 33 + .../public/components/currency_codes.ts | 46 + .../public/components/metric_vis.test.tsx | 905 ++++++++++++++++++ .../public/components/metric_vis.tsx | 240 +++++ .../metric_vis_renderer.tsx | 62 +- .../expression_metric/public/plugin.ts | 12 +- .../public/services/index.ts | 2 + .../public/services/theme_service.ts | 13 + .../public/services/ui_settings.ts | 13 + .../common/constants/base_formatters.ts | 2 + .../common/converters/currency.test.ts | 31 + .../common/converters/currency.ts | 23 + .../field_formats/common/converters/index.ts | 1 + src/plugins/field_formats/common/types.ts | 1 + .../public/__snapshots__/to_ast.test.ts.snap | 4 +- src/plugins/vis_types/metric/public/to_ast.ts | 2 +- .../page_objects/visualize_chart_page.ts | 2 +- .../page_objects/visualize_editor_page.ts | 2 +- .../services/dashboard/expectations.ts | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../snapshots/baseline/metric_empty_data.json | 2 +- .../baseline/metric_invalid_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/session/combined_test3.json | 2 +- .../snapshots/session/final_output_test.json | 2 +- .../snapshots/session/metric_all_data.json | 2 +- .../snapshots/session/metric_empty_data.json | 2 +- .../session/metric_multi_metric_data.json | 2 +- .../session/metric_percentage_mode.json | 2 +- .../session/metric_single_metric_data.json | 2 +- .../snapshots/session/partial_test_2.json | 2 +- .../snapshots/session/step_output_test3.json | 2 +- .../test_suites/run_pipeline/basic.ts | 4 +- .../test_suites/run_pipeline/metric.ts | 14 +- tsconfig.base.json | 2 + .../canvas_plugin_src/elements/index.ts | 3 +- .../elements/metric_vis/index.ts | 12 +- .../elements/metric_vis_legacy/index.ts | 21 + .../uis/models/metric_vis.ts | 2 +- .../i18n/elements/element_strings.test.ts | 4 +- .../canvas/i18n/elements/element_strings.ts | 10 +- .../visualization.test.ts | 2 +- .../metric_visualization/visualization.tsx | 2 +- .../translations/translations/fr-FR.json | 12 - .../translations/translations/ja-JP.json | 12 - .../translations/translations/zh-CN.json | 12 - .../time_to_visualize_security.ts | 2 +- .../apps/lens/group1/persistent_context.ts | 4 +- .../apps/lens/group2/add_to_dashboard.ts | 12 +- .../apps/lens/group2/epoch_millis.ts | 4 +- .../functional/apps/lens/group3/chart_data.ts | 2 +- .../functional/apps/lens/group3/formula.ts | 2 +- .../functional/apps/lens/group3/metrics.ts | 6 +- .../functional/apps/lens/group3/rollup.ts | 4 +- .../apps/lens/group3/tsvb_open_in_lens.ts | 2 +- .../data_visualizer/index_data_visualizer.ts | 4 +- 112 files changed, 2372 insertions(+), 343 deletions(-) create mode 100755 src/plugins/chart_expressions/expression_legacy_metric/.i18nrc.json create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/.storybook/main.js create mode 100755 src/plugins/chart_expressions/expression_legacy_metric/README.md create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/common/constants.ts rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap (99%) create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/index.ts rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/common/expression_functions/metric_vis_function.test.ts (100%) create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/metric_vis_function.ts create mode 100755 src/plugins/chart_expressions/expression_legacy_metric/common/index.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_functions.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_renderers.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/common/types/index.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/jest.config.js create mode 100755 src/plugins/chart_expressions/expression_legacy_metric/kibana.json rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/__mocks__/format_service.ts (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/__mocks__/palette_service.ts (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/__mocks__/services.ts (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/__stories__/metric_renderer.stories.tsx (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/__snapshots__/metric_component.test.tsx.snap (99%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/__snapshots__/with_auto_scale.test.tsx.snap (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/metric.scss (81%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/metric_component.test.tsx (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/metric_component.tsx (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/metric_value.test.tsx (92%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/metric_value.tsx (88%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/with_auto_scale.styles.ts (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/with_auto_scale.test.tsx (100%) rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/components/with_auto_scale.tsx (100%) create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/index.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/metric_vis_renderer.tsx rename src/plugins/chart_expressions/{expression_metric => expression_legacy_metric}/public/format_service.ts (100%) create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/index.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/plugin.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/services/format_service.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/services/index.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/services/palette_service.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/utils/format.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/utils/index.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/public/utils/palette.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/server/index.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/server/plugin.ts create mode 100644 src/plugins/chart_expressions/expression_legacy_metric/tsconfig.json create mode 100644 src/plugins/chart_expressions/expression_metric/public/__mocks__/theme_service.ts create mode 100644 src/plugins/chart_expressions/expression_metric/public/components/currency_codes.test.ts create mode 100644 src/plugins/chart_expressions/expression_metric/public/components/currency_codes.ts create mode 100644 src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx create mode 100644 src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx create mode 100644 src/plugins/chart_expressions/expression_metric/public/services/theme_service.ts create mode 100644 src/plugins/chart_expressions/expression_metric/public/services/ui_settings.ts create mode 100644 src/plugins/field_formats/common/converters/currency.test.ts create mode 100644 src/plugins/field_formats/common/converters/currency.ts create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis_legacy/index.ts diff --git a/.i18nrc.json b/.i18nrc.json index 412d16930c9ac..073a413fabf80 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -29,6 +29,7 @@ "expressionImage": "src/plugins/expression_image", "expressionMetric": "src/plugins/expression_metric", "expressionMetricVis": "src/plugins/chart_expressions/expression_metric", + "expressionLegacyMetricVis": "src/plugins/chart_expressions/expression_legacy_metric", "expressionPartitionVis": "src/plugins/chart_expressions/expression_partition_vis", "expressionXY": "src/plugins/chart_expressions/expression_xy", "expressionRepeatImage": "src/plugins/expression_repeat_image", @@ -57,10 +58,7 @@ "kibana-react": "src/plugins/kibana_react", "kibanaOverview": "src/plugins/kibana_overview", "lists": "packages/kbn-securitysolution-list-utils/src", - "management": [ - "src/legacy/core_plugins/management", - "src/plugins/management" - ], + "management": ["src/legacy/core_plugins/management", "src/plugins/management"], "monaco": "packages/kbn-monaco/src", "navigation": "src/plugins/navigation", "newsfeed": "src/plugins/newsfeed", @@ -74,13 +72,8 @@ "sharedUXPackages": "packages/shared-ux", "coloring": "packages/kbn-coloring/src", "statusPage": "src/legacy/core_plugins/status_page", - "telemetry": [ - "src/plugins/telemetry", - "src/plugins/telemetry_management_section" - ], - "timelion": [ - "src/plugins/vis_types/timelion" - ], + "telemetry": ["src/plugins/telemetry", "src/plugins/telemetry_management_section"], + "timelion": ["src/plugins/vis_types/timelion"], "uiActions": "src/plugins/ui_actions", "uiActionsEnhanced": "src/plugins/ui_actions_enhanced", "uiActionsExamples": "examples/ui_action_examples", diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index c939ab2dcf690..041c0cee57359 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -114,6 +114,10 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a |Expression Image plugin adds an image renderer to the expression plugin. The renderer will display the given image. +|{kib-repo}blob/{branch}/src/plugins/chart_expressions/expression_legacy_metric/README.md[expressionLegacyMetricVis] +|Expression MetricVis plugin adds a metric renderer and function to the expression plugin. The renderer will display the metric chart. + + |{kib-repo}blob/{branch}/src/plugins/expression_metric/README.md[expressionMetric] |Expression Metric plugin adds a metric renderer and function to the expression plugin. diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 0dafe3c51b77e..0dc1e1ee4675e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -96,6 +96,7 @@ pageLoadAssetSize: securitySolution: 273763 customIntegrations: 28810 expressionMetricVis: 23121 + expressionLegacyMetricVis: 23121 expressionHeatmap: 27505 visTypeMetric: 23332 bfetch: 22837 diff --git a/src/plugins/chart_expressions/expression_legacy_metric/.i18nrc.json b/src/plugins/chart_expressions/expression_legacy_metric/.i18nrc.json new file mode 100755 index 0000000000000..28e2d09a1a433 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/.i18nrc.json @@ -0,0 +1,6 @@ +{ + "prefix": "expressionLegacyMetricVis", + "paths": { + "expressionLegacyMetricVis": "." + } +} diff --git a/src/plugins/chart_expressions/expression_legacy_metric/.storybook/main.js b/src/plugins/chart_expressions/expression_legacy_metric/.storybook/main.js new file mode 100644 index 0000000000000..80e65c9e371f0 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/.storybook/main.js @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { defaultConfig } from '@kbn/storybook'; +import webpackMerge from 'webpack-merge'; +import { resolve } from 'path'; + +const mockConfig = { + resolve: { + alias: { + '../../../expression_legacy_metric/public/services': resolve( + __dirname, + '../public/__mocks__/services.ts' + ), + }, + }, +}; + +module.exports = { + ...defaultConfig, + webpackFinal: (config) => webpackMerge(config, mockConfig), +}; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/README.md b/src/plugins/chart_expressions/expression_legacy_metric/README.md new file mode 100755 index 0000000000000..07b830feae67d --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/README.md @@ -0,0 +1,9 @@ +# expressionLegacyMetricVis + +Expression MetricVis plugin adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart. + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/src/plugins/chart_expressions/expression_legacy_metric/common/constants.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/constants.ts new file mode 100644 index 0000000000000..54cfe41f2a3b7 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/constants.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const EXPRESSION_METRIC_NAME = 'legacyMetricVis'; + +export const LabelPosition = { + BOTTOM: 'bottom', + TOP: 'top', +} as const; diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap b/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap similarity index 99% rename from src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap rename to src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap index 44cc3fee09b1f..9a7a7d5a5035c 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/__snapshots__/metric_vis_function.test.ts.snap @@ -23,7 +23,7 @@ Object { exports[`interpreter/functions#metric returns an object with the correct structure 1`] = ` Object { - "as": "metricVis", + "as": "legacyMetricVis", "type": "render", "value": Object { "visConfig": Object { diff --git a/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/index.ts new file mode 100644 index 0000000000000..5eccaa62fe464 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { metricVisFunction } from './metric_vis_function'; diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/metric_vis_function.test.ts similarity index 100% rename from src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.test.ts rename to src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/metric_vis_function.test.ts diff --git a/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/metric_vis_function.ts new file mode 100644 index 0000000000000..8ec638d139bff --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/expression_functions/metric_vis_function.ts @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import { + prepareLogTable, + Dimension, + validateAccessor, +} from '@kbn/visualizations-plugin/common/utils'; +import { ColorMode } from '@kbn/charts-plugin/common'; +import { visType } from '../types'; +import { MetricVisExpressionFunctionDefinition } from '../types'; +import { EXPRESSION_METRIC_NAME, LabelPosition } from '../constants'; + +const errors = { + severalMetricsAndColorFullBackgroundSpecifiedError: () => + i18n.translate( + 'expressionLegacyMetricVis.function.errors.severalMetricsAndColorFullBackgroundSpecified', + { + defaultMessage: + 'Full background coloring cannot be applied to a visualization with multiple metrics.', + } + ), + splitByBucketAndColorFullBackgroundSpecifiedError: () => + i18n.translate( + 'expressionLegacyMetricVis.function.errors.splitByBucketAndColorFullBackgroundSpecified', + { + defaultMessage: + 'Full background coloring cannot be applied to visualizations that have a bucket specified.', + } + ), +}; + +export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ + name: EXPRESSION_METRIC_NAME, + type: 'render', + inputTypes: ['datatable'], + help: i18n.translate('expressionLegacyMetricVis.function.help', { + defaultMessage: 'Metric visualization', + }), + args: { + percentageMode: { + types: ['boolean'], + default: false, + help: i18n.translate('expressionLegacyMetricVis.function.percentageMode.help', { + defaultMessage: 'Shows metric in percentage mode. Requires colorRange to be set.', + }), + }, + colorMode: { + types: ['string'], + default: `"${ColorMode.None}"`, + options: [ColorMode.None, ColorMode.Labels, ColorMode.Background], + help: i18n.translate('expressionLegacyMetricVis.function.colorMode.help', { + defaultMessage: 'Which part of metric to color', + }), + strict: true, + }, + colorFullBackground: { + types: ['boolean'], + default: false, + help: i18n.translate('expressionLegacyMetricVis.function.colorFullBackground.help', { + defaultMessage: 'Applies the selected background color to the full visualization container', + }), + }, + palette: { + types: ['palette'], + help: i18n.translate('expressionLegacyMetricVis.function.palette.help', { + defaultMessage: 'Provides colors for the values, based on the bounds.', + }), + }, + showLabels: { + types: ['boolean'], + default: true, + help: i18n.translate('expressionLegacyMetricVis.function.showLabels.help', { + defaultMessage: 'Shows labels under the metric values.', + }), + }, + font: { + types: ['style'], + help: i18n.translate('expressionLegacyMetricVis.function.font.help', { + defaultMessage: 'Font settings.', + }), + default: `{font size=60 align="center"}`, + }, + labelFont: { + types: ['style'], + help: i18n.translate('expressionLegacyMetricVis.function.labelFont.help', { + defaultMessage: 'Label font settings.', + }), + default: `{font size=24 align="center"}`, + }, + labelPosition: { + types: ['string'], + options: [LabelPosition.BOTTOM, LabelPosition.TOP], + help: i18n.translate('expressionLegacyMetricVis.function.labelPosition.help', { + defaultMessage: 'Label position', + }), + default: LabelPosition.BOTTOM, + strict: true, + }, + metric: { + types: ['string', 'vis_dimension'], + help: i18n.translate('expressionLegacyMetricVis.function.metric.help', { + defaultMessage: 'metric dimension configuration', + }), + required: true, + multi: true, + }, + bucket: { + types: ['string', 'vis_dimension'], + help: i18n.translate('expressionLegacyMetricVis.function.bucket.help', { + defaultMessage: 'bucket dimension configuration', + }), + }, + autoScale: { + types: ['boolean'], + help: i18n.translate('expressionLegacyMetricVis.function.autoScale.help', { + defaultMessage: 'Enable auto scale', + }), + required: false, + }, + }, + fn(input, args, handlers) { + if (args.percentageMode && !args.palette?.params) { + throw new Error('Palette must be provided when using percentageMode'); + } + + // currently we can allow colorize full container only for one metric + if (args.colorFullBackground) { + if (args.bucket) { + throw new Error(errors.splitByBucketAndColorFullBackgroundSpecifiedError()); + } + + if (args.metric.length > 1 || input.rows.length > 1) { + throw new Error(errors.severalMetricsAndColorFullBackgroundSpecifiedError()); + } + } + + args.metric.forEach((metric) => validateAccessor(metric, input.columns)); + validateAccessor(args.bucket, input.columns); + + if (handlers?.inspectorAdapters?.tables) { + handlers.inspectorAdapters.tables.reset(); + handlers.inspectorAdapters.tables.allowCsvExport = true; + + const argsTable: Dimension[] = [ + [ + args.metric, + i18n.translate('expressionLegacyMetricVis.function.dimension.metric', { + defaultMessage: 'Metric', + }), + ], + ]; + if (args.bucket) { + argsTable.push([ + [args.bucket], + i18n.translate('expressionLegacyMetricVis.function.dimension.splitGroup', { + defaultMessage: 'Split group', + }), + ]); + } + const logTable = prepareLogTable(input, argsTable, true); + handlers.inspectorAdapters.tables.logDatatable('default', logTable); + } + + return { + type: 'render', + as: EXPRESSION_METRIC_NAME, + value: { + visData: input, + visType, + visConfig: { + metric: { + palette: args.palette?.params, + percentageMode: args.percentageMode, + metricColorMode: args.colorMode, + labels: { + show: args.showLabels, + position: args.labelPosition, + style: { + ...args.labelFont, + }, + }, + colorFullBackground: args.colorFullBackground, + style: { + bgColor: args.colorMode === ColorMode.Background, + labelColor: args.colorMode === ColorMode.Labels, + ...args.font, + }, + autoScale: args.autoScale, + }, + dimensions: { + metrics: args.metric, + ...(args.bucket ? { bucket: args.bucket } : {}), + }, + }, + }, + }; + }, +}); diff --git a/src/plugins/chart_expressions/expression_legacy_metric/common/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/index.ts new file mode 100755 index 0000000000000..34cbdb6745312 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const PLUGIN_ID = 'expressionLegacyMetricVis'; +export const PLUGIN_NAME = 'expressionLegacyMetricVis'; + +export type { + MetricArguments, + MetricInput, + MetricVisRenderConfig, + MetricVisExpressionFunctionDefinition, + DimensionsVisParam, + MetricVisParam, + VisParams, + MetricOptions, +} from './types'; + +export { metricVisFunction } from './expression_functions'; + +export { EXPRESSION_METRIC_NAME } from './constants'; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_functions.ts new file mode 100644 index 0000000000000..5ad540fc579f6 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_functions.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { PaletteOutput } from '@kbn/coloring'; +import { + Datatable, + ExpressionFunctionDefinition, + ExpressionValueRender, + Style, +} from '@kbn/expressions-plugin'; +import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; +import { ColorMode, CustomPaletteState } from '@kbn/charts-plugin/common'; +import { VisParams, visType, LabelPositionType } from './expression_renderers'; +import { EXPRESSION_METRIC_NAME } from '../constants'; + +export interface MetricArguments { + percentageMode: boolean; + colorMode: ColorMode; + showLabels: boolean; + palette?: PaletteOutput<CustomPaletteState>; + font: Style; + labelFont: Style; + labelPosition: LabelPositionType; + metric: Array<ExpressionValueVisDimension | string>; + bucket?: ExpressionValueVisDimension | string; + colorFullBackground: boolean; + autoScale?: boolean; +} + +export type MetricInput = Datatable; + +export interface MetricVisRenderConfig { + visType: typeof visType; + visData: Datatable; + visConfig: Pick<VisParams, 'metric' | 'dimensions'>; +} + +export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition< + typeof EXPRESSION_METRIC_NAME, + MetricInput, + MetricArguments, + ExpressionValueRender<MetricVisRenderConfig> +>; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_renderers.ts new file mode 100644 index 0000000000000..8c370480a7be9 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/types/expression_renderers.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { $Values } from '@kbn/utility-types'; +import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; +import { + ColorMode, + Labels, + CustomPaletteState, + Style as ChartStyle, +} from '@kbn/charts-plugin/common'; +import { Style } from '@kbn/expressions-plugin/common'; +import { LabelPosition } from '../constants'; + +export const visType = 'metric'; + +export interface DimensionsVisParam { + metrics: Array<ExpressionValueVisDimension | string>; + bucket?: ExpressionValueVisDimension | string; +} + +export type LabelPositionType = $Values<typeof LabelPosition>; + +export type MetricStyle = Style & Pick<ChartStyle, 'bgColor' | 'labelColor'>; + +export type LabelsConfig = Labels & { style: Style; position: LabelPositionType }; +export interface MetricVisParam { + percentageMode: boolean; + percentageFormatPattern?: string; + metricColorMode: ColorMode; + palette?: CustomPaletteState; + labels: LabelsConfig; + style: MetricStyle; + colorFullBackground: boolean; + autoScale?: boolean; +} + +export interface VisParams { + addTooltip: boolean; + addLegend: boolean; + dimensions: DimensionsVisParam; + metric: MetricVisParam; + type: typeof visType; +} + +export interface MetricOptions { + value: string; + label: string; + color?: string; + bgColor?: string; + lightText: boolean; + colIndex: number; + rowIndex: number; +} diff --git a/src/plugins/chart_expressions/expression_legacy_metric/common/types/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/common/types/index.ts new file mode 100644 index 0000000000000..9c50bfab1305d --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/common/types/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './expression_functions'; +export * from './expression_renderers'; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/jest.config.js b/src/plugins/chart_expressions/expression_legacy_metric/jest.config.js new file mode 100644 index 0000000000000..6b649ca8abadc --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/jest.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../', + roots: ['<rootDir>/src/plugins/chart_expressions/expression_legacy_metric'], + coverageDirectory: + '<rootDir>/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_legacy_metric', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '<rootDir>/src/plugins/chart_expressions/expression_legacy_metric/{common,public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/kibana.json b/src/plugins/chart_expressions/expression_legacy_metric/kibana.json new file mode 100755 index 0000000000000..0c3489ddc55d1 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/kibana.json @@ -0,0 +1,21 @@ +{ + "id": "expressionLegacyMetricVis", + "version": "1.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" + }, + "description": "Adds a `metric` renderer and function to the expression plugin. The renderer will display the `legacy metric` chart.", + "server": true, + "ui": true, + "requiredPlugins": [ + "expressions", + "fieldFormats", + "charts", + "visualizations", + "presentationUtil" + ], + "requiredBundles": ["kibanaUtils", "kibanaReact"], + "optionalPlugins": [] +} diff --git a/src/plugins/chart_expressions/expression_metric/public/__mocks__/format_service.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/__mocks__/format_service.ts similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/__mocks__/format_service.ts rename to src/plugins/chart_expressions/expression_legacy_metric/public/__mocks__/format_service.ts diff --git a/src/plugins/chart_expressions/expression_metric/public/__mocks__/palette_service.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/__mocks__/palette_service.ts similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/__mocks__/palette_service.ts rename to src/plugins/chart_expressions/expression_legacy_metric/public/__mocks__/palette_service.ts diff --git a/src/plugins/chart_expressions/expression_metric/public/__mocks__/services.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/__mocks__/services.ts similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/__mocks__/services.ts rename to src/plugins/chart_expressions/expression_legacy_metric/public/__mocks__/services.ts diff --git a/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/__stories__/metric_renderer.stories.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx rename to src/plugins/chart_expressions/expression_legacy_metric/public/__stories__/metric_renderer.stories.tsx diff --git a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap b/src/plugins/chart_expressions/expression_legacy_metric/public/components/__snapshots__/metric_component.test.tsx.snap similarity index 99% rename from src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/__snapshots__/metric_component.test.tsx.snap index ac950f3f7f2c4..106d45bc4a87c 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/metric_component.test.tsx.snap +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/components/__snapshots__/metric_component.test.tsx.snap @@ -116,4 +116,4 @@ exports[`MetricVisComponent should render correct structure for single metric 1` } } /> -`; +`; \ No newline at end of file diff --git a/src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/with_auto_scale.test.tsx.snap b/src/plugins/chart_expressions/expression_legacy_metric/public/components/__snapshots__/with_auto_scale.test.tsx.snap similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/components/__snapshots__/with_auto_scale.test.tsx.snap rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/__snapshots__/with_auto_scale.test.tsx.snap diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric.scss b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric.scss similarity index 81% rename from src/plugins/chart_expressions/expression_metric/public/components/metric.scss rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/metric.scss index c99c191c57755..7adcb109bc931 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric.scss +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric.scss @@ -5,7 +5,7 @@ // mtrChart__legend--small // mtrChart__legend-isLoading -.mtrVis { +.legacyMtrVis { @include euiScrollBar; height: 100%; width: 100%; @@ -17,23 +17,23 @@ overflow: auto; } -.mtrVis__value { +.legacyMtrVis__value { @include euiTextTruncate; font-weight: $euiFontWeightBold; } -.mtrVis__container { +.legacyMtrVis__container { text-align: center; padding: $euiSize; display: flex; flex-direction: column; } -.mtrVis__container--light { +.legacyMtrVis__container--light { color: $euiColorEmptyShade; } -.mtrVis__container-isfull { +.legacyMtrVis__container-isfull { min-height: 100%; min-width: max-content; display: flex; @@ -43,13 +43,14 @@ flex: 1 0 100%; } -.mtrVis__container-isFilterable { +.legacyMtrVis__container-isFilterable { cursor: pointer; transition: transform $euiAnimSpeedNormal $euiAnimSlightResistance; transform: translate(0, 0); - &:hover, &:focus { + &:hover, + &:focus { box-shadow: none; transform: translate(0, -2px); } -} +} \ No newline at end of file diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_component.test.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/components/metric_component.test.tsx rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_component.test.tsx diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_component.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/components/metric_component.tsx rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_component.tsx diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.test.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_value.test.tsx similarity index 92% rename from src/plugins/chart_expressions/expression_metric/public/components/metric_value.test.tsx rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_value.test.tsx index fee24d8aa5e7f..0590faebe5f7d 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.test.tsx +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_value.test.tsx @@ -75,7 +75,7 @@ describe('MetricVisValue', () => { /> ); component.simulate('click'); - expect(component.find('.mtrVis__container-isfilterable')).toHaveLength(1); + expect(component.find('.legacyMtrVis__container-isfilterable')).toHaveLength(1); }); it('should not add -isfilterable class if onFilter is not provided', () => { @@ -88,7 +88,7 @@ describe('MetricVisValue', () => { /> ); component.simulate('click'); - expect(component.find('.mtrVis__container-isfilterable')).toHaveLength(0); + expect(component.find('.legacyMtrVis__container-isfilterable')).toHaveLength(0); }); it('should call onFilter callback if provided', () => { @@ -116,6 +116,6 @@ describe('MetricVisValue', () => { labelConfig={labelConfig} /> ); - expect(component.find('.mtrVis__container-isfull').exists()).toBe(true); + expect(component.find('.legacyMtrVis__container-isfull').exists()).toBe(true); }); }); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_value.tsx similarity index 88% rename from src/plugins/chart_expressions/expression_metric/public/components/metric_value.tsx rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_value.tsx index 40ba0eb081564..1f9192aedc872 100644 --- a/src/plugins/chart_expressions/expression_metric/public/components/metric_value.tsx +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/components/metric_value.tsx @@ -23,10 +23,10 @@ interface MetricVisValueProps { export const MetricVisValue = (props: MetricVisValueProps) => { const { style, metric, onFilter, labelConfig, colorFullBackground, autoScale } = props; - const containerClassName = classNames('mtrVis__container', { - 'mtrVis__container--light': metric.lightText, - 'mtrVis__container-isfilterable': onFilter, - 'mtrVis__container-isfull': !autoScale && colorFullBackground, + const containerClassName = classNames('legacyMtrVis__container', { + 'legacyMtrVis__container--light': metric.lightText, + 'legacyMtrVis__container-isfilterable': onFilter, + 'legacyMtrVis__container-isfull': !autoScale && colorFullBackground, }); useLayoutEffect(() => { @@ -41,7 +41,7 @@ export const MetricVisValue = (props: MetricVisValueProps) => { > <div data-test-subj="metric_value" - className="mtrVis__value" + className="legacyMtrVis__value" style={{ ...(style.spec as CSSProperties), ...(metric.color ? { color: metric.color } : {}), @@ -75,7 +75,7 @@ export const MetricVisValue = (props: MetricVisValueProps) => { <button style={{ display: 'block' }} onClick={() => onFilter()} - title={i18n.translate('expressionMetricVis.filterTitle', { + title={i18n.translate('expressionLegacyMetricVis.filterTitle', { defaultMessage: 'Click to filter by field', })} > diff --git a/src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.styles.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/components/with_auto_scale.styles.ts similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.styles.ts rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/with_auto_scale.styles.ts diff --git a/src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.test.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/components/with_auto_scale.test.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.test.tsx rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/with_auto_scale.test.tsx diff --git a/src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/components/with_auto_scale.tsx similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/components/with_auto_scale.tsx rename to src/plugins/chart_expressions/expression_legacy_metric/public/components/with_auto_scale.tsx diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/index.ts new file mode 100644 index 0000000000000..98a987db2fd43 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getMetricVisRenderer } from './metric_vis_renderer'; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/metric_vis_renderer.tsx b/src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/metric_vis_renderer.tsx new file mode 100644 index 0000000000000..b8a51b49b714a --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/expression_renderers/metric_vis_renderer.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { ThemeServiceStart } from '@kbn/core/public'; +import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; +import { + ExpressionValueVisDimension, + VisualizationContainer, +} from '@kbn/visualizations-plugin/public'; +import { + ExpressionRenderDefinition, + IInterpreterRenderHandlers, +} from '@kbn/expressions-plugin/common/expression_renderers'; +import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; +import { Datatable } from '@kbn/expressions-plugin'; +import { EXPRESSION_METRIC_NAME, MetricVisRenderConfig, VisParams } from '../../common'; + +// @ts-ignore +const MetricVisComponent = lazy(() => import('../components/metric_component')); + +async function metricFilterable( + dimensions: VisParams['dimensions'], + table: Datatable, + handlers: IInterpreterRenderHandlers +) { + return Promise.all( + dimensions.metrics.map(async (metric: string | ExpressionValueVisDimension) => { + const column = getColumnByAccessor(metric, table.columns); + const colIndex = table.columns.indexOf(column!); + return Boolean( + await handlers.hasCompatibleActions?.({ + name: 'filter', + data: { + data: [ + { + table, + column: colIndex, + row: 0, + }, + ], + }, + }) + ); + }) + ); +} + +export const getMetricVisRenderer = ( + theme: ThemeServiceStart +): (() => ExpressionRenderDefinition<MetricVisRenderConfig>) => { + return () => ({ + name: EXPRESSION_METRIC_NAME, + displayName: 'metric visualization', + reuseDomNode: true, + render: async (domNode, { visData, visConfig }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + const filterable = await metricFilterable(visConfig.dimensions, visData, handlers); + + render( + <KibanaThemeProvider theme$={theme.theme$}> + <VisualizationContainer + data-test-subj="legacyMtrVis" + className="legacyMtrVis" + showNoResult={!visData.rows?.length} + handlers={handlers} + > + <MetricVisComponent + visData={visData} + visParams={visConfig} + renderComplete={() => handlers.done()} + fireEvent={handlers.event} + filterable={filterable} + /> + </VisualizationContainer> + </KibanaThemeProvider>, + domNode + ); + }, + }); +}; diff --git a/src/plugins/chart_expressions/expression_metric/public/format_service.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/format_service.ts similarity index 100% rename from src/plugins/chart_expressions/expression_metric/public/format_service.ts rename to src/plugins/chart_expressions/expression_legacy_metric/public/format_service.ts diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/index.ts new file mode 100644 index 0000000000000..9c9c1641b68a8 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExpressionLegacyMetricPlugin } from './plugin'; + +export function plugin() { + return new ExpressionLegacyMetricPlugin(); +} diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/plugin.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/plugin.ts new file mode 100644 index 0000000000000..fdfadb765d74e --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/plugin.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { Plugin as ExpressionsPublicPlugin } from '@kbn/expressions-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { metricVisFunction } from '../common'; +import { setFormatService, setPaletteService } from './services'; +import { getMetricVisRenderer } from './expression_renderers'; + +/** @internal */ +export interface ExpressionLegacyMetricPluginSetup { + expressions: ReturnType<ExpressionsPublicPlugin['setup']>; + charts: ChartsPluginSetup; +} + +/** @internal */ +export interface ExpressionLegacyMetricPluginStart { + fieldFormats: FieldFormatsStart; +} + +/** @internal */ +export class ExpressionLegacyMetricPlugin implements Plugin<void, void> { + public setup(core: CoreSetup, { expressions, charts }: ExpressionLegacyMetricPluginSetup) { + expressions.registerFunction(metricVisFunction); + expressions.registerRenderer(getMetricVisRenderer(core.theme)); + charts.palettes.getPalettes().then((palettes) => { + setPaletteService(palettes); + }); + } + + public start(core: CoreStart, { fieldFormats }: ExpressionLegacyMetricPluginStart) { + setFormatService(fieldFormats); + } +} diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/services/format_service.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/services/format_service.ts new file mode 100644 index 0000000000000..f8e48e3101952 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/services/format_service.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; +import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; + +export const [getFormatService, setFormatService] = + createGetterSetter<FieldFormatsStart>('fieldFormats'); diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/services/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/services/index.ts new file mode 100644 index 0000000000000..0b445d9c10b72 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/services/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getFormatService, setFormatService } from './format_service'; +export { getPaletteService, setPaletteService } from './palette_service'; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/services/palette_service.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/services/palette_service.ts new file mode 100644 index 0000000000000..60bee5353b270 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/services/palette_service.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { PaletteRegistry } from '@kbn/coloring'; +import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; + +export const [getPaletteService, setPaletteService] = + createGetterSetter<PaletteRegistry>('palette'); diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/utils/format.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/utils/format.ts new file mode 100644 index 0000000000000..80f5dec8151f6 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/utils/format.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FieldFormatsContentType, IFieldFormat } from '@kbn/field-formats-plugin/common'; + +export const formatValue = ( + value: number | string, + fieldFormatter: IFieldFormat, + format: FieldFormatsContentType = 'text' +) => { + if (typeof value === 'number' && isNaN(value)) { + return '-'; + } + + return fieldFormatter.convert(value, format); +}; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/utils/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/utils/index.ts new file mode 100644 index 0000000000000..66c305a14c460 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/utils/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export { parseRgbString, shouldApplyColor, needsLightText } from './palette'; +export { formatValue } from './format'; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/public/utils/palette.ts b/src/plugins/chart_expressions/expression_legacy_metric/public/utils/palette.ts new file mode 100644 index 0000000000000..7f588aa552385 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/public/utils/palette.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isColorDark } from '@elastic/eui'; + +export const parseRgbString = (rgb: string) => { + const groups = rgb.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*?(,\s*(\d+)\s*)?\)/) ?? []; + if (!groups) { + return null; + } + + const red = parseFloat(groups[1]); + const green = parseFloat(groups[2]); + const blue = parseFloat(groups[3]); + const opacity = groups[5] ? parseFloat(groups[5]) : undefined; + + return { red, green, blue, opacity }; +}; + +export const shouldApplyColor = (color: string) => { + const rgb = parseRgbString(color); + const { opacity } = rgb ?? {}; + + // if opacity === 0, it means there is no color to apply to the metric + return !rgb || (rgb && opacity !== 0); +}; + +export const needsLightText = (bgColor: string = '') => { + const rgb = parseRgbString(bgColor); + if (!rgb) { + return false; + } + + const { red, green, blue, opacity } = rgb; + return isColorDark(red, green, blue) && opacity !== 0; +}; diff --git a/src/plugins/chart_expressions/expression_legacy_metric/server/index.ts b/src/plugins/chart_expressions/expression_legacy_metric/server/index.ts new file mode 100644 index 0000000000000..9c9c1641b68a8 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/server/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ExpressionLegacyMetricPlugin } from './plugin'; + +export function plugin() { + return new ExpressionLegacyMetricPlugin(); +} diff --git a/src/plugins/chart_expressions/expression_legacy_metric/server/plugin.ts b/src/plugins/chart_expressions/expression_legacy_metric/server/plugin.ts new file mode 100644 index 0000000000000..6c3b224d35078 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/server/plugin.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { ExpressionsServerStart, ExpressionsServerSetup } from '@kbn/expressions-plugin/server'; +import { metricVisFunction } from '../common'; + +interface SetupDeps { + expressions: ExpressionsServerSetup; +} + +interface StartDeps { + expression: ExpressionsServerStart; +} + +export type ExpressionLegacyMetricPluginSetup = void; +export type ExpressionLegacyMetricPluginStart = void; + +export class ExpressionLegacyMetricPlugin + implements + Plugin< + ExpressionLegacyMetricPluginSetup, + ExpressionLegacyMetricPluginStart, + SetupDeps, + StartDeps + > +{ + public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionLegacyMetricPluginSetup { + expressions.registerFunction(metricVisFunction); + } + + public start(core: CoreStart): ExpressionLegacyMetricPluginStart {} + + public stop() {} +} diff --git a/src/plugins/chart_expressions/expression_legacy_metric/tsconfig.json b/src/plugins/chart_expressions/expression_legacy_metric/tsconfig.json new file mode 100644 index 0000000000000..ff5089c7f4d21 --- /dev/null +++ b/src/plugins/chart_expressions/expression_legacy_metric/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "isolatedModules": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../core/tsconfig.json" }, + { "path": "../../expressions/tsconfig.json" }, + { "path": "../../presentation_util/tsconfig.json" }, + { "path": "../../field_formats/tsconfig.json" }, + { "path": "../../charts/tsconfig.json" }, + { "path": "../../visualizations/tsconfig.json" }, + ] +} diff --git a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts index add31e7b12014..b7b3426f5e132 100644 --- a/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts +++ b/src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts @@ -13,29 +13,10 @@ import { Dimension, validateAccessor, } from '@kbn/visualizations-plugin/common/utils'; -import { ColorMode } from '@kbn/charts-plugin/common'; +import { LayoutDirection } from '@elastic/charts'; import { visType } from '../types'; import { MetricVisExpressionFunctionDefinition } from '../types'; -import { EXPRESSION_METRIC_NAME, LabelPosition } from '../constants'; - -const errors = { - severalMetricsAndColorFullBackgroundSpecifiedError: () => - i18n.translate( - 'expressionMetricVis.function.errors.severalMetricsAndColorFullBackgroundSpecified', - { - defaultMessage: - 'Full background coloring cannot be applied to a visualization with multiple metrics.', - } - ), - splitByBucketAndColorFullBackgroundSpecifiedError: () => - i18n.translate( - 'expressionMetricVis.function.errors.splitByBucketAndColorFullBackgroundSpecified', - { - defaultMessage: - 'Full background coloring cannot be applied to visualizations that have a bucket specified.', - } - ), -}; +import { EXPRESSION_METRIC_NAME } from '../constants'; export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ name: EXPRESSION_METRIC_NAME, @@ -45,105 +26,76 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ defaultMessage: 'Metric visualization', }), args: { - percentageMode: { - types: ['boolean'], - default: false, - help: i18n.translate('expressionMetricVis.function.percentageMode.help', { - defaultMessage: 'Shows metric in percentage mode. Requires colorRange to be set.', - }), - }, - colorMode: { - types: ['string'], - default: `"${ColorMode.None}"`, - options: [ColorMode.None, ColorMode.Labels, ColorMode.Background], - help: i18n.translate('expressionMetricVis.function.colorMode.help', { - defaultMessage: 'Which part of metric to color', + metric: { + types: ['vis_dimension', 'string'], + help: i18n.translate('expressionMetricVis.function.metric.help', { + defaultMessage: 'The primary metric.', }), - strict: true, }, - colorFullBackground: { - types: ['boolean'], - default: false, - help: i18n.translate('expressionMetricVis.function.colorFullBackground.help', { - defaultMessage: 'Applies the selected background color to the full visualization container', + secondaryMetric: { + types: ['vis_dimension', 'string'], + help: i18n.translate('expressionMetricVis.function.secondaryMetric.help', { + defaultMessage: 'The secondary metric (shown above the primary).', }), }, - palette: { - types: ['palette'], - help: i18n.translate('expressionMetricVis.function.palette.help', { - defaultMessage: 'Provides colors for the values, based on the bounds.', + breakdownBy: { + types: ['vis_dimension', 'string'], + help: i18n.translate('expressionMetricVis.function.breakdownBy.help', { + defaultMessage: 'The dimension containing the labels for sub-categories.', }), }, - showLabels: { - types: ['boolean'], - default: true, - help: i18n.translate('expressionMetricVis.function.showLabels.help', { - defaultMessage: 'Shows labels under the metric values.', + subtitle: { + types: ['string'], + help: i18n.translate('expressionMetricVis.function.subtitle.help', { + defaultMessage: 'The subtitle for a single metric. Overridden if breakdownBy is supplied.', }), }, - font: { - types: ['style'], - help: i18n.translate('expressionMetricVis.function.font.help', { - defaultMessage: 'Font settings.', + extraText: { + types: ['string'], + help: i18n.translate('expressionMetricVis.function.extra.help', { + defaultMessage: 'Text to be shown above metric value. Overridden by secondaryMetric.', }), - default: `{font size=60 align="center"}`, }, - labelFont: { - types: ['style'], - help: i18n.translate('expressionMetricVis.function.labelFont.help', { - defaultMessage: 'Label font settings.', + progressMax: { + types: ['vis_dimension', 'string'], + help: i18n.translate('expressionMetricVis.function.progressMax.help.', { + defaultMessage: 'The dimension containing the maximum value.', }), - default: `{font size=24 align="center"}`, }, - labelPosition: { + progressDirection: { types: ['string'], - options: [LabelPosition.BOTTOM, LabelPosition.TOP], - help: i18n.translate('expressionMetricVis.function.labelPosition.help', { - defaultMessage: 'Label position', + options: [LayoutDirection.Vertical, LayoutDirection.Horizontal], + default: LayoutDirection.Vertical, + help: i18n.translate('expressionMetricVis.function.progressDirection.help', { + defaultMessage: 'The direction the progress bar should grow.', }), - default: LabelPosition.BOTTOM, strict: true, }, - metric: { - types: ['string', 'vis_dimension'], - help: i18n.translate('expressionMetricVis.function.metric.help', { - defaultMessage: 'metric dimension configuration', + palette: { + types: ['palette'], + help: i18n.translate('expressionMetricVis.function.palette.help', { + defaultMessage: 'Provides colors for the values, based on the bounds.', }), - required: true, - multi: true, }, - bucket: { - types: ['string', 'vis_dimension'], - help: i18n.translate('expressionMetricVis.function.bucket.help', { - defaultMessage: 'bucket dimension configuration', + maxCols: { + types: ['number'], + help: i18n.translate('expressionMetricVis.function.maxCols.help', { + defaultMessage: 'Specifies the max number of columns in the metric grid.', }), + default: 5, }, - autoScale: { - types: ['boolean'], - help: i18n.translate('expressionMetricVis.function.autoScale.help', { - defaultMessage: 'Enable auto scale', + minTiles: { + types: ['number'], + help: i18n.translate('expressionMetricVis.function.minTiles.help', { + defaultMessage: + 'Specifies the minimum number of tiles in the metric grid regardless of the input data.', }), - required: false, }, }, fn(input, args, handlers) { - if (args.percentageMode && !args.palette?.params) { - throw new Error('Palette must be provided when using percentageMode'); - } - - // currently we can allow colorize full container only for one metric - if (args.colorFullBackground) { - if (args.bucket) { - throw new Error(errors.splitByBucketAndColorFullBackgroundSpecifiedError()); - } - - if (args.metric.length > 1 || input.rows.length > 1) { - throw new Error(errors.severalMetricsAndColorFullBackgroundSpecifiedError()); - } - } - - args.metric.forEach((metric) => validateAccessor(metric, input.columns)); - validateAccessor(args.bucket, input.columns); + validateAccessor(args.metric, input.columns); + validateAccessor(args.secondaryMetric, input.columns); + validateAccessor(args.breakdownBy, input.columns); if (handlers?.inspectorAdapters?.tables) { handlers.inspectorAdapters.tables.reset(); @@ -151,20 +103,40 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ const argsTable: Dimension[] = [ [ - args.metric, + [args.metric], i18n.translate('expressionMetricVis.function.dimension.metric', { defaultMessage: 'Metric', }), ], ]; - if (args.bucket) { + + if (args.secondaryMetric) { argsTable.push([ - [args.bucket], + [args.secondaryMetric], + i18n.translate('expressionMetricVis.function.dimension.secondaryMetric', { + defaultMessage: 'Secondary Metric', + }), + ]); + } + + if (args.breakdownBy) { + argsTable.push([ + [args.breakdownBy], i18n.translate('expressionMetricVis.function.dimension.splitGroup', { defaultMessage: 'Split group', }), ]); } + + if (args.progressMax) { + argsTable.push([ + [args.progressMax], + i18n.translate('expressionMetricVis.function.dimension.maximum', { + defaultMessage: 'Maximum', + }), + ]); + } + const logTable = prepareLogTable(input, argsTable, true); handlers.inspectorAdapters.tables.logDatatable('default', logTable); } @@ -177,27 +149,18 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({ visType, visConfig: { metric: { + subtitle: args.subtitle, + extraText: args.extraText, palette: args.palette?.params, - percentageMode: args.percentageMode, - metricColorMode: args.colorMode, - labels: { - show: args.showLabels, - position: args.labelPosition, - style: { - ...args.labelFont, - }, - }, - colorFullBackground: args.colorFullBackground, - style: { - bgColor: args.colorMode === ColorMode.Background, - labelColor: args.colorMode === ColorMode.Labels, - ...args.font, - }, - autoScale: args.autoScale, + progressDirection: args.progressDirection, + maxCols: args.maxCols, + minTiles: args.minTiles, }, dimensions: { - metrics: args.metric, - ...(args.bucket ? { bucket: args.bucket } : {}), + metric: args.metric, + secondaryMetric: args.secondaryMetric, + breakdownBy: args.breakdownBy, + progressMax: args.progressMax, }, }, }, diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts index 5ad540fc579f6..428afc9426a3e 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts @@ -7,29 +7,28 @@ */ import type { PaletteOutput } from '@kbn/coloring'; +import { LayoutDirection } from '@elastic/charts'; import { Datatable, ExpressionFunctionDefinition, ExpressionValueRender, - Style, } from '@kbn/expressions-plugin'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; -import { ColorMode, CustomPaletteState } from '@kbn/charts-plugin/common'; -import { VisParams, visType, LabelPositionType } from './expression_renderers'; +import { CustomPaletteState } from '@kbn/charts-plugin/common'; +import { VisParams, visType } from './expression_renderers'; import { EXPRESSION_METRIC_NAME } from '../constants'; export interface MetricArguments { - percentageMode: boolean; - colorMode: ColorMode; - showLabels: boolean; + metric: ExpressionValueVisDimension | string; + secondaryMetric?: ExpressionValueVisDimension | string; + breakdownBy?: ExpressionValueVisDimension | string; + subtitle?: string; + extraText?: string; + progressMax?: ExpressionValueVisDimension | string; + progressDirection: LayoutDirection; palette?: PaletteOutput<CustomPaletteState>; - font: Style; - labelFont: Style; - labelPosition: LabelPositionType; - metric: Array<ExpressionValueVisDimension | string>; - bucket?: ExpressionValueVisDimension | string; - colorFullBackground: boolean; - autoScale?: boolean; + maxCols: number; + minTiles?: number; } export type MetricInput = Datatable; diff --git a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts index 8c370480a7be9..20be978b55684 100644 --- a/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts +++ b/src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts @@ -6,38 +6,26 @@ * Side Public License, v 1. */ -import { $Values } from '@kbn/utility-types'; import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; -import { - ColorMode, - Labels, - CustomPaletteState, - Style as ChartStyle, -} from '@kbn/charts-plugin/common'; -import { Style } from '@kbn/expressions-plugin/common'; -import { LabelPosition } from '../constants'; +import { CustomPaletteState } from '@kbn/charts-plugin/common'; +import { LayoutDirection } from '@elastic/charts'; export const visType = 'metric'; export interface DimensionsVisParam { - metrics: Array<ExpressionValueVisDimension | string>; - bucket?: ExpressionValueVisDimension | string; + metric: ExpressionValueVisDimension | string; + secondaryMetric?: ExpressionValueVisDimension | string; + breakdownBy?: ExpressionValueVisDimension | string; + progressMax?: ExpressionValueVisDimension | string; } -export type LabelPositionType = $Values<typeof LabelPosition>; - -export type MetricStyle = Style & Pick<ChartStyle, 'bgColor' | 'labelColor'>; - -export type LabelsConfig = Labels & { style: Style; position: LabelPositionType }; export interface MetricVisParam { - percentageMode: boolean; - percentageFormatPattern?: string; - metricColorMode: ColorMode; + subtitle?: string; + extraText?: string; palette?: CustomPaletteState; - labels: LabelsConfig; - style: MetricStyle; - colorFullBackground: boolean; - autoScale?: boolean; + progressDirection: LayoutDirection; + maxCols: number; + minTiles?: number; } export interface VisParams { diff --git a/src/plugins/chart_expressions/expression_metric/kibana.json b/src/plugins/chart_expressions/expression_metric/kibana.json index dec818b1f17df..ba4cd882c0b52 100755 --- a/src/plugins/chart_expressions/expression_metric/kibana.json +++ b/src/plugins/chart_expressions/expression_metric/kibana.json @@ -6,10 +6,16 @@ "name": "Vis Editors", "githubTeam": "kibana-vis-editors" }, - "description": "Expression MetricVis plugin adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart.", + "description": "Adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart.", "server": true, "ui": true, - "requiredPlugins": ["expressions", "fieldFormats", "charts", "visualizations", "presentationUtil"], + "requiredPlugins": [ + "expressions", + "fieldFormats", + "charts", + "visualizations", + "presentationUtil" + ], "requiredBundles": ["kibanaUtils", "kibanaReact"], "optionalPlugins": [] } diff --git a/src/plugins/chart_expressions/expression_metric/public/__mocks__/theme_service.ts b/src/plugins/chart_expressions/expression_metric/public/__mocks__/theme_service.ts new file mode 100644 index 0000000000000..9690bd4a2d486 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/__mocks__/theme_service.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const getThemeService = () => { + return { + useChartsTheme: () => ({}), + }; +}; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.test.ts b/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.test.ts new file mode 100644 index 0000000000000..03266f3189460 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getCurrencyCode } from './currency_codes'; +// @ts-ignore +import numeralLanguages from '@elastic/numeral/languages'; + +describe('getCurrencyCode', () => { + const allLanguages = [ + ['en', '$'], + ...numeralLanguages.map((language: { id: string; lang: { currency: { symbol: string } } }) => { + const { + id, + lang: { + currency: { symbol }, + }, + } = language; + return [id, symbol]; + }), + ]; + + it.each(allLanguages)( + 'should have currency code for locale "%s" and currency "%s"', + (locale, symbol) => { + expect(getCurrencyCode(locale, symbol)).toBeDefined(); + } + ); +}); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.ts b/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.ts new file mode 100644 index 0000000000000..8bc288a3ac590 --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/components/currency_codes.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// NOTE: needs to be kept in line with https://github.com/elastic/numeral-js/blob/kibana-fork/languages.js + USD +const currencyCodeMap: Record<string, string> = { + 'en-$': 'USD', + 'be-nl-€': 'EUR', + 'chs-¥': 'CNY', + 'cs-kč': 'CZK', + 'da-dk-dkk': 'DKK', + 'de-ch-chf': 'CHF', + 'de-€': 'EUR', + 'en-gb-£': 'GBP', + 'es-es-€': 'EUR', + 'es-$': '', + 'et-€': 'EUR', + 'fi-€': 'EUR', + 'fr-ca-$': 'CAD', + 'fr-ch-chf': 'CHF', + 'fr-€': 'EUR', + 'hu-ft': 'HUF', + 'it-€': 'EUR', + 'ja-¥': 'JPY', + 'nl-nl-€': 'EUR', + 'pl-pln': 'PLN', + 'pt-br-r$': 'BRL', + 'pt-pt-€': 'EUR', + 'ru-ua-₴': 'UAH', + 'ru-руб.': 'RUB', + 'sk-€': 'EUR', + 'th-฿': 'THB', + 'tr-₺': 'TRY', + 'uk-ua-₴': 'UAH', +}; + +/** + * Returns currency code for use with the Intl API. + */ +export const getCurrencyCode = (localeId: string, currencySymbol: string) => { + return currencyCodeMap[`${localeId.trim()}-${currencySymbol.trim()}`.toLowerCase()]; +}; diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx new file mode 100644 index 0000000000000..877d27450e03c --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.test.tsx @@ -0,0 +1,905 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { Datatable } from '@kbn/expressions-plugin/common'; +import MetricVis, { MetricVisComponentProps } from './metric_vis'; +import { LayoutDirection, Metric, MetricWProgress, Settings } from '@elastic/charts'; +import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common'; +import { SerializableRecord } from '@kbn/utility-types'; +import numeral from '@elastic/numeral'; + +const mockDeserialize = jest.fn(() => ({ + getConverterFor: jest.fn(() => () => 'formatted duration'), +})); + +const mockGetColorForValue = jest.fn<undefined | string, any>(() => undefined); + +const mockLookupCurrentLocale = jest.fn(() => 'en'); + +jest.mock('../services', () => ({ + getFormatService: () => { + return { + deserialize: mockDeserialize, + }; + }, + getPaletteService: () => ({ + get: jest.fn(() => ({ getColorForValue: mockGetColorForValue })), + }), + getThemeService: () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getThemeService } = require('../__mocks__/theme_service'); + return getThemeService(); + }, + getUiSettingsService: () => { + return { + get: mockLookupCurrentLocale, + }; + }, +})); + +jest.mock('@kbn/field-formats-plugin/common', () => ({ + FORMATS_UI_SETTINGS: { + FORMAT_NUMBER_DEFAULT_LOCALE: 'format_number_default_locale', + }, +})); + +jest.mock('@elastic/numeral', () => ({ + language: jest.fn(() => 'en'), + languageData: jest.fn(() => ({ + currency: { + symbol: '$', + }, + })), +})); + +type Props = MetricVisComponentProps; + +const dayOfWeekColumnId = 'col-0-0'; +const basePriceColumnId = 'col-1-1'; +const minPriceColumnId = 'col-2-2'; + +const table: Datatable = { + type: 'datatable', + columns: [ + { + id: dayOfWeekColumnId, + name: 'day_of_week: Descending', + meta: { + type: 'string', + field: 'day_of_week', + index: 'kibana_sample_data_ecommerce', + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: '(missing value)', + }, + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + id: '0', + enabled: true, + type: 'terms', + params: { + field: 'day_of_week', + orderBy: '1', + order: 'desc', + size: 6, + otherBucket: false, + otherBucketLabel: 'Other', + missingBucket: false, + missingBucketLabel: '(missing value)', + }, + schema: 'segment', + }, + }, + }, + { + id: basePriceColumnId, + name: 'Median products.base_price', + meta: { + type: 'number', + field: 'products.base_price', + index: 'kibana_sample_data_ecommerce', + params: { + id: 'number', + params: { + pattern: '$0,0.00', + }, + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + id: '1', + enabled: true, + type: 'median', + params: { + field: 'products.base_price', + }, + schema: 'metric', + }, + }, + }, + { + id: 'col-2-2', + name: 'Median products.min_price', + meta: { + type: 'number', + field: 'products.min_price', + index: 'kibana_sample_data_ecommerce', + params: { + id: 'number', + params: { + pattern: '$0,0.00', + }, + }, + source: 'esaggs', + sourceParams: { + hasPrecisionError: false, + indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', + id: '2', + enabled: true, + type: 'median', + params: { + field: 'products.min_price', + }, + schema: 'metric', + }, + }, + }, + ], + rows: [ + { + [dayOfWeekColumnId]: 'Friday', + [basePriceColumnId]: 28.984375, + [minPriceColumnId]: 13.6328125, + }, + { + [dayOfWeekColumnId]: 'Wednesday', + [basePriceColumnId]: 28.984375, + [minPriceColumnId]: 13.639539930555555, + }, + { + [dayOfWeekColumnId]: 'Saturday', + [basePriceColumnId]: 25.984375, + [minPriceColumnId]: 13.34375, + }, + { + [dayOfWeekColumnId]: 'Sunday', + [basePriceColumnId]: 25.784375, + [minPriceColumnId]: 13.4921875, + }, + { + [dayOfWeekColumnId]: 'Thursday', + [basePriceColumnId]: 25.348011363636363, + [minPriceColumnId]: 13.34375, + }, + { + [dayOfWeekColumnId]: 'Monday', + [basePriceColumnId]: 24.984375, + [minPriceColumnId]: 13.242513020833334, + }, + ], +}; + +describe('MetricVisComponent', function () { + describe('single metric', () => { + const config: Props['config'] = { + metric: { + progressDirection: 'vertical', + maxCols: 5, + }, + dimensions: { + metric: basePriceColumnId, + }, + }; + + it('should render a single metric value', () => { + const component = shallow( + <MetricVis config={config} data={table} renderComplete={() => {}} /> + ); + + const { data } = component.find(Metric).props(); + + expect(data).toBeDefined(); + expect(data?.length).toBe(1); + + const visConfig = data![0][0]!; + + expect(visConfig).toMatchInlineSnapshot(` + Object { + "color": "#343741", + "extra": <span />, + "subtitle": undefined, + "title": "Median products.base_price", + "value": 28.984375, + "valueFormatter": [Function], + } + `); + }); + it('should display subtitle and extra text', () => { + const component = shallow( + <MetricVis + config={{ + ...config, + metric: { ...config.metric, subtitle: 'subtitle', extraText: 'extra text' }, + }} + data={table} + renderComplete={() => {}} + /> + ); + + const [[visConfig]] = component.find(Metric).props().data!; + + expect(visConfig!.subtitle).toBe('subtitle'); + expect(visConfig!.extra).toEqual(<span>extra text</span>); + + expect(visConfig).toMatchInlineSnapshot(` + Object { + "color": "#343741", + "extra": <span> + extra text + </span>, + "subtitle": "subtitle", + "title": "Median products.base_price", + "value": 28.984375, + "valueFormatter": [Function], + } + `); + }); + it('should display secondary metric', () => { + const component = shallow( + <MetricVis + config={{ + ...config, + metric: { ...config.metric, subtitle: 'subtitle', extraText: 'extra text' }, + dimensions: { ...config.dimensions, secondaryMetric: minPriceColumnId }, + }} + data={table} + renderComplete={() => {}} + /> + ); + + const [[visConfig]] = component.find(Metric).props().data!; + + // overrides subtitle and extra text + expect(visConfig!.subtitle).toBe(table.columns[2].name); + expect(visConfig!.extra).toEqual(<span>13.63</span>); + + expect(visConfig).toMatchInlineSnapshot(` + Object { + "color": "#343741", + "extra": <span> + 13.63 + </span>, + "subtitle": "Median products.min_price", + "title": "Median products.base_price", + "value": 28.984375, + "valueFormatter": [Function], + } + `); + }); + + it('should display progress bar if min and max provided', () => { + const getConfig = (max?: string, direction: LayoutDirection = 'vertical') => + shallow( + <MetricVis + config={{ + ...config, + metric: { + ...config.metric, + progressDirection: direction, + }, + dimensions: { + ...config.dimensions, + progressMax: max, + }, + }} + data={table} + renderComplete={() => {}} + /> + ) + .find(Metric) + .props().data![0][0]!; + + expect(getConfig(undefined)).not.toHaveProperty('domain'); + expect(getConfig(undefined)).not.toHaveProperty('progressBarDirection'); + + expect(getConfig('foobar')).not.toHaveProperty('domain'); + expect(getConfig('foobar')).not.toHaveProperty('progressBarDirection'); + + const configWithProgress = getConfig(basePriceColumnId) as MetricWProgress; + + expect(configWithProgress.domain).toEqual({ min: 0, max: table.rows[0][basePriceColumnId] }); + expect(configWithProgress.progressBarDirection).toBe('vertical'); + + expect(configWithProgress).toMatchInlineSnapshot(` + Object { + "color": "#343741", + "domain": Object { + "max": 28.984375, + "min": 0, + }, + "extra": <span />, + "progressBarDirection": "vertical", + "subtitle": undefined, + "title": "Median products.base_price", + "value": 28.984375, + "valueFormatter": [Function], + } + `); + + expect( + (getConfig(basePriceColumnId, 'horizontal') as MetricWProgress).progressBarDirection + ).toBe('horizontal'); + }); + + it('should fetch color from palette if provided', () => { + const colorFromPalette = 'color-from-palette'; + + mockGetColorForValue.mockReturnValue(colorFromPalette); + + const component = shallow( + <MetricVis + config={{ + ...config, + metric: { + ...config.metric, + palette: { + colors: [], + gradient: true, + stops: [], + range: 'number', + rangeMin: 2, + rangeMax: 10, + }, + }, + }} + data={table} + renderComplete={() => {}} + /> + ); + + const [[datum]] = component.find(Metric).props().data!; + + expect(datum!.color).toBe(colorFromPalette); + expect(mockGetColorForValue.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + 28.984375, + Object { + "colors": Array [], + "gradient": true, + "range": "number", + "rangeMax": 10, + "rangeMin": 2, + "stops": Array [], + }, + Object { + "max": 10, + "min": 2, + }, + ], + ] + `); + }); + }); + + describe('metric grid', () => { + const config: Props['config'] = { + metric: { + progressDirection: 'vertical', + maxCols: 5, + }, + dimensions: { + metric: basePriceColumnId, + breakdownBy: dayOfWeekColumnId, + }, + }; + + it('should render a grid if breakdownBy dimension supplied', () => { + const component = shallow( + <MetricVis config={config} data={table} renderComplete={() => {}} /> + ); + + const { data } = component.find(Metric).props(); + + expect(data).toBeDefined(); + expect(data?.flat().length).toBe(table.rows.length); + + const visConfig = data![0]; + + expect(visConfig).toMatchInlineSnapshot(` + Array [ + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Friday", + "value": 28.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Wednesday", + "value": 28.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Saturday", + "value": 25.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Sunday", + "value": 25.784375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Thursday", + "value": 25.348011363636363, + "valueFormatter": [Function], + }, + ] + `); + }); + + it('should display extra text or secondary metric', () => { + const componentWithSecondaryDimension = shallow( + <MetricVis + config={{ + ...config, + dimensions: { ...config.dimensions, secondaryMetric: minPriceColumnId }, + // extra text included to make sure it's overridden + metric: { ...config.metric, extraText: 'howdy' }, + }} + data={table} + renderComplete={() => {}} + /> + ); + + expect( + componentWithSecondaryDimension + .find(Metric) + .props() + .data?.[0].map((datum) => datum?.extra) + ).toMatchInlineSnapshot(` + Array [ + <span> + 13.63 + </span>, + <span> + 13.64 + </span>, + <span> + 13.34 + </span>, + <span> + 13.49 + </span>, + <span> + 13.34 + </span>, + ] + `); + + const componentWithExtraText = shallow( + <MetricVis + config={{ + ...config, + metric: { ...config.metric, extraText: 'howdy' }, + }} + data={table} + renderComplete={() => {}} + /> + ); + + expect( + componentWithExtraText + .find(Metric) + .props() + .data?.[0].map((datum) => datum?.extra) + ).toMatchInlineSnapshot(` + Array [ + <span> + howdy + </span>, + <span> + howdy + </span>, + <span> + howdy + </span>, + <span> + howdy + </span>, + <span> + howdy + </span>, + ] + `); + }); + + it('should respect maxCols and minTiles', () => { + const getConfigs = (maxCols?: number, minTiles?: number) => + shallow( + <MetricVis + config={{ + ...config, + metric: { + ...config.metric, + ...(maxCols ? { maxCols } : {}), + minTiles, + }, + }} + data={table} + renderComplete={() => {}} + /> + ) + .find(Metric) + .props().data!; + + const configsWithDefaults = getConfigs(undefined, undefined); + expect(configsWithDefaults.length).toBe(2); + expect(configsWithDefaults[0].length).toBe(5); + + const configsWithCustomCols = getConfigs(2, undefined); + expect(configsWithCustomCols.length).toBe(3); + expect(configsWithCustomCols[0].length).toBe(2); + expect(configsWithCustomCols[1].length).toBe(2); + expect(configsWithCustomCols[2].length).toBe(2); + + const configsWithMinTiles = getConfigs(5, 10); + expect(configsWithMinTiles.length).toBe(2); + expect(configsWithMinTiles[1].length).toBe(5); + expect(configsWithMinTiles).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Friday", + "value": 28.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Wednesday", + "value": 28.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Saturday", + "value": 25.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Sunday", + "value": 25.784375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Thursday", + "value": 25.348011363636363, + "valueFormatter": [Function], + }, + ], + Array [ + Object { + "color": "#343741", + "extra": <span />, + "subtitle": "Median products.base_price", + "title": "Monday", + "value": 24.984375, + "valueFormatter": [Function], + }, + undefined, + undefined, + undefined, + undefined, + ], + ] + `); + }); + + it('should display progress bar if max provided', () => { + expect( + shallow( + <MetricVis + config={{ + ...config, + metric: { + ...config.metric, + }, + dimensions: { + ...config.dimensions, + progressMax: basePriceColumnId, + }, + }} + data={table} + renderComplete={() => {}} + /> + ) + .find(Metric) + .props().data + ).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "color": "#343741", + "domain": Object { + "max": 28.984375, + "min": 0, + }, + "extra": <span />, + "progressBarDirection": "vertical", + "subtitle": "Median products.base_price", + "title": "Friday", + "value": 28.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "domain": Object { + "max": 28.984375, + "min": 0, + }, + "extra": <span />, + "progressBarDirection": "vertical", + "subtitle": "Median products.base_price", + "title": "Wednesday", + "value": 28.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "domain": Object { + "max": 25.984375, + "min": 0, + }, + "extra": <span />, + "progressBarDirection": "vertical", + "subtitle": "Median products.base_price", + "title": "Saturday", + "value": 25.984375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "domain": Object { + "max": 25.784375, + "min": 0, + }, + "extra": <span />, + "progressBarDirection": "vertical", + "subtitle": "Median products.base_price", + "title": "Sunday", + "value": 25.784375, + "valueFormatter": [Function], + }, + Object { + "color": "#343741", + "domain": Object { + "max": 25.348011363636363, + "min": 0, + }, + "extra": <span />, + "progressBarDirection": "vertical", + "subtitle": "Median products.base_price", + "title": "Thursday", + "value": 25.348011363636363, + "valueFormatter": [Function], + }, + ], + Array [ + Object { + "color": "#343741", + "domain": Object { + "max": 24.984375, + "min": 0, + }, + "extra": <span />, + "progressBarDirection": "vertical", + "subtitle": "Median products.base_price", + "title": "Monday", + "value": 24.984375, + "valueFormatter": [Function], + }, + ], + ] + `); + }); + }); + + it('should report render complete', () => { + const renderCompleteSpy = jest.fn(); + const component = shallow( + <MetricVis + config={{ + metric: { + progressDirection: 'vertical', + maxCols: 5, + }, + dimensions: { + metric: basePriceColumnId, + }, + }} + data={table} + renderComplete={renderCompleteSpy} + /> + ); + component.find(Settings).props().onRenderChange!(false); + + expect(renderCompleteSpy).not.toHaveBeenCalled(); + + component.find(Settings).props().onRenderChange!(true); + + expect(renderCompleteSpy).toHaveBeenCalledTimes(1); + }); + + describe('metric value formatting', () => { + const getFormattedMetrics = ( + value: number, + secondaryValue: number, + fieldFormatter: SerializedFieldFormat<SerializableRecord> + ) => { + const config: Props['config'] = { + metric: { + progressDirection: 'vertical', + maxCols: 5, + }, + dimensions: { + metric: '1', + secondaryMetric: '2', + }, + }; + + const component = shallow( + <MetricVis + config={config} + data={{ + type: 'datatable', + columns: [ + { + id: '1', + name: '', + meta: { type: 'number', params: fieldFormatter }, + }, + { + id: '2', + name: '', + meta: { type: 'number', params: fieldFormatter }, + }, + ], + rows: [{ '1': value, '2': secondaryValue }], + }} + renderComplete={() => {}} + /> + ); + + const { + value: primaryMetric, + valueFormatter, + extra, + } = component.find(Metric).props().data?.[0][0]!; + + return { primary: valueFormatter(primaryMetric), secondary: extra?.props.children }; + }; + + it('correctly formats plain numbers', () => { + const { primary, secondary } = getFormattedMetrics(394.2393, 983123.984, { id: 'number' }); + expect(primary).toBe('394.24'); + expect(secondary).toBe('983.12K'); + }); + + it('correctly formats currency', () => { + const { primary, secondary } = getFormattedMetrics(1000.839, 11.2, { id: 'currency' }); + expect(primary).toBe('$1.00K'); + expect(secondary).toBe('$11.20'); + + mockLookupCurrentLocale.mockReturnValueOnce('be-nl'); + // @ts-expect-error + (numeral.languageData as jest.Mock).mockReturnValueOnce({ + currency: { + symbol: '€', + }, + }); + + const { primary: primaryEuro } = getFormattedMetrics(1000.839, 0, { + id: 'currency', + }); + expect(primaryEuro).toBe('1,00 тыс. €'); + // check that we restored the numeral.js state + expect(numeral.language).toHaveBeenLastCalledWith('en'); + }); + + it('correctly formats percentages', () => { + const { primary, secondary } = getFormattedMetrics(0.23939, 11.2, { id: 'percent' }); + expect(primary).toBe('23.94%'); + expect(secondary).toBe('1.12K%'); + }); + + it('correctly formats bytes', () => { + const base = 1024; + + const { primary: bytesValue } = getFormattedMetrics(base - 1, 0, { id: 'bytes' }); + expect(bytesValue).toBe('1,023 byte'); + + const { primary: kiloBytesValue } = getFormattedMetrics(Math.pow(base, 1), 0, { + id: 'bytes', + }); + expect(kiloBytesValue).toBe('1 kB'); + + const { primary: megaBytesValue } = getFormattedMetrics(Math.pow(base, 2), 0, { + id: 'bytes', + }); + expect(megaBytesValue).toBe('1 MB'); + + const { primary: moreThanPetaValue } = getFormattedMetrics(Math.pow(base, 6), 0, { + id: 'bytes', + }); + expect(moreThanPetaValue).toBe('1,024 PB'); + }); + + it('correctly formats durations', () => { + const { primary, secondary } = getFormattedMetrics(1, 1, { + id: 'duration', + params: { + // the following params should be preserved + inputFormat: 'minutes', + // the following params should be overridden + outputFormat: 'precise', + outputPrecision: 2, + useShortSuffix: false, + }, + }); + + expect(primary).toBe('formatted duration'); + expect(secondary).toBe('formatted duration'); + expect(mockDeserialize).toHaveBeenCalledTimes(2); + expect(mockDeserialize).toHaveBeenCalledWith({ + id: 'duration', + params: { + inputFormat: 'minutes', + outputFormat: 'humanizePrecise', + outputPrecision: 1, + useShortSuffix: true, + }, + }); + }); + }); +}); diff --git a/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx new file mode 100644 index 0000000000000..8d6bd9cecfe3a --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/components/metric_vis.tsx @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; + +import numeral from '@elastic/numeral'; +import { i18n } from '@kbn/i18n'; +import { Chart, Metric, MetricSpec, RenderChangeListener, Settings } from '@elastic/charts'; +import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils'; +import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common'; +import { + Datatable, + DatatableColumn, + DatatableRow, + IInterpreterRenderHandlers, +} from '@kbn/expressions-plugin'; +import { CustomPaletteState } from '@kbn/charts-plugin/public'; +import { euiLightVars } from '@kbn/ui-theme'; +import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common'; +import { VisParams } from '../../common'; +import { + getPaletteService, + getThemeService, + getFormatService, + getUiSettingsService, +} from '../services'; +import { getCurrencyCode } from './currency_codes'; + +const defaultColor = euiLightVars.euiColorDarkestShade; + +const getBytesUnit = (value: number) => { + const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte']; + const abs = Math.abs(value); + + const base = 1024; + let unit = units[0]; + let matched = abs < base; + let power; + + if (!matched) { + for (power = 1; power < units.length; power++) { + const [min, max] = [Math.pow(base, power), Math.pow(base, power + 1)]; + if (abs >= min && abs < max) { + unit = units[power]; + matched = true; + value = value / min; + break; + } + } + } + + if (!matched) { + value = value / Math.pow(base, units.length - 1); + unit = units[units.length - 1]; + } + + return { value, unit }; +}; + +const getFormatter = ( + accessor: ExpressionValueVisDimension | string, + columns: Datatable['columns'] +) => { + const serializedFieldFormat = getFormatByAccessor(accessor, columns); + const formatId = serializedFieldFormat?.id ?? 'number'; + + if (!['number', 'currency', 'percent', 'bytes', 'duration'].includes(formatId)) { + throw new Error( + i18n.translate('expressionMetricVis.errors.unsupportedColumnFormat', { + defaultMessage: 'Metric Visualization - Unsupported column format: "{id}"', + values: { + id: formatId, + }, + }) + ); + } + + if (formatId === 'duration') { + const formatter = getFormatService().deserialize({ + ...serializedFieldFormat, + params: { + ...serializedFieldFormat!.params, + outputFormat: 'humanizePrecise', + outputPrecision: 1, + useShortSuffix: true, + }, + }); + return formatter.getConverterFor('text'); + } + + const uiSettings = getUiSettingsService(); + + const locale = uiSettings.get(FORMATS_UI_SETTINGS.FORMAT_NUMBER_DEFAULT_LOCALE) || 'en'; + + const intlOptions: Intl.NumberFormatOptions = { + maximumFractionDigits: 2, + }; + + if (['number', 'currency', 'percent'].includes(formatId)) { + intlOptions.notation = 'compact'; + } + + if (formatId === 'currency') { + const currentNumeralLang = numeral.language(); + numeral.language(locale); + + const { + currency: { symbol: currencySymbol }, + // @ts-expect-error + } = numeral.languageData(); + + // restore previous value + numeral.language(currentNumeralLang); + + intlOptions.currency = getCurrencyCode(locale, currencySymbol); + intlOptions.style = 'currency'; + } + + if (formatId === 'percent') { + intlOptions.style = 'percent'; + } + + return formatId === 'bytes' + ? (rawValue: number) => { + const { value, unit } = getBytesUnit(rawValue); + return new Intl.NumberFormat(locale, { ...intlOptions, style: 'unit', unit }).format(value); + } + : new Intl.NumberFormat(locale, intlOptions).format; +}; + +const getColor = (value: number, paletteParams: CustomPaletteState | undefined) => + paletteParams + ? getPaletteService().get('custom')?.getColorForValue?.(value, paletteParams, { + min: paletteParams.rangeMin, + max: paletteParams.rangeMax, + }) || defaultColor + : defaultColor; + +export interface MetricVisComponentProps { + data: Datatable; + config: Pick<VisParams, 'metric' | 'dimensions'>; + renderComplete: IInterpreterRenderHandlers['done']; +} + +const MetricVisComponent = ({ data, config, renderComplete }: MetricVisComponentProps) => { + const primaryMetricColumn = getColumnByAccessor(config.dimensions.metric, data.columns)!; + const formatPrimaryMetric = getFormatter(config.dimensions.metric, data.columns); + + let secondaryMetricColumn: DatatableColumn | undefined; + let formatSecondaryMetric: ReturnType<typeof getFormatter>; + if (config.dimensions.secondaryMetric) { + secondaryMetricColumn = getColumnByAccessor(config.dimensions.secondaryMetric, data.columns); + formatSecondaryMetric = getFormatter(config.dimensions.secondaryMetric, data.columns); + } + + const breakdownByColumn = config.dimensions.breakdownBy + ? getColumnByAccessor(config.dimensions.breakdownBy, data.columns) + : undefined; + + let getProgressBarConfig = (_row: DatatableRow) => ({}); + + if (config.dimensions.progressMax) { + const maxColId = getColumnByAccessor(config.dimensions.progressMax, data.columns)?.id; + if (maxColId) { + getProgressBarConfig = (_row: DatatableRow) => ({ + domain: { + min: 0, + max: _row[maxColId], + }, + progressBarDirection: config.metric.progressDirection, + }); + } + } + + const metricConfigs: MetricSpec['data'][number] = ( + breakdownByColumn ? data.rows : data.rows.slice(0, 1) + ).map((row) => { + const value = row[primaryMetricColumn.id]; + const title = breakdownByColumn ? row[breakdownByColumn.id] : primaryMetricColumn.name; + const subtitle = breakdownByColumn + ? primaryMetricColumn.name + : secondaryMetricColumn?.name ?? config.metric.subtitle; + return { + value, + valueFormatter: formatPrimaryMetric, + title, + subtitle, + extra: ( + <span> + {secondaryMetricColumn + ? formatSecondaryMetric!(row[secondaryMetricColumn.id]) + : config.metric.extraText} + </span> + ), + color: getColor(value, config.metric.palette), + ...getProgressBarConfig(row), + }; + }); + + if (config.metric.minTiles) { + while (metricConfigs.length < config.metric.minTiles) metricConfigs.push(undefined); + } + + const grid: MetricSpec['data'] = []; + const { + metric: { maxCols }, + } = config; + for (let i = 0; i < metricConfigs.length; i += maxCols) { + grid.push(metricConfigs.slice(i, i + maxCols)); + } + + const chartTheme = getThemeService().useChartsTheme(); + const onRenderChange = useCallback<RenderChangeListener>( + (isRendered) => { + if (isRendered) { + renderComplete(); + } + }, + [renderComplete] + ); + + return ( + <Chart> + <Settings + theme={[{ background: { color: 'transparent' } }, chartTheme]} + onRenderChange={onRenderChange} + /> + <Metric id="metric" data={grid} /> + </Chart> + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { MetricVisComponent as default }; diff --git a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx index 8b281c6e91a78..e2237ad14facb 100644 --- a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx @@ -11,47 +11,12 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { ThemeServiceStart } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; -import { - ExpressionValueVisDimension, - VisualizationContainer, -} from '@kbn/visualizations-plugin/public'; -import { - ExpressionRenderDefinition, - IInterpreterRenderHandlers, -} from '@kbn/expressions-plugin/common/expression_renderers'; -import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils'; -import { Datatable } from '@kbn/expressions-plugin'; -import { EXPRESSION_METRIC_NAME, MetricVisRenderConfig, VisParams } from '../../common'; +import { ExpressionRenderDefinition } from '@kbn/expressions-plugin/common/expression_renderers'; +import { VisualizationContainer } from '@kbn/visualizations-plugin/public'; +import { css } from '@emotion/react'; +import { EXPRESSION_METRIC_NAME, MetricVisRenderConfig } from '../../common'; -// @ts-ignore -const MetricVisComponent = lazy(() => import('../components/metric_component')); - -async function metricFilterable( - dimensions: VisParams['dimensions'], - table: Datatable, - handlers: IInterpreterRenderHandlers -) { - return Promise.all( - dimensions.metrics.map(async (metric: string | ExpressionValueVisDimension) => { - const column = getColumnByAccessor(metric, table.columns); - const colIndex = table.columns.indexOf(column!); - return Boolean( - await handlers.hasCompatibleActions?.({ - name: 'filter', - data: { - data: [ - { - table, - column: colIndex, - row: 0, - }, - ], - }, - }) - ); - }) - ); -} +const MetricVis = lazy(() => import('../components/metric_vis')); export const getMetricVisRenderer = ( theme: ThemeServiceStart @@ -65,23 +30,18 @@ export const getMetricVisRenderer = ( unmountComponentAtNode(domNode); }); - const filterable = await metricFilterable(visConfig.dimensions, visData, handlers); - render( <KibanaThemeProvider theme$={theme.theme$}> <VisualizationContainer data-test-subj="mtrVis" - className="mtrVis" - showNoResult={!visData.rows?.length} + css={css` + height: 100%; + width: 100%; + `} + showNoResult={!visData.rows.length} handlers={handlers} > - <MetricVisComponent - visData={visData} - visParams={visConfig} - renderComplete={() => handlers.done()} - fireEvent={handlers.event} - filterable={filterable} - /> + <MetricVis data={visData} config={visConfig} renderComplete={() => handlers.done()} /> </VisualizationContainer> </KibanaThemeProvider>, domNode diff --git a/src/plugins/chart_expressions/expression_metric/public/plugin.ts b/src/plugins/chart_expressions/expression_metric/public/plugin.ts index 439fb398dbb8a..0ced2bb2d320f 100644 --- a/src/plugins/chart_expressions/expression_metric/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_metric/public/plugin.ts @@ -13,6 +13,8 @@ import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { metricVisFunction } from '../common'; import { setFormatService, setPaletteService } from './services'; import { getMetricVisRenderer } from './expression_renderers'; +import { setThemeService } from './services/theme_service'; +import { setUiSettingsService } from './services/ui_settings'; /** @internal */ export interface ExpressionMetricPluginSetup { @@ -27,12 +29,14 @@ export interface ExpressionMetricPluginStart { /** @internal */ export class ExpressionMetricPlugin implements Plugin<void, void> { - public setup(core: CoreSetup, { expressions, charts }: ExpressionMetricPluginSetup) { + public async setup(core: CoreSetup, { expressions, charts }: ExpressionMetricPluginSetup) { expressions.registerFunction(metricVisFunction); expressions.registerRenderer(getMetricVisRenderer(core.theme)); - charts.palettes.getPalettes().then((palettes) => { - setPaletteService(palettes); - }); + + setUiSettingsService(core.uiSettings); + setThemeService(charts.theme); + const palettes = await charts.palettes.getPalettes(); + setPaletteService(palettes); } public start(core: CoreStart, { fieldFormats }: ExpressionMetricPluginStart) { diff --git a/src/plugins/chart_expressions/expression_metric/public/services/index.ts b/src/plugins/chart_expressions/expression_metric/public/services/index.ts index 0b445d9c10b72..68edb67f9bb81 100644 --- a/src/plugins/chart_expressions/expression_metric/public/services/index.ts +++ b/src/plugins/chart_expressions/expression_metric/public/services/index.ts @@ -7,4 +7,6 @@ */ export { getFormatService, setFormatService } from './format_service'; +export { getThemeService, setThemeService } from './theme_service'; export { getPaletteService, setPaletteService } from './palette_service'; +export { getUiSettingsService, setUiSettingsService } from './ui_settings'; diff --git a/src/plugins/chart_expressions/expression_metric/public/services/theme_service.ts b/src/plugins/chart_expressions/expression_metric/public/services/theme_service.ts new file mode 100644 index 0000000000000..55ac6fb18cdad --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/services/theme_service.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; +import { ChartsPluginSetup } from '@kbn/charts-plugin/public'; + +export const [getThemeService, setThemeService] = + createGetterSetter<ChartsPluginSetup['theme']>('charts.theme'); diff --git a/src/plugins/chart_expressions/expression_metric/public/services/ui_settings.ts b/src/plugins/chart_expressions/expression_metric/public/services/ui_settings.ts new file mode 100644 index 0000000000000..6d884a31371cb --- /dev/null +++ b/src/plugins/chart_expressions/expression_metric/public/services/ui_settings.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CoreSetup } from '@kbn/core/public'; +import { createGetterSetter } from '@kbn/kibana-utils-plugin/public'; + +export const [getUiSettingsService, setUiSettingsService] = + createGetterSetter<CoreSetup['uiSettings']>('uiSettings'); diff --git a/src/plugins/field_formats/common/constants/base_formatters.ts b/src/plugins/field_formats/common/constants/base_formatters.ts index 89febf1027453..1808b97317530 100644 --- a/src/plugins/field_formats/common/constants/base_formatters.ts +++ b/src/plugins/field_formats/common/constants/base_formatters.ts @@ -12,6 +12,7 @@ import { BoolFormat, BytesFormat, ColorFormat, + CurrencyFormat, DurationFormat, GeoPointFormat, IpFormat, @@ -30,6 +31,7 @@ export const baseFormatters: FieldFormatInstanceType[] = [ BoolFormat, BytesFormat, ColorFormat, + CurrencyFormat, DurationFormat, GeoPointFormat, IpFormat, diff --git a/src/plugins/field_formats/common/converters/currency.test.ts b/src/plugins/field_formats/common/converters/currency.test.ts new file mode 100644 index 0000000000000..b36ce0ed8b2f2 --- /dev/null +++ b/src/plugins/field_formats/common/converters/currency.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CurrencyFormat } from './currency'; +import { FORMATS_UI_SETTINGS } from '../constants/ui_settings'; +import { FieldFormatsGetConfigFn } from '../types'; + +describe('CurrencyFormat', () => { + const config: { [key: string]: string } = { + [FORMATS_UI_SETTINGS.FORMAT_CURRENCY_DEFAULT_PATTERN]: '($0,0.[00])', + }; + + const getConfig: FieldFormatsGetConfigFn = (key: string) => config[key]; + + test('default pattern', () => { + const formatter = new CurrencyFormat({}, getConfig); + + expect(formatter.convert(12000.23)).toBe('$12,000.23'); + }); + + test('custom pattern', () => { + const formatter = new CurrencyFormat({ pattern: '$0.[0]' }, getConfig); + + expect(formatter.convert('12000.23')).toBe('$12000.2'); + }); +}); diff --git a/src/plugins/field_formats/common/converters/currency.ts b/src/plugins/field_formats/common/converters/currency.ts new file mode 100644 index 0000000000000..0dee37ab46e97 --- /dev/null +++ b/src/plugins/field_formats/common/converters/currency.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { NumeralFormat } from './numeral'; +import { FIELD_FORMAT_IDS } from '../types'; + +/** @public */ +export class CurrencyFormat extends NumeralFormat { + static id = FIELD_FORMAT_IDS.CURRENCY; + static title = i18n.translate('fieldFormats.currency.title', { + defaultMessage: 'Currency', + }); + + id = CurrencyFormat.id; + title = CurrencyFormat.title; + allowsNumericalAggregations = true; +} diff --git a/src/plugins/field_formats/common/converters/index.ts b/src/plugins/field_formats/common/converters/index.ts index 9babe212afac0..0fe21a32b22a7 100644 --- a/src/plugins/field_formats/common/converters/index.ts +++ b/src/plugins/field_formats/common/converters/index.ts @@ -8,6 +8,7 @@ export { UrlFormat } from './url'; export { BytesFormat } from './bytes'; +export { CurrencyFormat } from './currency'; export { RelativeDateFormat } from './relative_date'; export { DurationFormat } from './duration'; export { GeoPointFormat } from './geo_point'; diff --git a/src/plugins/field_formats/common/types.ts b/src/plugins/field_formats/common/types.ts index 04c655c972af0..f52545408976e 100644 --- a/src/plugins/field_formats/common/types.ts +++ b/src/plugins/field_formats/common/types.ts @@ -61,6 +61,7 @@ export enum FIELD_FORMAT_IDS { BOOLEAN = 'boolean', BYTES = 'bytes', COLOR = 'color', + CURRENCY = 'currency', CUSTOM = 'custom', DATE = 'date', DATE_NANOS = 'date_nanos', diff --git a/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap index 41b2596ccf7cf..f2ff66a38f7dc 100644 --- a/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_types/metric/public/__snapshots__/to_ast.test.ts.snap @@ -59,7 +59,7 @@ Object { false, ], }, - "function": "metricVis", + "function": "legacyMetricVis", "type": "function", }, ], @@ -123,7 +123,7 @@ Object { false, ], }, - "function": "metricVis", + "function": "legacyMetricVis", "type": "function", }, ], diff --git a/src/plugins/vis_types/metric/public/to_ast.ts b/src/plugins/vis_types/metric/public/to_ast.ts index 09e34d6f80861..0341d5d7b853b 100644 --- a/src/plugins/vis_types/metric/public/to_ast.ts +++ b/src/plugins/vis_types/metric/public/to_ast.ts @@ -52,7 +52,7 @@ export const toExpressionAst: VisToExpressionAst<VisParams> = (vis, params) => { const hasColorRanges = colorsRange && colorsRange.length > 1; - const metricVis = buildExpressionFunction('metricVis', { + const metricVis = buildExpressionFunction('legacyMetricVis', { percentageMode, colorMode: hasColorRanges ? metricColorMode : ColorMode.None, showLabels: labels?.show ?? false, diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 93e811ef5b6a2..bec2383629e7b 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -401,7 +401,7 @@ export class VisualizeChartPageObject extends FtrService { public async getMetric() { const elements = await this.find.allByCssSelector( - '[data-test-subj="visualizationLoader"] .mtrVis__container' + '[data-test-subj="visualizationLoader"] .legacyMtrVis__container' ); const values = await Promise.all( elements.map(async (element) => { diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index ce73208280114..cd683256c623a 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -477,7 +477,7 @@ export class VisualizeEditorPageObject extends FtrService { public async clickMetricByIndex(index: number) { const metrics = await this.find.allByCssSelector( - '[data-test-subj="visualizationLoader"] .mtrVis .mtrVis__container' + '[data-test-subj="visualizationLoader"] .legacyMtrVis .legacyMtrVis__container' ); expect(metrics.length).greaterThan(index); await metrics[index].click(); diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts index 081e5cd5b85c0..73c49525ef4a1 100644 --- a/test/functional/services/dashboard/expectations.ts +++ b/test/functional/services/dashboard/expectations.ts @@ -191,7 +191,7 @@ export class DashboardExpectService extends FtrService { async metricValuesExist(values: string[]) { this.log.debug(`DashboardExpect.metricValuesExist(${values})`); - await this.textWithinCssElementExists(values, '.mtrVis__value'); + await this.textWithinCssElementExists(values, '.legacyMtrVis__value'); } async tsvbMetricValuesExist(values: string[]) { diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index bd0c93b1de057..d88af7db80e0a 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json index e38a14fe2b57e..75922f0e30344 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_empty_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_empty_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json index bb949523e978a..daf08c67211cc 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_invalid_data.json @@ -1 +1 @@ -"[metricVis] > [visdimension] > Provided column name or index is invalid: 0" \ No newline at end of file +"[legacyMetricVis] > [visdimension] > Provided column name or index is invalid: 0" \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 306c5f40b3d25..b28a8eeff2de4 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index 01fe67d1e6a15..096482423cd81 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index bf2ddf5e6e184..e417a1fb95d1f 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index bd0c93b1de057..d88af7db80e0a 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"bytes","params":null},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_empty_data.json b/test/interpreter_functional/snapshots/session/metric_empty_data.json index e38a14fe2b57e..75922f0e30344 100644 --- a/test/interpreter_functional/snapshots/session/metric_empty_data.json +++ b/test/interpreter_functional/snapshots/session/metric_empty_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 306c5f40b3d25..b28a8eeff2de4 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"number"},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index 01fe67d1e6a15..096482423cd81 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":{"colors":["rgb(0,0,0,0)","rgb(100, 100, 100)"],"continuity":"none","gradient":false,"range":"number","rangeMax":10000,"rangeMin":0,"stops":[0,10000]},"percentageMode":true,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index bf2ddf5e6e184..e417a1fb95d1f 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"},{"id":"col-2-1","meta":{"field":"bytes","index":"logstash-*","params":{"id":"bytes","params":null},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"field":"bytes"},"schema":"metric","type":"max"},"type":"number"},"name":"Max bytes"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 5d22d728c0a4a..17357fe60664e 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"metricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"legacyMetricVis","type":"render","value":{"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"autoScale":null,"colorFullBackground":false,"labels":{"position":"bottom","show":true,"style":{"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:24px;line-height:1","spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"24px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}},"metricColorMode":"None","palette":null,"percentageMode":false,"style":{"bgColor":false,"css":"font-family:'Open Sans', Helvetica, Arial, sans-serif;font-weight:normal;font-style:normal;text-decoration:none;text-align:center;font-size:60px;line-height:1","labelColor":false,"spec":{"fontFamily":"'Open Sans', Helvetica, Arial, sans-serif","fontSize":"60px","fontStyle":"normal","fontWeight":"normal","lineHeight":"1","textAlign":"center","textDecoration":"none"},"type":"style"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"hasPrecisionError":false,"id":"1","indexPatternId":"logstash-*","params":{"emptyAsNull":false},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"meta":{"source":"logstash-*","statistics":{"totalCount":14004},"type":"esaggs"},"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.ts b/test/interpreter_functional/test_suites/run_pipeline/basic.ts index 526373aeeaf53..404f8c408984d 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -46,7 +46,7 @@ export default function ({ const expression = `kibana | kibana_context | esaggs index={indexPatternLoad id='logstash-*'} aggs={aggCount id="1" enabled=true schema="metric"} aggs={aggTerms id="2" enabled=true schema="segment" field="response.raw" size=4 order="desc" orderBy="1"} - | metricVis metric={visdimension 1 format="number"} bucket={visdimension 0} + | legacyMetricVis metric={visdimension 1 format="number"} bucket={visdimension 0} `; // we can execute an expression and validate the result manually: @@ -94,7 +94,7 @@ export default function ({ await expectExpression('partial_test_1', tagCloudExpr, context).toMatchSnapshot() ).toMatchScreenshot(); - const metricExpr = `metricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; + const metricExpr = `legacyMetricVis metric={visdimension 1 format="number"} bucket={visdimension 0}`; await ( await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() ).toMatchScreenshot(); diff --git a/test/interpreter_functional/test_suites/run_pipeline/metric.ts b/test/interpreter_functional/test_suites/run_pipeline/metric.ts index 09d0e076d9868..0aaa95e91b54e 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/metric.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/metric.ts @@ -14,7 +14,7 @@ export default function ({ updateBaselines, }: FtrProviderContext & { updateBaselines: boolean }) { let expectExpression: ExpectExpression; - describe('metricVis pipeline expression tests', () => { + describe('legacyMetricVis pipeline expression tests', () => { before(() => { expectExpression = expectExpressionProvider({ getService, updateBaselines }); }); @@ -31,7 +31,7 @@ export default function ({ }); it('with empty data', async () => { - const expression = 'metricVis metric={visdimension 0}'; + const expression = 'legacyMetricVis metric={visdimension 0}'; await ( await expectExpression('metric_empty_data', expression, { ...dataContext, @@ -41,7 +41,7 @@ export default function ({ }); it('with single metric data', async () => { - const expression = 'metricVis metric={visdimension 0}'; + const expression = 'legacyMetricVis metric={visdimension 0}'; await ( await expectExpression( 'metric_single_metric_data', @@ -52,7 +52,7 @@ export default function ({ }); it('with multiple metric data', async () => { - const expression = 'metricVis metric={visdimension 0} metric={visdimension 1}'; + const expression = 'legacyMetricVis metric={visdimension 0} metric={visdimension 1}'; await ( await expectExpression( 'metric_multi_metric_data', @@ -63,7 +63,7 @@ export default function ({ }); it('with metric and bucket data', async () => { - const expression = 'metricVis metric={visdimension 0} bucket={visdimension 2}'; + const expression = 'legacyMetricVis metric={visdimension 0} bucket={visdimension 2}'; await ( await expectExpression('metric_all_data', expression, dataContext).toMatchSnapshot() ).toMatchScreenshot(); @@ -71,7 +71,7 @@ export default function ({ it('with percentageMode option', async () => { const expression = - 'metricVis metric={visdimension 0} percentageMode=true \ + 'legacyMetricVis metric={visdimension 0} percentageMode=true \ palette={palette stop=0 color="rgb(0,0,0,0)" stop=10000 color="rgb(100, 100, 100)" range="number" continuity="none"}'; await ( await expectExpression( @@ -85,7 +85,7 @@ export default function ({ describe('throws error at metric', () => { it('with invalid data', async () => { - const expression = 'metricVis metric={visdimension 0}'; + const expression = 'legacyMetricVis metric={visdimension 0}'; await ( await expectExpression('metric_invalid_data', expression).toMatchSnapshot() ).toMatchScreenshot(); diff --git a/tsconfig.base.json b/tsconfig.base.json index b9b634dd8a607..9946503830c70 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -55,6 +55,8 @@ "@kbn/expression-gauge-plugin/*": ["src/plugins/chart_expressions/expression_gauge/*"], "@kbn/expression-heatmap-plugin": ["src/plugins/chart_expressions/expression_heatmap"], "@kbn/expression-heatmap-plugin/*": ["src/plugins/chart_expressions/expression_heatmap/*"], + "@kbn/expression-legacy-metric-vis-plugin": ["src/plugins/chart_expressions/expression_legacy_metric"], + "@kbn/expression-legacy-metric-vis-plugin/*": ["src/plugins/chart_expressions/expression_legacy_metric/*"], "@kbn/expression-metric-vis-plugin": ["src/plugins/chart_expressions/expression_metric"], "@kbn/expression-metric-vis-plugin/*": ["src/plugins/chart_expressions/expression_metric/*"], "@kbn/expression-partition-vis-plugin": ["src/plugins/chart_expressions/expression_partition_vis"], diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts index f4383bb3e3251..38a1e7567e73f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -32,6 +32,7 @@ import { verticalBarChart } from './vert_bar_chart'; import { verticalProgressBar } from './vertical_progress_bar'; import { verticalProgressPill } from './vertical_progress_pill'; import { tagCloud } from './tag_cloud'; +import { legacyMetricVis } from './metric_vis_legacy'; import { metricVis } from './metric_vis'; import { heatmap } from './heatmap'; @@ -80,5 +81,5 @@ export const initializeElements: SetupInitializer<ElementFactory[]> = (core, plu // For testing purpose. Will be removed after exposing `metricVis` element. export const initializeElementsSpec: SetupInitializer<ElementFactory[]> = (core, plugins) => { const specs = initializeElements(core, plugins); - return [...applyElementStrings([metricVis]), ...specs]; + return [...applyElementStrings([metricVis, legacyMetricVis]), ...specs]; }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts index 3c5a4c16565c6..b95442629079c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis/index.ts @@ -4,18 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import { ElementFactory } from '../../../types'; export const metricVis: ElementFactory = () => ({ name: 'metricVis', - displayName: '(New) Metric Vis', + displayName: 'Metric Vis', type: 'chart', help: 'Metric visualization', icon: 'visMetric', expression: `kibana -| selectFilter -| demodata -| head 1 -| metricVis metric={visdimension "percent_uptime"} colorMode="Labels" -| render`, + | selectFilter + | demodata + | head 1 + | metricVis metric="cost"`, }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis_legacy/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis_legacy/index.ts new file mode 100644 index 0000000000000..d120de1238fbf --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/elements/metric_vis_legacy/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ElementFactory } from '../../../types'; + +export const legacyMetricVis: ElementFactory = () => ({ + name: 'legacyMetricVis', + displayName: 'Legacy metric Vis', + type: 'chart', + help: 'Legacy metric visualization', + icon: 'visMetric', + expression: `kibana +| selectFilter +| demodata +| head 1 +| legacyMetricVis metric={visdimension "percent_uptime"} colorMode="Labels" +| render`, +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts index 403522399e363..59f86910396ee 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/models/metric_vis.ts @@ -14,7 +14,7 @@ import { getState, getValue } from '../../../public/lib/resolved_arg'; const { MetricVis: strings } = ViewStrings; export const metricVis = () => ({ - name: 'metricVis', + name: 'legacyMetricVis', displayName: strings.getDisplayName(), args: [ { diff --git a/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts b/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts index 9e237789eb946..0aaef79bb20f7 100644 --- a/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts +++ b/x-pack/plugins/canvas/i18n/elements/element_strings.test.ts @@ -21,7 +21,9 @@ describe('ElementStrings', () => { }); test('All string definitions should correspond to an existing element', () => { - stringKeys.forEach((key) => expect(elementNames).toContain(key)); + stringKeys.forEach((key) => { + expect(elementNames).toContain(key); + }); }); const strings = Object.values(elementStrings); diff --git a/x-pack/plugins/canvas/i18n/elements/element_strings.ts b/x-pack/plugins/canvas/i18n/elements/element_strings.ts index a1497707a14a2..747e2ee8d0c18 100644 --- a/x-pack/plugins/canvas/i18n/elements/element_strings.ts +++ b/x-pack/plugins/canvas/i18n/elements/element_strings.ts @@ -232,12 +232,20 @@ export const getElementStrings = (): ElementStringDict => ({ }, metricVis: { displayName: i18n.translate('xpack.canvas.elements.metricVisDisplayName', { - defaultMessage: '(New) Metric Vis', + defaultMessage: 'Metric', }), help: i18n.translate('xpack.canvas.elements.metricVisHelpText', { defaultMessage: 'Metric visualization', }), }, + legacyMetricVis: { + displayName: i18n.translate('xpack.canvas.elements.legacyMetricVisDisplayName', { + defaultMessage: 'Legacy Metric Vis', + }), + help: i18n.translate('xpack.canvas.elements.legacyMetricVisHelpText', { + defaultMessage: 'Legacy metric visualization. We recommend using metricVis instead.', + }), + }, heatmap: { displayName: i18n.translate('xpack.canvas.elements.heatmapDisplayName', { defaultMessage: 'Heatmap', diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts index 85f14fee1b4e6..a27f6b9e849e8 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts @@ -372,7 +372,7 @@ describe('metric_visualization', () => { true, ], }, - "function": "metricVis", + "function": "legacyMetricVis", "type": "function", }, ], diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 786d5b588baef..b751fc5bb274e 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -105,7 +105,7 @@ const toExpression = ( ...(datasourceExpression?.chain ?? []), { type: 'function', - function: 'metricVis', + function: 'legacyMetricVis', arguments: { labelPosition: [state?.titlePosition || DEFAULT_TITLE_POSITION], font: [ diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index f40f5321c77a2..7513c33637460 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -3462,23 +3462,11 @@ "expressionMetric.functions.metricHelpText": "Affiche un nombre sur une étiquette.", "expressionMetric.renderer.metric.displayName": "Indicateur", "expressionMetric.renderer.metric.helpDescription": "Présenter un nombre sur une étiquette", - "expressionMetricVis.filterTitle": "Cliquer pour filtrer par champ", - "expressionMetricVis.function.autoScale.help": "Activer le scaling automatique", - "expressionMetricVis.function.bucket.help": "configuration des dimensions de compartiment", - "expressionMetricVis.function.colorFullBackground.help": "Applique la couleur d'arrière-plan sélectionnée à l'intégralité du conteneur de visualisation", - "expressionMetricVis.function.colorMode.help": "La partie de l'indicateur à colorer", "expressionMetricVis.function.dimension.metric": "Indicateur", "expressionMetricVis.function.dimension.splitGroup": "Diviser le groupe", - "expressionMetricVis.function.errors.severalMetricsAndColorFullBackgroundSpecified": "La couleur d'arrière-plan intégrale ne peut pas être appliquée à une visualisation contenant plusieurs indicateurs.", - "expressionMetricVis.function.errors.splitByBucketAndColorFullBackgroundSpecified": "La couleur d'arrière-plan intégrale ne peut pas être appliquée à des visualisations pour lesquelles un compartiment est spécifié.", - "expressionMetricVis.function.font.help": "Paramètres de police.", "expressionMetricVis.function.help": "Visualisation de l'indicateur", - "expressionMetricVis.function.labelFont.help": "Paramètres de police de l'étiquette.", - "expressionMetricVis.function.labelPosition.help": "Position de l'étiquette", "expressionMetricVis.function.metric.help": "configuration des dimensions d’indicateur", "expressionMetricVis.function.palette.help": "Fournit des couleurs pour les valeurs, basées sur les limites.", - "expressionMetricVis.function.percentageMode.help": "Affiche l'indicateur en mode de pourcentage. Nécessite qu’une plage de couleurs soit définie.", - "expressionMetricVis.function.showLabels.help": "Affiche les étiquettes sous les valeurs d’indicateur.", "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "Filtrer sur la valeur", "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}, options de filtre", "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "Exclure la valeur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1957a72941ba1..6259805966410 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3460,23 +3460,11 @@ "expressionMetric.functions.metricHelpText": "ラベルの上に数字を表示します。", "expressionMetric.renderer.metric.displayName": "メトリック", "expressionMetric.renderer.metric.helpDescription": "ラベルの上に数字をレンダリングします", - "expressionMetricVis.filterTitle": "クリックすると、フィールドでフィルタリングします", - "expressionMetricVis.function.autoScale.help": "自動スケールを有効にする", - "expressionMetricVis.function.bucket.help": "バケットディメンションの構成です。", - "expressionMetricVis.function.colorFullBackground.help": "選択した背景色をビジュアライゼーションコンテナー全体に適用します", - "expressionMetricVis.function.colorMode.help": "色を変更するメトリックの部分", "expressionMetricVis.function.dimension.metric": "メトリック", "expressionMetricVis.function.dimension.splitGroup": "グループを分割", - "expressionMetricVis.function.errors.severalMetricsAndColorFullBackgroundSpecified": "全体の背景色は複数のメトリックがあるビジュアライゼーションには適用できません。", - "expressionMetricVis.function.errors.splitByBucketAndColorFullBackgroundSpecified": "全体の背景色はバケットが指定されたビジュアライゼーションには適用できません。", - "expressionMetricVis.function.font.help": "フォント設定です。", "expressionMetricVis.function.help": "メトリックビジュアライゼーション", - "expressionMetricVis.function.labelFont.help": "ラベルフォント設定です。", - "expressionMetricVis.function.labelPosition.help": "ラベル位置", "expressionMetricVis.function.metric.help": "メトリックディメンションの構成です。", "expressionMetricVis.function.palette.help": "境界に基づく値の色を指定します。", - "expressionMetricVis.function.percentageMode.help": "百分率モードでメトリックを表示します。colorRange を設定する必要があります。", - "expressionMetricVis.function.showLabels.help": "メトリック値の下にラベルを表示します。", "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "値でフィルター", "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション", "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "値を除外", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d765633ad18b7..ddee6ff3528b6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3464,23 +3464,11 @@ "expressionMetric.functions.metricHelpText": "在标签上显示数字。", "expressionMetric.renderer.metric.displayName": "指标", "expressionMetric.renderer.metric.helpDescription": "在标签上呈现数字", - "expressionMetricVis.filterTitle": "单击按字段筛选", - "expressionMetricVis.function.autoScale.help": "启用自动缩放", - "expressionMetricVis.function.bucket.help": "存储桶维度配置", - "expressionMetricVis.function.colorFullBackground.help": "将选定背景色应用于全可视化容器", - "expressionMetricVis.function.colorMode.help": "指标的哪部分要上色", "expressionMetricVis.function.dimension.metric": "指标", "expressionMetricVis.function.dimension.splitGroup": "拆分组", - "expressionMetricVis.function.errors.severalMetricsAndColorFullBackgroundSpecified": "无法将全背景着色应用于具有多个指标的可视化。", - "expressionMetricVis.function.errors.splitByBucketAndColorFullBackgroundSpecified": "无法将全背景着色应用于指定了存储桶的可视化。", - "expressionMetricVis.function.font.help": "字体设置。", "expressionMetricVis.function.help": "指标可视化", - "expressionMetricVis.function.labelFont.help": "标签字体设置。", - "expressionMetricVis.function.labelPosition.help": "标签位置", "expressionMetricVis.function.metric.help": "指标维度配置", "expressionMetricVis.function.palette.help": "根据边界为值提供颜色。", - "expressionMetricVis.function.percentageMode.help": "以百分比模式显示指标。需要设置 colorRange。", - "expressionMetricVis.function.showLabels.help": "在指标值下显示标签。", "expressionPartitionVis.legend.filterForValueButtonAriaLabel": "筛留值", "expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项", "expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "筛除值", diff --git a/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts index 57ddc76835213..e648189ce6b9d 100644 --- a/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts +++ b/x-pack/test/functional/apps/dashboard/group1/feature_controls/time_to_visualize_security.ts @@ -130,7 +130,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional/apps/lens/group1/persistent_context.ts b/x-pack/test/functional/apps/lens/group1/persistent_context.ts index 065e1878f1563..eb64a9904b1b8 100644 --- a/x-pack/test/functional/apps/lens/group1/persistent_context.ts +++ b/x-pack/test/functional/apps/lens/group1/persistent_context.ts @@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.navigationalSearch.clickOnOption(0); await PageObjects.lens.waitForEmptyWorkspace(); await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.dragFieldToWorkspace('@timestamp', 'mtrVis'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', 'legacyMtrVis'); }); it('preserves time range', async () => { // fill the navigation search and select empty @@ -121,7 +121,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.clickVisType('lens'); await PageObjects.lens.waitForEmptyWorkspace(); await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.dragFieldToWorkspace('@timestamp', 'mtrVis'); + await PageObjects.lens.dragFieldToWorkspace('@timestamp', 'legacyMtrVis'); const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); expect(timePickerValues.start).to.eql(PageObjects.timePicker.defaultStartTime); diff --git a/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts b/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts index 8ad5cd41e0bec..0d162de68b68a 100644 --- a/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts +++ b/x-pack/test/functional/apps/lens/group2/add_to_dashboard.ts @@ -36,7 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); }; @@ -59,7 +59,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await listingTable.searchForItemWithName('Artistpreviouslyknownaslens'); await PageObjects.lens.clickVisualizeListItemTitle('Artistpreviouslyknownaslens'); await PageObjects.lens.goToTimeRange(); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Maximum of bytes', '19,986'); }; @@ -303,10 +303,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await testSubjects.click('lnsApp_saveButton'); const hasOptions = await testSubjects.exists('add-to-dashboard-options'); @@ -349,10 +349,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await testSubjects.click('lnsApp_saveButton'); const hasOptions = await testSubjects.exists('add-to-dashboard-options'); diff --git a/x-pack/test/functional/apps/lens/group2/epoch_millis.ts b/x-pack/test/functional/apps/lens/group2/epoch_millis.ts index ce773baf27ad9..9096eaa1aab18 100644 --- a/x-pack/test/functional/apps/lens/group2/epoch_millis.ts +++ b/x-pack/test/functional/apps/lens/group2/epoch_millis.ts @@ -43,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'count', field: 'Records', }); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('1'); }); @@ -52,7 +52,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.enableTimeShift(); await PageObjects.lens.setTimeShift('3d'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); expect(await PageObjects.lens.getDatatableCellText(0, 0)).to.eql('2'); }); }); diff --git a/x-pack/test/functional/apps/lens/group3/chart_data.ts b/x-pack/test/functional/apps/lens/group3/chart_data.ts index 6ef40c11407e8..978c98721c18c 100644 --- a/x-pack/test/functional/apps/lens/group3/chart_data.ts +++ b/x-pack/test/functional/apps/lens/group3/chart_data.ts @@ -144,7 +144,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should render metric', async () => { await PageObjects.lens.switchToVisualization('lnsMetric'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Average of bytes', '5,727.322'); }); }); diff --git a/x-pack/test/functional/apps/lens/group3/formula.ts b/x-pack/test/functional/apps/lens/group3/formula.ts index 806e892cec643..c5bdf17f9e154 100644 --- a/x-pack/test/functional/apps/lens/group3/formula.ts +++ b/x-pack/test/functional/apps/lens/group3/formula.ts @@ -174,7 +174,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'formula', }); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); expect(await PageObjects.lens.getErrorCount()).to.eql(0); }); diff --git a/x-pack/test/functional/apps/lens/group3/metrics.ts b/x-pack/test/functional/apps/lens/group3/metrics.ts index 17668206f7c3b..c3b35671f2761 100644 --- a/x-pack/test/functional/apps/lens/group3/metrics.ts +++ b/x-pack/test/functional/apps/lens/group3/metrics.ts @@ -50,21 +50,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.setValue('lnsPalettePanel_dynamicColoring_range_value_1', '21000', { clearWithKeyboard: true, }); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); const styleObj = await PageObjects.lens.getMetricStyle(); expect(styleObj.color).to.be('rgb(32, 146, 128)'); }); it('should change the color when reverting the palette', async () => { await testSubjects.click('lnsPalettePanel_dynamicColoring_reverseColors'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); const styleObj = await PageObjects.lens.getMetricStyle(); expect(styleObj.color).to.be('rgb(204, 86, 66)'); }); it('should reset the color stops when changing palette to a predefined one', async () => { await PageObjects.lens.changePaletteTo('temperature'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); const styleObj = await PageObjects.lens.getMetricStyle(); expect(styleObj.color).to.be('rgb(235, 239, 245)'); }); diff --git a/x-pack/test/functional/apps/lens/group3/rollup.ts b/x-pack/test/functional/apps/lens/group3/rollup.ts index a4bfd39ec4f22..ceb7a94ad9df8 100644 --- a/x-pack/test/functional/apps/lens/group3/rollup.ts +++ b/x-pack/test/functional/apps/lens/group3/rollup.ts @@ -90,12 +90,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { operation: 'sum', field: 'bytes', }); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Sum of bytes', '16,788'); await PageObjects.lens.switchFirstLayerIndexPattern('lens_rolled_up_data'); - await PageObjects.lens.waitForVisualization('mtrVis'); + await PageObjects.lens.waitForVisualization('legacyMtrVis'); await PageObjects.lens.assertMetric('Sum of bytes', '16,788'); }); diff --git a/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts index f9d21f80462e6..a7acd8bf5ba1c 100644 --- a/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts +++ b/x-pack/test/functional/apps/lens/group3/tsvb_open_in_lens.ts @@ -157,7 +157,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const button = await testSubjects.find('visualizeEditInLensButton'); await button.click(); - await lens.waitForVisualization('mtrVis'); + await lens.waitForVisualization('legacyMtrVis'); await retry.try(async () => { const dimensions = await testSubjects.findAll('lns-dimensionTrigger'); expect(await dimensions[1].getVisibleText()).to.be('Count of records'); diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 1f4c20ea6faa5..8b2a7f3aa2ddb 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -256,7 +256,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { if (lensMetricField) { await ml.dataVisualizerTable.assertLensActionShowChart( lensMetricField.fieldName, - 'mtrVis' + 'legacyMtrVis' ); await ml.navigation.browserBackTo('dataVisualizerTable'); } @@ -267,7 +267,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { if (lensNonMetricField) { await ml.dataVisualizerTable.assertLensActionShowChart( lensNonMetricField.fieldName, - 'mtrVis' + 'legacyMtrVis' ); await ml.navigation.browserBackTo('dataVisualizerTable'); } From a349512b1e99365eca291cb677d797b24e7cadec Mon Sep 17 00:00:00 2001 From: Nathan Reese <reese.nathan@elastic.co> Date: Fri, 8 Jul 2022 11:15:53 -0600 Subject: [PATCH 10/21] [maps] convert TileStatusTracker to redux connected react component (#135943) * [maps] convert TileStatusTracker to redux connected react component * eslint * Update x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx Co-authored-by: Nick Peihl <nickpeihl@gmail.com> Co-authored-by: Nick Peihl <nickpeihl@gmail.com> --- .../connected_components/mb_map/index.ts | 16 -- .../connected_components/mb_map/mb_map.tsx | 53 +------ .../mb_map/tile_status_tracker/index.ts | 45 ++++++ .../tile_status_tracker.test.tsx} | 73 +++++---- .../tile_status_tracker.tsx} | 149 ++++++++++++------ .../connected_components/mb_map/utils.ts | 26 --- 6 files changed, 192 insertions(+), 170 deletions(-) create mode 100644 x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/index.ts rename x-pack/plugins/maps/public/connected_components/mb_map/{tile_status_tracker.test.ts => tile_status_tracker/tile_status_tracker.test.tsx} (76%) rename x-pack/plugins/maps/public/connected_components/mb_map/{tile_status_tracker.ts => tile_status_tracker/tile_status_tracker.tsx} (62%) diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index 26ba57c62525b..51ecebd500502 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -15,11 +15,8 @@ import { mapDestroyed, mapExtentChanged, mapReady, - setAreTilesLoaded, - setLayerDataLoadErrorStatus, setMapInitError, setMouseCoordinates, - updateMetaFromTiles, } from '../../actions'; import { getCustomIcons, @@ -34,7 +31,6 @@ import { getDrawMode, getIsFullScreen } from '../../selectors/ui_selectors'; import { getInspectorAdapters, getOnMapMove } from '../../reducers/non_serializable_instances'; import { MapStoreState } from '../../reducers/store'; import { DRAW_MODE } from '../../../common/constants'; -import { TileMetaFeature } from '../../../common/descriptor_types'; import type { MapExtentState } from '../../reducers/map/types'; function mapStateToProps(state: MapStoreState) { @@ -80,18 +76,6 @@ function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyActi setMapInitError(errorMessage: string) { dispatch(setMapInitError(errorMessage)); }, - setAreTilesLoaded(layerId: string, areTilesLoaded: boolean) { - dispatch(setAreTilesLoaded(layerId, areTilesLoaded)); - }, - updateMetaFromTiles(layerId: string, features: TileMetaFeature[]) { - dispatch(updateMetaFromTiles(layerId, features)); - }, - clearTileLoadError(layerId: string) { - dispatch(setLayerDataLoadErrorStatus(layerId, null)); - }, - setTileLoadError(layerId: string, errorMessage: string) { - dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); - }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 15421376933a3..b4c030c62e0fd 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -20,15 +20,8 @@ import { clampToLatBounds, clampToLonBounds } from '../../../common/elasticsearc import { getInitialView } from './get_initial_view'; import { getPreserveDrawingBuffer, isScreenshotMode } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; -import { IVectorSource } from '../../classes/sources/vector_source'; import { MapSettings } from '../../reducers/map'; -import { - CustomIcon, - Goto, - MapCenterAndZoom, - TileMetaFeature, - Timeslice, -} from '../../../common/descriptor_types'; +import { CustomIcon, Goto, MapCenterAndZoom, Timeslice } from '../../../common/descriptor_types'; import { CUSTOM_ICON_SIZE, DECIMAL_DEGREES_PRECISION, @@ -39,7 +32,7 @@ import { import { getGlyphUrl } from '../../util'; import { syncLayerOrder } from './sort_layers'; -import { getTileMetaFeatures, removeOrphanedSourcesAndLayers } from './utils'; +import { removeOrphanedSourcesAndLayers } from './utils'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { TileStatusTracker } from './tile_status_tracker'; import { DrawFeatureControl } from './draw_control/draw_feature_control'; @@ -70,13 +63,9 @@ export interface Props { getActionContext?: () => ActionExecutionContext; onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; renderTooltipContent?: RenderToolTipContent; - setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; timeslice?: Timeslice; - updateMetaFromTiles: (layerId: string, features: TileMetaFeature[]) => void; featureModeActive: boolean; filterModeActive: boolean; - setTileLoadError(layerId: string, errorMessage: string): void; - clearTileLoadError(layerId: string): void; onMapMove?: (lat: number, lon: number, zoom: number) => void; } @@ -93,7 +82,6 @@ export class MbMap extends Component<Props, State> { private _prevLayerList?: ILayer[]; private _prevTimeslice?: Timeslice; private _navigationControl = new maplibregl.NavigationControl({ showCompass: false }); - private _tileStatusTracker?: TileStatusTracker; state: State = { mbMap: undefined, @@ -114,9 +102,6 @@ export class MbMap extends Component<Props, State> { if (this._checker) { this._checker.destroy(); } - if (this._tileStatusTracker) { - this._tileStatusTracker.destroy(); - } if (this.state.mbMap) { this.state.mbMap.remove(); this.state.mbMap = undefined; @@ -124,21 +109,6 @@ export class MbMap extends Component<Props, State> { this.props.onMapDestroyed(); } - // This keeps track of the latest update calls, per layerId - _queryForMeta = (layer: ILayer) => { - const source = layer.getSource(); - if ( - this.state.mbMap && - layer.isVisible() && - source.isESSource() && - typeof (source as IVectorSource).isMvt === 'function' && - (source as IVectorSource).isMvt() - ) { - const features = getTileMetaFeatures(this.state.mbMap, layer.getMbSourceId()); - this.props.updateMetaFromTiles(layer.getId(), features); - } - }; - _debouncedSync = _.debounce(() => { if (this._isMounted && this.props.isMapReady && this.state.mbMap) { const hasLayerListChanged = this._prevLayerList !== this.props.layerList; // Comparing re-select memoized instance so no deep equals needed @@ -203,22 +173,6 @@ export class MbMap extends Component<Props, State> { mbMap.dragRotate.disable(); mbMap.touchZoomRotate.disableRotation(); - this._tileStatusTracker = new TileStatusTracker({ - mbMap, - getCurrentLayerList: () => this.props.layerList, - updateTileStatus: (layer: ILayer, areTilesLoaded: boolean, errorMessage?: string) => { - this.props.setAreTilesLoaded(layer.getId(), areTilesLoaded); - - if (errorMessage) { - this.props.setTileLoadError(layer.getId(), errorMessage); - } else { - this.props.clearTileLoadError(layer.getId()); - } - - this._queryForMeta(layer); - }, - }); - let emptyImage: HTMLImageElement; mbMap.on('styleimagemissing', (e: unknown) => { if (emptyImage) { @@ -472,6 +426,7 @@ export class MbMap extends Component<Props, State> { let tooltipControl; let scaleControl; let keydownScrollZoomControl; + let tileStatusTrackerControl; if (this.state.mbMap) { drawFilterControl = this.props.addFilters && this.props.filterModeActive ? ( @@ -496,6 +451,7 @@ export class MbMap extends Component<Props, State> { keydownScrollZoomControl = this.props.settings.keydownScrollZoom ? ( <KeydownScrollZoom mbMap={this.state.mbMap} /> ) : null; + tileStatusTrackerControl = <TileStatusTracker mbMap={this.state.mbMap} />; } return ( <div @@ -509,6 +465,7 @@ export class MbMap extends Component<Props, State> { {keydownScrollZoomControl} {scaleControl} {tooltipControl} + {tileStatusTrackerControl} </div> ); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/index.ts new file mode 100644 index 0000000000000..7bb209240d56e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/index.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { connect } from 'react-redux'; +import { TileMetaFeature } from '../../../../common/descriptor_types'; +import { + setAreTilesLoaded, + setLayerDataLoadErrorStatus, + updateMetaFromTiles, +} from '../../../actions'; +import { getLayerList } from '../../../selectors/map_selectors'; +import { MapStoreState } from '../../../reducers/store'; +import { TileStatusTracker } from './tile_status_tracker'; + +function mapStateToProps(state: MapStoreState) { + return { + layerList: getLayerList(state), + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyAction>) { + return { + setAreTilesLoaded(layerId: string, areTilesLoaded: boolean) { + dispatch(setAreTilesLoaded(layerId, areTilesLoaded)); + }, + updateMetaFromTiles(layerId: string, features: TileMetaFeature[]) { + dispatch(updateMetaFromTiles(layerId, features)); + }, + clearTileLoadError(layerId: string) { + dispatch(setLayerDataLoadErrorStatus(layerId, null)); + }, + setTileLoadError(layerId: string, errorMessage: string) { + dispatch(setLayerDataLoadErrorStatus(layerId, errorMessage)); + }, + }; +} + +const connected = connect(mapStateToProps, mapDispatchToProps)(TileStatusTracker); +export { connected as TileStatusTracker }; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.test.tsx similarity index 76% rename from x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts rename to x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.test.tsx index 6485582149db7..3279b6e6f2470 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.test.tsx @@ -6,9 +6,11 @@ */ // eslint-disable-next-line max-classes-per-file -import { TileStatusTracker } from './tile_status_tracker'; +import React from 'react'; +import { mount } from 'enzyme'; import type { Map as MbMap } from '@kbn/mapbox-gl'; -import { ILayer } from '../../classes/layers/layer'; +import { TileStatusTracker } from './tile_status_tracker'; +import { ILayer } from '../../../classes/layers/layer'; class MockMbMap { public listeners: Array<{ type: string; callback: (e: unknown) => void }> = []; @@ -49,6 +51,18 @@ class MockLayer { ownsMbSourceId(mbSourceId: string) { return this._mbSourceId === mbSourceId; } + + isVisible() { + return true; + } + + getSource() { + return { + isESSource() { + return false; + }, + }; + } } function createMockLayer(id: string, mbSourceId: string): ILayer { @@ -83,23 +97,35 @@ async function sleep(timeout: number) { }); } +const mockMbMap = new MockMbMap(); +const defaultProps = { + mbMap: mockMbMap as unknown as MbMap, + layerList: [], + setAreTilesLoaded: () => {}, + updateMetaFromTiles: () => {}, + clearTileLoadError: () => {}, + setTileLoadError: () => {}, +}; + describe('TileStatusTracker', () => { - test('should add and remove tiles', async () => { - const mockMbMap = new MockMbMap(); + test('should set tile load status', async () => { + const layerList = [ + createMockLayer('foo', 'foosource'), + createMockLayer('bar', 'barsource'), + createMockLayer('foobar', 'foobarsource'), + ]; const loadedMap: Map<string, boolean> = new Map<string, boolean>(); - new TileStatusTracker({ - mbMap: mockMbMap as unknown as MbMap, - updateTileStatus: (layer, areTilesLoaded) => { - loadedMap.set(layer.getId(), areTilesLoaded); - }, - getCurrentLayerList: () => { - return [ - createMockLayer('foo', 'foosource'), - createMockLayer('bar', 'barsource'), - createMockLayer('foobar', 'foobarsource'), - ]; - }, - }); + const setAreTilesLoaded = (layerId: string, areTilesLoaded: boolean) => { + loadedMap.set(layerId, areTilesLoaded); + }; + + const component = mount( + <TileStatusTracker + {...defaultProps} + layerList={layerList} + setAreTilesLoaded={setAreTilesLoaded} + /> + ); mockMbMap.emit('sourcedataloading', createMockMbDataEvent('foosource', 'aa11')); @@ -126,20 +152,9 @@ describe('TileStatusTracker', () => { expect(loadedMap.get('foo')).toBe(false); // still outstanding tile requests expect(loadedMap.get('bar')).toBe(true); // tiles were aborted or errored expect(loadedMap.has('foobar')).toBe(false); // never received tile requests, status should not have been reported for layer - }); - test('should cleanup listeners on destroy', async () => { - const mockMbMap = new MockMbMap(); - const tileStatusTracker = new TileStatusTracker({ - mbMap: mockMbMap as unknown as MbMap, - updateTileStatus: () => {}, - getCurrentLayerList: () => { - return []; - }, - }); + component.unmount(); - expect(mockMbMap.listeners.length).toBe(4); - tileStatusTracker.destroy(); expect(mockMbMap.listeners.length).toBe(0); }); }); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx similarity index 62% rename from x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts rename to x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx index c349ef0ede3b6..ce23768477993 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker/tile_status_tracker.tsx @@ -5,12 +5,16 @@ * 2.0. */ -import type { Map as MapboxMap, MapSourceDataEvent } from '@kbn/mapbox-gl'; import _ from 'lodash'; +import { Component } from 'react'; +import type { Map as MbMap, MapSourceDataEvent } from '@kbn/mapbox-gl'; import { i18n } from '@kbn/i18n'; -import { ILayer } from '../../classes/layers/layer'; -import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants'; -import { getTileKey } from '../../classes/util/geo_tile_utils'; +import { TileMetaFeature } from '../../../../common/descriptor_types'; +import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; +import { ILayer } from '../../../classes/layers/layer'; +import { IVectorSource } from '../../../classes/sources/vector_source'; +import { getTileKey } from '../../../classes/util/geo_tile_utils'; +import { ES_MVT_META_LAYER_NAME } from '../../../classes/util/tile_meta_feature_utils'; interface MbTile { // references internal object from mapbox @@ -28,32 +32,49 @@ interface Tile { mbTile: MbTile; } -export class TileStatusTracker { +export interface Props { + mbMap: MbMap; + layerList: ILayer[]; + setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; + updateMetaFromTiles: (layerId: string, features: TileMetaFeature[]) => void; + clearTileLoadError: (layerId: string) => void; + setTileLoadError: (layerId: string, errorMessage: string) => void; +} + +export class TileStatusTracker extends Component<Props> { + private _isMounted = false; // Tile cache tracks active tile requests // 'sourcedataloading' event adds tile request to cache // 'sourcedata' and 'error' events remove tile request from cache // Tile requests with 'aborted' status are removed from cache when reporting 'areTilesLoaded' status private _tileCache: Tile[] = []; - // Tile error cache tracks tile request errors per layer // Error cache is cleared when map center tile changes private _tileErrorCache: Record<string, TileError[]> = {}; - // Layer cache tracks layers that have requested one or more tiles // Layer cache is used so that only a layer that has requested one or more tiles reports 'areTilesLoaded' status // layer cache is never cleared private _layerCache: Map<string, boolean> = new Map<string, boolean>(); - private _prevCenterTileKey?: string; - private readonly _mbMap: MapboxMap; - private readonly _updateTileStatus: ( - layer: ILayer, - areTilesLoaded: boolean, - errorMessage?: string - ) => void; - private readonly _getCurrentLayerList: () => ILayer[]; - - private readonly _onSourceDataLoading = (e: MapSourceDataEvent) => { + + componentDidMount() { + this._isMounted = true; + this.props.mbMap.on('sourcedataloading', this._onSourceDataLoading); + this.props.mbMap.on('error', this._onError); + this.props.mbMap.on('sourcedata', this._onSourceData); + this.props.mbMap.on('move', this._onMove); + } + + componentWillUnmount() { + this._isMounted = false; + this.props.mbMap.off('error', this._onError); + this.props.mbMap.off('sourcedata', this._onSourceData); + this.props.mbMap.off('sourcedataloading', this._onSourceDataLoading); + this.props.mbMap.off('move', this._onMove); + this._tileCache.length = 0; + } + + _onSourceDataLoading = (e: MapSourceDataEvent) => { if ( e.sourceId && e.sourceId !== SPATIAL_FILTERS_LAYER_ID && @@ -61,7 +82,7 @@ export class TileStatusTracker { e.tile && (e.source.type === 'vector' || e.source.type === 'raster') ) { - const targetLayer = this._getCurrentLayerList().find((layer) => { + const targetLayer = this.props.layerList.find((layer) => { return layer.ownsMbSourceId(e.sourceId); }); const layerId = targetLayer ? targetLayer.getId() : undefined; @@ -86,14 +107,14 @@ export class TileStatusTracker { } }; - private readonly _onError = (e: MapSourceDataEvent & { error: Error & { status: number } }) => { + _onError = (e: MapSourceDataEvent & { error: Error & { status: number } }) => { if ( e.sourceId && e.sourceId !== SPATIAL_FILTERS_LAYER_ID && e.tile && (e.source.type === 'vector' || e.source.type === 'raster') ) { - const targetLayer = this._getCurrentLayerList().find((layer) => { + const targetLayer = this.props.layerList.find((layer) => { return layer.ownsMbSourceId(e.sourceId); }); const layerId = targetLayer ? targetLayer.getId() : undefined; @@ -109,7 +130,7 @@ export class TileStatusTracker { } }; - private readonly _onSourceData = (e: MapSourceDataEvent) => { + _onSourceData = (e: MapSourceDataEvent) => { if ( e.sourceId && e.sourceId !== SPATIAL_FILTERS_LAYER_ID && @@ -125,44 +146,33 @@ export class TileStatusTracker { * Clear errors when center tile changes. * Tracking center tile provides the cleanest way to know when a new data fetching cycle is beginning */ - private readonly _onMove = () => { - const center = this._mbMap.getCenter(); + _onMove = () => { + const center = this.props.mbMap.getCenter(); // Maplibre rounds zoom when 'source.roundZoom' is true and floors zoom when 'source.roundZoom' is false // 'source.roundZoom' is true for raster and video layers // 'source.roundZoom' is false for vector layers // Always floor zoom to keep logic as simple as possible and not have to track center tile by source. // We are mainly concerned with showing errors from Elasticsearch vector tile requests (which are vector sources) - const centerTileKey = getTileKey(center.lat, center.lng, Math.floor(this._mbMap.getZoom())); + const centerTileKey = getTileKey( + center.lat, + center.lng, + Math.floor(this.props.mbMap.getZoom()) + ); if (this._prevCenterTileKey !== centerTileKey) { this._prevCenterTileKey = centerTileKey; this._tileErrorCache = {}; } }; - constructor({ - mbMap, - updateTileStatus, - getCurrentLayerList, - }: { - mbMap: MapboxMap; - updateTileStatus: (layer: ILayer, areTilesLoaded: boolean, errorMessage?: string) => void; - getCurrentLayerList: () => ILayer[]; - }) { - this._updateTileStatus = updateTileStatus; - this._getCurrentLayerList = getCurrentLayerList; - - this._mbMap = mbMap; - this._mbMap.on('sourcedataloading', this._onSourceDataLoading); - this._mbMap.on('error', this._onError); - this._mbMap.on('sourcedata', this._onSourceData); - this._mbMap.on('move', this._onMove); - } - _updateTileStatusForAllLayers = _.debounce(() => { + if (!this._isMounted) { + return; + } + this._tileCache = this._tileCache.filter((tile) => { return typeof tile.mbTile.aborted === 'boolean' ? !tile.mbTile.aborted : true; }); - const layerList = this._getCurrentLayerList(); + const layerList = this.props.layerList; for (let i = 0; i < layerList.length; i++) { const layer: ILayer = layerList[i]; @@ -191,7 +201,7 @@ export class TileStatusTracker { }); }) : []; - this._updateTileStatus( + this._updateTileStatusForLayer( layer, !atLeastOnePendingTile, tileErrorMessages.length @@ -207,6 +217,47 @@ export class TileStatusTracker { } }, 100); + _updateTileStatusForLayer = (layer: ILayer, areTilesLoaded: boolean, errorMessage?: string) => { + this.props.setAreTilesLoaded(layer.getId(), areTilesLoaded); + + if (errorMessage) { + this.props.setTileLoadError(layer.getId(), errorMessage); + } else { + this.props.clearTileLoadError(layer.getId()); + } + + const source = layer.getSource(); + if ( + layer.isVisible() && + source.isESSource() && + typeof (source as IVectorSource).isMvt === 'function' && + (source as IVectorSource).isMvt() + ) { + // querySourceFeatures can return duplicated features when features cross tile boundaries. + // Tile meta will never have duplicated features since by their nature, tile meta is a feature contained within a single tile + const mbFeatures = this.props.mbMap.querySourceFeatures(layer.getMbSourceId(), { + sourceLayer: ES_MVT_META_LAYER_NAME, + filter: [], + }); + + const features = mbFeatures + .map((mbFeature) => { + try { + return { + type: 'Feature', + id: mbFeature?.id, + geometry: mbFeature?.geometry, // this getter might throw with non-conforming geometries + properties: mbFeature?.properties, + } as TileMetaFeature; + } catch (e) { + return null; + } + }) + .filter((mbFeature: TileMetaFeature | null) => mbFeature !== null) as TileMetaFeature[]; + this.props.updateMetaFromTiles(layer.getId(), features); + } + }; + _removeTileFromCache = (mbSourceId: string, mbKey: string) => { const trackedIndex = this._tileCache.findIndex((tile) => { return tile.mbKey === (mbKey as unknown as string) && tile.mbSourceId === mbSourceId; @@ -218,11 +269,7 @@ export class TileStatusTracker { } }; - destroy() { - this._mbMap.off('error', this._onError); - this._mbMap.off('sourcedata', this._onSourceData); - this._mbMap.off('sourcedataloading', this._onSourceDataLoading); - this._mbMap.off('move', this._onMove); - this._tileCache.length = 0; + render() { + return null; } } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts b/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts index 552a618e11f7e..d42746316aa6c 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts @@ -6,10 +6,8 @@ */ import type { Map as MbMap } from '@kbn/mapbox-gl'; -import { TileMetaFeature } from '../../../common/descriptor_types'; import { isGlDrawLayer } from './sort_layers'; import { ILayer } from '../../classes/layers/layer'; -import { ES_MVT_META_LAYER_NAME } from '../../classes/util/tile_meta_feature_utils'; export function removeOrphanedSourcesAndLayers( mbMap: MbMap, @@ -60,27 +58,3 @@ export function removeOrphanedSourcesAndLayers( } mbSourcesToRemove.forEach((mbSourceId) => mbMap.removeSource(mbSourceId)); } - -export function getTileMetaFeatures(mbMap: MbMap, mbSourceId: string): TileMetaFeature[] { - // querySourceFeatures can return duplicated features when features cross tile boundaries. - // Tile meta will never have duplicated features since by there nature, tile meta is a feature contained within a single tile - const mbFeatures = mbMap.querySourceFeatures(mbSourceId, { - sourceLayer: ES_MVT_META_LAYER_NAME, - filter: [], - }); - - return mbFeatures - .map((mbFeature) => { - try { - return { - type: 'Feature', - id: mbFeature?.id, - geometry: mbFeature?.geometry, // this getter might throw with non-conforming geometries - properties: mbFeature?.properties, - } as TileMetaFeature; - } catch (e) { - return null; - } - }) - .filter((mbFeature) => mbFeature !== null) as TileMetaFeature[]; -} From 52c9de890a46e1a30e3c0a129a446b1c8a41c905 Mon Sep 17 00:00:00 2001 From: Tiago Costa <tiago.costa@elastic.co> Date: Fri, 8 Jul 2022 18:18:32 +0100 Subject: [PATCH 11/21] skip flaky suite (#135914) --- test/functional/apps/console/_console.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index a79f46280dd66..de4ed241f4110 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -155,7 +155,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('with folded/unfolded lines in request body', () => { + // FLAKY: https://github.com/elastic/kibana/issues/135914 + describe.skip('with folded/unfolded lines in request body', () => { const enterRequestWithBody = async () => { await PageObjects.console.enterRequest(); await PageObjects.console.pressEnter(); From c51402f2b3398d0751da0f3fc19f94cc69bedcff Mon Sep 17 00:00:00 2001 From: Tiago Costa <tiago.costa@elastic.co> Date: Fri, 8 Jul 2022 18:23:08 +0100 Subject: [PATCH 12/21] skip flaky suite (#86544) --- .../security_and_spaces/apis/copy_to_space.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts index 48b8b0e114af2..3d8fe2ba18ed6 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/copy_to_space.ts @@ -29,7 +29,8 @@ export default function copyToSpaceSpacesAndSecuritySuite({ getService }: FtrPro createMultiNamespaceTestCases, } = copyToSpaceTestSuiteFactory(es, esArchiver, supertestWithoutAuth); - describe('copy to spaces', () => { + // FLAKY: https://github.com/elastic/kibana/issues/86544 + describe.skip('copy to spaces', () => { [ { spaceId: SPACES.DEFAULT.spaceId, From a24365dfc3167711bad1a06c8108db77df8eaddb Mon Sep 17 00:00:00 2001 From: Michael Dokolin <mikhail.dokolin@elastic.co> Date: Fri, 8 Jul 2022 19:59:37 +0200 Subject: [PATCH 13/21] Update `@kbn/monaco` code owners (#136030) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3caf39ff52f3d..ac57096e148ef 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,6 +57,7 @@ /examples/search_examples/ @elastic/kibana-app-services /packages/kbn-datemath/ @elastic/kibana-app-services /packages/kbn-interpreter/ @elastic/kibana-app-services +/packages/kbn-monaco/ @elastic/kibana-app-services /packages/kbn-react-field/ @elastic/kibana-app-services /packages/kbn-es-query/ @elastic/kibana-app-services /packages/kbn-field-types/ @elastic/kibana-app-services @@ -394,7 +395,6 @@ /x-pack/plugins/watcher/ @elastic/platform-deployment-management /x-pack/plugins/ingest_pipelines/ @elastic/platform-deployment-management /packages/kbn-ace/ @elastic/platform-deployment-management -/packages/kbn-monaco/ @elastic/platform-deployment-management #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/platform-deployment-management # Security Solution From 1b40758e14269471310abe52bc8f664b94a3d84f Mon Sep 17 00:00:00 2001 From: Dominique Clarke <dominique.clarke@elastic.co> Date: Fri, 8 Jul 2022 14:02:14 -0400 Subject: [PATCH 14/21] [Synthetics] Add synthetics overview api route and redux state (#135920) * synthetics - add synthetics overview api route and redux state * Update x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * adjust file structure * adjust codec * Update x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts * adjust types * adjust tests * move route constant * move route constant Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../common/constants/synthetics/rest_api.ts | 1 + .../monitor_management/monitor_types.ts | 17 ++ .../public/apps/synthetics/state/index.ts | 1 + .../synthetics/state/monitor_list/index.ts | 1 + .../apps/synthetics/state/overview/actions.ts | 16 ++ .../apps/synthetics/state/overview/api.ts | 25 +++ .../apps/synthetics/state/overview/effects.ts | 22 +++ .../apps/synthetics/state/overview/index.ts | 57 ++++++ .../apps/synthetics/state/overview/models.ts | 10 ++ .../synthetics/state/overview/selectors.ts | 13 ++ .../apps/synthetics/state/root_effect.ts | 2 + .../apps/synthetics/state/root_reducer.ts | 2 + .../__mocks__/syncthetics_store.mock.ts | 12 ++ .../plugins/synthetics/server/routes/index.ts | 2 + .../routes/monitor_cruds/get_monitor.ts | 170 +++++++++++++----- .../uptime/rest/fixtures/http_monitor.json | 30 +++- .../apis/uptime/rest/get_monitor_overview.ts | 122 +++++++++++++ .../api_integration/apis/uptime/rest/index.ts | 1 + 18 files changed, 447 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/overview/actions.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/state/overview/selectors.ts create mode 100644 x-pack/test/api_integration/apis/uptime/rest/get_monitor_overview.ts diff --git a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts index e05556ce22c37..ed8ff0bc50e0d 100644 --- a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts +++ b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts @@ -7,4 +7,5 @@ export enum SYNTHETICS_API_URLS { MONITOR_STATUS = `/internal/synthetics/monitor/status`, + SYNTHETICS_OVERVIEW = '/internal/synthetics/overview', } diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 958fc0a5c0a17..62af6aadc8ba3 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -355,6 +355,23 @@ export const MonitorManagementListResultCodec = t.type({ export type MonitorManagementListResult = t.TypeOf<typeof MonitorManagementListResultCodec>; +export const MonitorOverviewResultCodec = t.type({ + total: t.number, + allMonitorIds: t.array(t.string), + pages: t.record( + t.number, + t.array( + t.interface({ + name: t.string, + id: t.string, + location: MonitorServiceLocationsCodec, + }) + ) + ), +}); + +export type MonitorOverviewResult = t.TypeOf<typeof MonitorOverviewResultCodec>; + export const SyntheticsMonitorWithSecretsCodec = t.intersection([ EncryptedSyntheticsMonitorCodec, t.interface({ diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts index c4b2f0afb0b6f..6076292c34550 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts @@ -16,3 +16,4 @@ export * from './synthetics_enablement'; export * from './service_locations'; export * from './monitor_list'; export * from './monitor_summary'; +export * from './overview'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts index 94fecac414d90..dc3bd19b41b99 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts @@ -49,6 +49,7 @@ export const monitorListReducer = createReducer(initialState, (builder) => { }); }); +export * from './api'; export * from './models'; export * from './actions'; export * from './effects'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/actions.ts new file mode 100644 index 0000000000000..cef0570885691 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/actions.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAsyncAction } from '../utils/actions'; + +import { MonitorOverviewPageState } from './models'; +import { MonitorOverviewResult } from '../../../../../common/runtime_types'; + +export const fetchMonitorOverviewAction = createAsyncAction< + MonitorOverviewPageState, + MonitorOverviewResult +>('fetchMonitorOverivewAction'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts new file mode 100644 index 0000000000000..f9d3511a4f5af --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/api.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SYNTHETICS_API_URLS } from '../../../../../common/constants'; +import { + MonitorOverviewResult, + MonitorOverviewResultCodec, +} from '../../../../../common/runtime_types'; +import { apiService } from '../../../../utils/api_service'; + +import { MonitorOverviewPageState } from './models'; + +export const fetchMonitorOverview = async ( + pageState: MonitorOverviewPageState +): Promise<MonitorOverviewResult> => { + return await apiService.get( + SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW, + { perPage: pageState.perPage }, + MonitorOverviewResultCodec + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts new file mode 100644 index 0000000000000..c110ff2b67088 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/effects.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { takeLeading } from 'redux-saga/effects'; +import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchMonitorOverviewAction } from './actions'; +import { fetchMonitorOverview } from './api'; + +export function* fetchMonitorOverviewEffect() { + yield takeLeading( + fetchMonitorOverviewAction.get, + fetchEffectFactory( + fetchMonitorOverview, + fetchMonitorOverviewAction.success, + fetchMonitorOverviewAction.fail + ) + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts new file mode 100644 index 0000000000000..875fe8f273693 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReducer } from '@reduxjs/toolkit'; + +import { MonitorOverviewResult } from '../../../../../common/runtime_types'; + +import { IHttpSerializedFetchError, serializeHttpFetchError } from '../utils/http_error'; + +import { MonitorOverviewPageState } from './models'; +import { fetchMonitorOverviewAction } from './actions'; + +export interface MonitorOverviewState { + data: MonitorOverviewResult; + pageState: MonitorOverviewPageState; + loading: boolean; + error: IHttpSerializedFetchError | null; +} + +const initialState: MonitorOverviewState = { + data: { + total: 0, + allMonitorIds: [], + pages: {}, + }, + pageState: { + perPage: 20, + }, + loading: false, + error: null, +}; + +export const monitorOverviewReducer = createReducer(initialState, (builder) => { + builder + .addCase(fetchMonitorOverviewAction.get, (state, action) => { + state.pageState = action.payload; + state.loading = true; + }) + .addCase(fetchMonitorOverviewAction.success, (state, action) => { + state.loading = false; + state.data = action.payload; + }) + .addCase(fetchMonitorOverviewAction.fail, (state, action) => { + state.loading = false; + state.error = serializeHttpFetchError(action.payload); + }); +}); + +export * from './models'; +export * from './actions'; +export * from './effects'; +export * from './selectors'; +export { fetchMonitorOverview } from './api'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts new file mode 100644 index 0000000000000..d7dcaafeba917 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/models.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface MonitorOverviewPageState { + perPage: number; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/selectors.ts new file mode 100644 index 0000000000000..aec2633788fea --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/overview/selectors.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from 'reselect'; + +import { SyntheticsAppState } from '../root_reducer'; + +export const selectOverviewState = (state: SyntheticsAppState) => state.overview; +export const selectOverviewDataState = createSelector(selectOverviewState, (state) => state.data); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts index 45214cf4d2461..9f76d5c973508 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts @@ -10,6 +10,7 @@ import { fetchMonitorStatusEffect, fetchSyntheticsMonitorEffect } from './monito import { fetchIndexStatusEffect } from './index_status'; import { fetchSyntheticsEnablementEffect } from './synthetics_enablement'; import { fetchMonitorListEffect } from './monitor_list'; +import { fetchMonitorOverviewEffect } from './overview'; import { fetchServiceLocationsEffect } from './service_locations'; export const rootEffect = function* root(): Generator { @@ -20,5 +21,6 @@ export const rootEffect = function* root(): Generator { fork(fetchMonitorListEffect), fork(fetchMonitorStatusEffect), fork(fetchSyntheticsMonitorEffect), + fork(fetchMonitorOverviewEffect), ]); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts index bd4b25b456e93..8fbb92efaaa98 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts @@ -14,6 +14,7 @@ import { indexStatusReducer } from './index_status'; import { syntheticsEnablementReducer } from './synthetics_enablement'; import { monitorListReducer } from './monitor_list'; import { serviceLocationsReducer } from './service_locations'; +import { monitorOverviewReducer } from './overview'; export const rootReducer = combineReducers({ ui: uiReducer, @@ -23,6 +24,7 @@ export const rootReducer = combineReducers({ serviceLocations: serviceLocationsReducer, monitorStatus: monitorStatusReducer, syntheticsMonitor: syntheticsMonitorReducer, + overview: monitorOverviewReducer, }); export type SyntheticsAppState = ReturnType<typeof rootReducer>; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts index 3a9c13f928a76..4ca2633d1165f 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts @@ -77,6 +77,18 @@ export const mockState: SyntheticsAppState = { error: null, loading: false, }, + overview: { + pageState: { + perPage: 10, + }, + data: { + total: 0, + allMonitorIds: [], + pages: {}, + }, + error: null, + loading: false, + }, syntheticsEnablement: { loading: false, error: null, enablement: null }, monitorStatus: { data: null, diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index 250d94d448151..0d74822303033 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -16,6 +16,7 @@ import { } from './synthetics_service/enablement'; import { getAllSyntheticsMonitorRoute, + getSyntheticsMonitorOverviewRoute, getSyntheticsMonitorRoute, } from './monitor_cruds/get_monitor'; import { runOnceSyntheticsMonitorRoute } from './synthetics_service/run_once_monitor'; @@ -38,6 +39,7 @@ export const syntheticsAppRestApiRoutes: UMRestApiRouteFactory[] = [ getServiceLocationsRoute, getSyntheticsMonitorRoute, getAllSyntheticsMonitorRoute, + getSyntheticsMonitorOverviewRoute, installIndexTemplatesRoute, runOnceSyntheticsMonitorRoute, testNowMonitorRoute, diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts index 1bfd1f468fe05..0035ab004f016 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/get_monitor.ts @@ -4,16 +4,71 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { schema, TypeOf } from '@kbn/config-schema'; -import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import { ServiceLocations } from '../../../common/runtime_types'; +import { + SavedObjectsClientContract, + SavedObjectsErrorHelpers, + SavedObjectsFindResponse, +} from '@kbn/core/server'; +import { EncryptedSyntheticsMonitor, ServiceLocations } from '../../../common/runtime_types'; import { monitorAttributes } from '../../../common/types/saved_objects'; import { UMServerLibs } from '../../legacy_uptime/lib/lib'; import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types'; -import { API_URLS } from '../../../common/constants'; +import { API_URLS, SYNTHETICS_API_URLS } from '../../../common/constants'; import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor'; import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors'; +import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters'; + +const querySchema = schema.object({ + page: schema.maybe(schema.number()), + perPage: schema.maybe(schema.number()), + sortField: schema.maybe(schema.string()), + sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), + query: schema.maybe(schema.string()), + filter: schema.maybe(schema.string()), + tags: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + monitorType: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + locations: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + status: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), +}); + +type MonitorsQuery = TypeOf<typeof querySchema>; + +const getMonitors = ( + request: MonitorsQuery, + server: UptimeServerSetup, + savedObjectsClient: SavedObjectsClientContract +): Promise<SavedObjectsFindResponse<EncryptedSyntheticsMonitor>> => { + const { + perPage = 50, + page, + sortField, + sortOrder, + query, + tags, + monitorType, + locations, + filter = '', + } = request as MonitorsQuery; + + const locationFilter = parseLocationFilter(server.syntheticsService.locations, locations); + + const filters = + getFilter('tags', tags) + + getFilter('type', monitorType) + + getFilter('locations.id', locationFilter); + + return savedObjectsClient.find({ + type: syntheticsMonitorType, + perPage, + page, + sortField: sortField === 'schedule.keyword' ? 'schedule.number' : sortField, + sortOrder, + searchFields: ['name', 'tags.text', 'locations.id.text', 'urls'], + search: query ? `${query}*` : undefined, + filter: filters + filter, + }); +}; export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -47,21 +102,6 @@ export const getSyntheticsMonitorRoute: UMRestApiRouteFactory = (libs: UMServerL }, }); -const querySchema = schema.object({ - page: schema.maybe(schema.number()), - perPage: schema.maybe(schema.number()), - sortField: schema.maybe(schema.string()), - sortOrder: schema.maybe(schema.oneOf([schema.literal('desc'), schema.literal('asc')])), - query: schema.maybe(schema.string()), - filter: schema.maybe(schema.string()), - tags: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), - monitorType: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), - locations: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), - status: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), -}); - -type MonitorsQuery = TypeOf<typeof querySchema>; - export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ method: 'GET', path: API_URLS.SYNTHETICS_MONITORS, @@ -69,35 +109,8 @@ export const getAllSyntheticsMonitorRoute: UMRestApiRouteFactory = () => ({ query: querySchema, }, handler: async ({ request, savedObjectsClient, server }): Promise<any> => { - const { - perPage = 50, - page, - sortField, - sortOrder, - query, - tags, - monitorType, - locations, - filter = '', - } = request.query as MonitorsQuery; - - const locationFilter = parseLocationFilter(server.syntheticsService.locations, locations); - - const filters = - getFilter('tags', tags) + - getFilter('type', monitorType) + - getFilter('locations.id', locationFilter); - - const monitorsPromise = savedObjectsClient.find({ - type: syntheticsMonitorType, - perPage, - page, - sortField: sortField === 'schedule.keyword' ? 'schedule.number' : sortField, - sortOrder, - searchFields: ['name', 'tags.text', 'locations.id.text', 'urls'], - search: query ? `${query}*` : undefined, - filter: filters + filter, - }); + const { filters, query } = request.query; + const monitorsPromise = getMonitors(request.query, server, savedObjectsClient); if (filters || query) { const totalMonitorsPromise = savedObjectsClient.find({ @@ -162,3 +175,64 @@ const parseLocationFilter = (serviceLocations: ServiceLocations, locations?: str export const findLocationItem = (query: string, locations: ServiceLocations) => { return locations.find(({ id, label }) => query === id || label === query); }; + +export const getSyntheticsMonitorOverviewRoute: UMRestApiRouteFactory = () => ({ + method: 'GET', + path: SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW, + validate: { + query: querySchema, + }, + handler: async ({ request, savedObjectsClient, server }): Promise<any> => { + const { perPage = 5 } = request.query; + const { saved_objects: monitors } = await getMonitors( + { + perPage: 1000, + sortField: 'name.keyword', + sortOrder: 'asc', + page: 1, + }, + server, + savedObjectsClient + ); + + const allMonitorIds: string[] = []; + const pages: Record<number, unknown[]> = {}; + let currentPage = 1; + let currentItem = 0; + let total = 0; + + monitors.forEach((monitor) => { + /* collect all monitor ids for use + * in filtering overview requests */ + const id = monitor.id; + allMonitorIds.push(id); + + /* for reach location, add a config item */ + const locations = monitor.attributes.locations; + locations.forEach((location) => { + const config = { + id, + name: monitor.attributes.name, + location, + }; + if (!pages[currentPage]) { + pages[currentPage] = [config]; + } else { + pages[currentPage].push(config); + } + currentItem++; + total++; + if (currentItem % perPage === 0) { + currentPage++; + currentItem = 0; + } + }); + }); + + return { + pages, + total, + allMonitorIds, + }; + }, +}); diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json index 78f77bc53b095..665bd92ac6134 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/http_monitor.json @@ -49,16 +49,28 @@ "TLSv1.2" ], "name": "test-monitor-name", - "locations": [{ - "id": "eu-west-01", - "label": "Europe West", - "geo": { - "lat": 33.2343132435, - "lon": 73.2342343434 + "locations": [ + { + "id": "eu-west-01", + "label": "Europe West", + "geo": { + "lat": 33.2343132435, + "lon": 73.2342343434 + }, + "url": "https://example-url.com", + "isServiceManaged": true }, - "url": "https://example-url.com", - "isServiceManaged": true - }], + { + "id": "eu-west-02", + "label": "Europe West", + "geo": { + "lat": 33.2343132435, + "lon": 73.2342343434 + }, + "url": "https://example-url.com", + "isServiceManaged": true + } + ], "namespace": "testnamespace", "revision": 1, "origin": "ui" diff --git a/x-pack/test/api_integration/apis/uptime/rest/get_monitor_overview.ts b/x-pack/test/api_integration/apis/uptime/rest/get_monitor_overview.ts new file mode 100644 index 0000000000000..56d69e8beb033 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/rest/get_monitor_overview.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { SimpleSavedObject } from '@kbn/core/public'; +import { MonitorFields } from '@kbn/synthetics-plugin/common/runtime_types'; +import { SYNTHETICS_API_URLS, API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { getFixtureJson } from './helper/get_fixture_json'; + +export default function ({ getService }: FtrProviderContext) { + describe('[GET] /internal/synthetics/overview', function () { + this.tags('skipCloud'); + + const supertest = getService('supertest'); + + let _monitors: MonitorFields[]; + let monitors: MonitorFields[]; + + const deleteMonitor = async (id: string) => { + try { + await supertest + .delete(`${API_URLS.SYNTHETICS_MONITORS}/${id}`) + .set('kbn-xsrf', 'true') + .expect(200); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + }; + + const saveMonitor = async (monitor: MonitorFields) => { + const res = await supertest + .post(API_URLS.SYNTHETICS_MONITORS) + .set('kbn-xsrf', 'true') + .send(monitor); + + return res.body as SimpleSavedObject<MonitorFields>; + }; + + before(async () => { + await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + const { body } = await supertest + .get(API_URLS.SYNTHETICS_MONITORS) + .set('kbn-xsrf', 'true') + .expect(200); + await Promise.all([ + (body.monitors as Array<SimpleSavedObject<MonitorFields>>).map((monitor) => { + return deleteMonitor(monitor.id); + }), + ]); + + _monitors = [getFixtureJson('http_monitor')]; + }); + + beforeEach(() => { + monitors = []; + for (let i = 0; i < 20; i++) { + monitors.push({ + ..._monitors[0], + name: `${_monitors[0].name}${i}`, + }); + } + }); + + describe('returns total number of monitor combinations', () => { + it('returns the correct response', async () => { + let savedMonitors: SimpleSavedObject[] = []; + try { + const savedResponse = await Promise.all(monitors.map(saveMonitor)); + savedMonitors = savedResponse; + + const apiResponse = await supertest.get( + SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW + '?perPage=20' + ); + + expect(apiResponse.body.total).eql(monitors.length * 2); + expect(apiResponse.body.allMonitorIds.sort()).eql( + savedMonitors.map((monitor) => monitor.id).sort() + ); + expect(apiResponse.body.pages).to.have.keys(['1', '2']); + expect(apiResponse.body.pages[1].length).eql(20); + } finally { + await Promise.all( + savedMonitors.map((monitor) => { + return deleteMonitor(monitor.id); + }) + ); + } + }); + + it('adjusts pagination correctly', async () => { + let savedMonitors: SimpleSavedObject[] = []; + try { + const savedResponse = await Promise.all(monitors.map(saveMonitor)); + savedMonitors = savedResponse; + + const apiResponse = await supertest.get( + SYNTHETICS_API_URLS.SYNTHETICS_OVERVIEW + '?perPage=5' + ); + + expect(apiResponse.body.total).eql(monitors.length * 2); + expect(apiResponse.body.allMonitorIds.sort()).eql( + savedMonitors.map((monitor) => monitor.id).sort() + ); + expect(apiResponse.body.pages).to.have.keys(['1', '2', '3', '4', '5']); + expect(apiResponse.body.pages[1].length).eql(5); + } finally { + await Promise.all( + savedMonitors.map((monitor) => { + return deleteMonitor(monitor.id); + }) + ); + } + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/rest/index.ts b/x-pack/test/api_integration/apis/uptime/rest/index.ts index 891076c1730bc..64be9de4a2c31 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/index.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/index.ts @@ -74,6 +74,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('uptime CRUD routes', () => { loadTestFile(require.resolve('./get_monitor')); + loadTestFile(require.resolve('./get_monitor_overview')); loadTestFile(require.resolve('./add_monitor')); loadTestFile(require.resolve('./add_monitor_project')); loadTestFile(require.resolve('./edit_monitor')); From ea9875dff55bc81ed8b142db7370a31ba2e4c7f3 Mon Sep 17 00:00:00 2001 From: Jack <zizhou.wang@elastic.co> Date: Fri, 8 Jul 2022 14:09:11 -0400 Subject: [PATCH 15/21] [8.4] [Kubernetes Security] Implement hide/show widget toggle and page last updated at (#135667) * Add widgets toggle button and storage value in local storage * Put toggle into separate component * Implement updated at timestamp next to the hide/show widgets button --- .../kubernetes_security/common/constants.ts | 2 + .../common/translations.ts | 8 + .../kubernetes_security_routes/index.test.tsx | 5 + .../kubernetes_security_routes/index.tsx | 301 ++++++++++-------- .../kubernetes_security_routes/styles.ts | 19 +- .../components/percent_widget/styles.ts | 2 +- .../components/widgets_toggle/index.test.tsx | 57 ++++ .../components/widgets_toggle/index.tsx | 29 ++ .../kubernetes_security/public/hooks/index.ts | 1 + .../public/hooks/use_last_updated.ts | 23 ++ 10 files changed, 311 insertions(+), 136 deletions(-) create mode 100644 x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.test.tsx create mode 100644 x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.tsx create mode 100644 x-pack/plugins/kubernetes_security/public/hooks/use_last_updated.ts diff --git a/x-pack/plugins/kubernetes_security/common/constants.ts b/x-pack/plugins/kubernetes_security/common/constants.ts index 52f99f76ddab8..d94e997dd2bf3 100644 --- a/x-pack/plugins/kubernetes_security/common/constants.ts +++ b/x-pack/plugins/kubernetes_security/common/constants.ts @@ -6,6 +6,8 @@ */ export const KUBERNETES_PATH = '/kubernetes' as const; +export const KUBERNETES_TITLE = 'Kubernetes'; +export const LOCAL_STORAGE_HIDE_WIDGETS_KEY = 'kubernetesSecurity:shouldHideWidgets'; export const AGGREGATE_ROUTE = '/internal/kubernetes_security/aggregate'; export const COUNT_ROUTE = '/internal/kubernetes_security/count'; diff --git a/x-pack/plugins/kubernetes_security/common/translations.ts b/x-pack/plugins/kubernetes_security/common/translations.ts index da5bdeb4266fa..6d0a88e5dc904 100644 --- a/x-pack/plugins/kubernetes_security/common/translations.ts +++ b/x-pack/plugins/kubernetes_security/common/translations.ts @@ -21,3 +21,11 @@ export const SEARCH_GROUP_GROUP_BY = i18n.translate( export const SEARCH_GROUP_SORT_BY = i18n.translate('xpack.kubernetesSecurity.searchGroup.sortBy', { defaultMessage: 'Sort by', }); + +export const WIDGET_TOGGLE_SHOW = i18n.translate('xpack.kubernetesSecurity.widgetsToggle.show', { + defaultMessage: 'Show widgets', +}); + +export const WIDGET_TOGGLE_HIDE = i18n.translate('xpack.kubernetesSecurity.widgetsToggle.hide', { + defaultMessage: 'Hide widgets', +}); diff --git a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx index 38e2bb89da854..aa1e3ff802136 100644 --- a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx +++ b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.test.tsx @@ -21,6 +21,10 @@ jest.mock('../percent_widget', () => ({ PercentWidget: () => <div>{'Mock percent widget'}</div>, })); +jest.mock('../../hooks/use_last_updated', () => ({ + useLastUpdated: () => <div>{'Mock updated now'}</div>, +})); + const renderWithRouter = ( initialEntries: MemoryRouterProps['initialEntries'] = ['/kubernetes'] ) => { @@ -66,5 +70,6 @@ describe('Kubernetes security routes', () => { renderWithRouter(); expect(screen.getAllByText('Mock kubernetes widget')).toHaveLength(3); expect(screen.getAllByText('Mock percent widget')).toHaveLength(2); + expect(screen.getAllByText('Mock updated now')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx index 62c4c88dbac9a..1c826b9dcf1fe 100644 --- a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx +++ b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/index.tsx @@ -7,6 +7,7 @@ import React, { useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { EuiBadge, EuiFlexGroup, @@ -15,12 +16,15 @@ import { EuiSpacer, EuiText, EuiTextColor, + EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { euiThemeVars } from '@kbn/ui-theme'; import { KUBERNETES_PATH, + KUBERNETES_TITLE, + LOCAL_STORAGE_HIDE_WIDGETS_KEY, ENTRY_LEADER_INTERACTIVE, ENTRY_LEADER_USER_ID, ENTRY_LEADER_ENTITY_ID, @@ -29,8 +33,10 @@ import { KubernetesWidget } from '../kubernetes_widget'; import { PercentWidget } from '../percent_widget'; import { KubernetesSecurityDeps } from '../../types'; import { AggregateResult } from '../../../common/types/aggregate'; +import { useLastUpdated } from '../../hooks'; import { useStyles } from './styles'; import { TreeViewContainer } from '../tree_view_container'; +import { WidgetsToggle } from '../widgets_toggle'; const KubernetesSecurityRoutesComponent = ({ filter, @@ -38,7 +44,12 @@ const KubernetesSecurityRoutesComponent = ({ globalFilter, renderSessionsView, }: KubernetesSecurityDeps) => { + const [shouldHideWidgets, setShouldHideWidgets] = useLocalStorage( + LOCAL_STORAGE_HIDE_WIDGETS_KEY, + false + ); const styles = useStyles(); + const lastUpdated = useLastUpdated(globalFilter); const onReduceInteractiveAggs = useCallback( (result: AggregateResult[]): Record<string, number> => @@ -64,150 +75,172 @@ const KubernetesSecurityRoutesComponent = ({ [] ); + const handleToggleHideWidgets = useCallback(() => { + setShouldHideWidgets(!shouldHideWidgets); + }, [setShouldHideWidgets, shouldHideWidgets]); + return ( <Switch> <Route strict exact path={KUBERNETES_PATH}> {filter} - <EuiFlexGroup> - <EuiFlexItem> - <KubernetesWidget - title="Clusters" - icon="heatmap" - iconColor="success" - data={4} - isAlert={true} - > - <EuiBadge - color="danger" - href="#" - target="blank" - css={{ - ...styles.widgetBadge, - '.euiBadge__content': { - width: '100%', - '.euiBadge__text': { - display: 'flex', - justifyContent: 'space-between', - }, - }, - }} - > - <div>{'93 alerts '}</div>View alerts - </EuiBadge> - </KubernetesWidget> - </EuiFlexItem> + <EuiFlexGroup gutterSize="none" css={styles.titleSection}> <EuiFlexItem> - <KubernetesWidget title="Nodes" icon="node" iconColor="#9170B8" data={16} /> + <EuiTitle size="l"> + <h1>{KUBERNETES_TITLE}</h1> + </EuiTitle> </EuiFlexItem> - <EuiFlexItem> - <KubernetesWidget title="Pods" icon="package" iconColor="warning" data={775}> - <EuiBadge css={{ ...styles.widgetBadge, justifyContent: 'center' }}> - <EuiTextColor css={{ marginRight: '16px' }} color="success"> - <span css={{ fontWeight: 700 }}>1000</span> - {' live'} - </EuiTextColor> - <span css={{ fontWeight: 700 }}>42</span> - {' disabled'} - </EuiBadge> - </KubernetesWidget> + <EuiFlexItem grow={false} css={styles.titleActions}> + <div css={styles.updatedAt}>{lastUpdated}</div> + <WidgetsToggle + shouldHideWidgets={shouldHideWidgets} + handleToggleHideWidgets={handleToggleHideWidgets} + /> </EuiFlexItem> </EuiFlexGroup> - <EuiSpacer size="m" /> - <EuiFlexGroup css={styles.percentageWidgets}> - <EuiFlexItem> - <PercentWidget - title={ - <> - <EuiText size="xs" css={styles.percentageChartTitle}> - <FormattedMessage - id="xpack.kubernetesSecurity.sessionChart.title" - defaultMessage="Session Interactivity" - /> - </EuiText> - <EuiIconTip - content={ - <FormattedMessage - id="xpack.kubernetesSecurity.sessionChart.tooltip" - defaultMessage="Interactive sessions have a controlling terminal and often - imply that a human is entering the commands." + {!shouldHideWidgets && ( + <> + <EuiFlexGroup> + <EuiFlexItem> + <KubernetesWidget + title="Clusters" + icon="heatmap" + iconColor="success" + data={4} + isAlert={true} + > + <EuiBadge + color="danger" + href="#" + target="blank" + css={{ + ...styles.widgetBadge, + '.euiBadge__content': { + width: '100%', + '.euiBadge__text': { + display: 'flex', + justifyContent: 'space-between', + }, + }, + }} + > + <div>{'93 alerts '}</div>View alerts + </EuiBadge> + </KubernetesWidget> + </EuiFlexItem> + <EuiFlexItem> + <KubernetesWidget title="Nodes" icon="node" iconColor="#9170B8" data={16} /> + </EuiFlexItem> + <EuiFlexItem> + <KubernetesWidget title="Pods" icon="package" iconColor="warning" data={775}> + <EuiBadge css={{ ...styles.widgetBadge, justifyContent: 'center' }}> + <EuiTextColor css={{ marginRight: '16px' }} color="success"> + <span css={{ fontWeight: 700 }}>1000</span> + {' live'} + </EuiTextColor> + <span css={{ fontWeight: 700 }}>42</span> + {' disabled'} + </EuiBadge> + </KubernetesWidget> + </EuiFlexItem> + </EuiFlexGroup> + <EuiSpacer size="m" /> + <EuiFlexGroup css={styles.percentageWidgets}> + <EuiFlexItem> + <PercentWidget + title={ + <> + <EuiText size="xs" css={styles.percentageChartTitle}> + <FormattedMessage + id="xpack.kubernetesSecurity.sessionChart.title" + defaultMessage="Session Interactivity" + /> + </EuiText> + <EuiIconTip + content={ + <FormattedMessage + id="xpack.kubernetesSecurity.sessionChart.tooltip" + defaultMessage="Interactive sessions have a controlling terminal and often + imply that a human is entering the commands." + /> + } /> - } - /> - </> - } - widgetKey="sessionsPercentage" - indexPattern={indexPattern} - globalFilter={globalFilter} - dataValueMap={{ - true: { - name: i18n.translate('xpack.kubernetesSecurity.sessionChart.interactive', { - defaultMessage: 'Interactive', - }), - fieldName: ENTRY_LEADER_INTERACTIVE, - color: euiThemeVars.euiColorVis0, - }, - false: { - name: i18n.translate('xpack.kubernetesSecurity.sessionChart.nonInteractive', { - defaultMessage: 'Non-interactive', - }), - fieldName: ENTRY_LEADER_INTERACTIVE, - color: euiThemeVars.euiColorVis1, - shouldHideFilter: true, - }, - }} - groupedBy={ENTRY_LEADER_INTERACTIVE} - countBy={ENTRY_LEADER_ENTITY_ID} - onReduce={onReduceInteractiveAggs} - /> - </EuiFlexItem> - <EuiFlexItem> - <PercentWidget - title={ - <> - <EuiText size="xs" css={styles.percentageChartTitle}> - <FormattedMessage - id="xpack.kubernetesSecurity.entryUserChart.title" - defaultMessage="Session Entry Users" - /> - </EuiText> - <EuiIconTip - content={ - <FormattedMessage - id="xpack.kubernetesSecurity.entryUserChart.tooltip" - defaultMessage="The session user is the initial Linux user associated - with the session. This user may be set from authentication of a remote - login or automatically for service sessions started by init." + </> + } + widgetKey="sessionsPercentage" + indexPattern={indexPattern} + globalFilter={globalFilter} + dataValueMap={{ + true: { + name: i18n.translate('xpack.kubernetesSecurity.sessionChart.interactive', { + defaultMessage: 'Interactive', + }), + fieldName: ENTRY_LEADER_INTERACTIVE, + color: euiThemeVars.euiColorVis0, + }, + false: { + name: i18n.translate('xpack.kubernetesSecurity.sessionChart.nonInteractive', { + defaultMessage: 'Non-interactive', + }), + fieldName: ENTRY_LEADER_INTERACTIVE, + color: euiThemeVars.euiColorVis1, + shouldHideFilter: true, + }, + }} + groupedBy={ENTRY_LEADER_INTERACTIVE} + countBy={ENTRY_LEADER_ENTITY_ID} + onReduce={onReduceInteractiveAggs} + /> + </EuiFlexItem> + <EuiFlexItem> + <PercentWidget + title={ + <> + <EuiText size="xs" css={styles.percentageChartTitle}> + <FormattedMessage + id="xpack.kubernetesSecurity.entryUserChart.title" + defaultMessage="Session Entry Users" + /> + </EuiText> + <EuiIconTip + content={ + <FormattedMessage + id="xpack.kubernetesSecurity.entryUserChart.tooltip" + defaultMessage="The session user is the initial Linux user associated + with the session. This user may be set from authentication of a remote + login or automatically for service sessions started by init." + /> + } /> - } - /> - </> - } - widgetKey="rootLoginPercentage" - indexPattern={indexPattern} - globalFilter={globalFilter} - dataValueMap={{ - '0': { - name: i18n.translate('xpack.kubernetesSecurity.entryUserChart.root', { - defaultMessage: 'Root', - }), - fieldName: ENTRY_LEADER_USER_ID, - color: euiThemeVars.euiColorVis2, - }, - nonRoot: { - name: i18n.translate('xpack.kubernetesSecurity.entryUserChart.nonRoot', { - defaultMessage: 'Non-root', - }), - fieldName: ENTRY_LEADER_USER_ID, - color: euiThemeVars.euiColorVis3, - shouldHideFilter: true, - }, - }} - groupedBy={ENTRY_LEADER_USER_ID} - countBy={ENTRY_LEADER_ENTITY_ID} - onReduce={onReduceRootAggs} - /> - </EuiFlexItem> - </EuiFlexGroup> + </> + } + widgetKey="rootLoginPercentage" + indexPattern={indexPattern} + globalFilter={globalFilter} + dataValueMap={{ + '0': { + name: i18n.translate('xpack.kubernetesSecurity.entryUserChart.root', { + defaultMessage: 'Root', + }), + fieldName: ENTRY_LEADER_USER_ID, + color: euiThemeVars.euiColorVis2, + }, + nonRoot: { + name: i18n.translate('xpack.kubernetesSecurity.entryUserChart.nonRoot', { + defaultMessage: 'Non-root', + }), + fieldName: ENTRY_LEADER_USER_ID, + color: euiThemeVars.euiColorVis3, + shouldHideFilter: true, + }, + }} + groupedBy={ENTRY_LEADER_USER_ID} + countBy={ENTRY_LEADER_ENTITY_ID} + onReduce={onReduceRootAggs} + /> + </EuiFlexItem> + </EuiFlexGroup> + </> + )} <TreeViewContainer globalFilter={globalFilter} renderSessionsView={renderSessionsView} /> </Route> </Switch> diff --git a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/styles.ts b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/styles.ts index a6725007fb5f7..3bab1ded50f55 100644 --- a/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/styles.ts +++ b/x-pack/plugins/kubernetes_security/public/components/kubernetes_security_routes/styles.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { CSSObject } from '@emotion/react'; +import type { CSSObject } from '@emotion/react'; import { useEuiTheme } from '../../hooks'; export const useStyles = () => { @@ -15,6 +15,20 @@ export const useStyles = () => { const cached = useMemo(() => { const { size, font } = euiTheme; + const titleSection: CSSObject = { + marginBottom: size.l, + }; + + const titleActions: CSSObject = { + marginLeft: 'auto', + flexDirection: 'row', + alignItems: 'center', + }; + + const updatedAt: CSSObject = { + marginRight: size.m, + }; + const widgetBadge: CSSObject = { position: 'absolute', bottom: size.base, @@ -45,6 +59,9 @@ export const useStyles = () => { }; return { + titleSection, + titleActions, + updatedAt, widgetBadge, treeViewContainer, percentageWidgets, diff --git a/x-pack/plugins/kubernetes_security/public/components/percent_widget/styles.ts b/x-pack/plugins/kubernetes_security/public/components/percent_widget/styles.ts index 57f1427710999..e0725a1d84b4a 100644 --- a/x-pack/plugins/kubernetes_security/public/components/percent_widget/styles.ts +++ b/x-pack/plugins/kubernetes_security/public/components/percent_widget/styles.ts @@ -6,7 +6,7 @@ */ import { useMemo } from 'react'; -import { CSSObject } from '@emotion/react'; +import type { CSSObject } from '@emotion/react'; import { useEuiTheme } from '../../hooks'; export const useStyles = () => { diff --git a/x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.test.tsx b/x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.test.tsx new file mode 100644 index 0000000000000..cb2db24d28faf --- /dev/null +++ b/x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { WIDGET_TOGGLE_SHOW, WIDGET_TOGGLE_HIDE } from '../../../common/translations'; +import { WidgetsToggle, TOGGLE_TEST_ID } from '.'; + +describe('WidgetsToggle component', () => { + let render: () => ReturnType<AppContextTestRender['render']>; + let renderResult: ReturnType<typeof render>; + let mockedContext: AppContextTestRender; + const handleToggleHideWidgets = jest.fn(); + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + }); + + describe('When PercentWidget is mounted', () => { + it('show "hide widgets" text when shouldHideWidgets is false', async () => { + renderResult = mockedContext.render( + <WidgetsToggle + shouldHideWidgets={false} + handleToggleHideWidgets={handleToggleHideWidgets} + /> + ); + + expect(renderResult.getByText(WIDGET_TOGGLE_HIDE)).toBeVisible(); + }); + it('show "show widgets" text when shouldHideWidgets is true', async () => { + renderResult = mockedContext.render( + <WidgetsToggle shouldHideWidgets={true} handleToggleHideWidgets={handleToggleHideWidgets} /> + ); + + expect(renderResult.getByText(WIDGET_TOGGLE_SHOW)).toBeVisible(); + }); + it('shouldHideWidgets defaults to false when not provided', async () => { + renderResult = mockedContext.render( + <WidgetsToggle handleToggleHideWidgets={handleToggleHideWidgets} /> + ); + + expect(renderResult.getByText(WIDGET_TOGGLE_HIDE)).toBeVisible(); + }); + it('clicking the toggle fires the callback', async () => { + renderResult = mockedContext.render( + <WidgetsToggle handleToggleHideWidgets={handleToggleHideWidgets} /> + ); + + renderResult.queryByTestId(TOGGLE_TEST_ID)?.click(); + expect(handleToggleHideWidgets).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.tsx b/x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.tsx new file mode 100644 index 0000000000000..5a31b8504f8ab --- /dev/null +++ b/x-pack/plugins/kubernetes_security/public/components/widgets_toggle/index.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { WIDGET_TOGGLE_SHOW, WIDGET_TOGGLE_HIDE } from '../../../common/translations'; + +export const TOGGLE_TEST_ID = 'kubernetesSecurity:widgetToggle'; + +interface WidgetsToggleDeps { + handleToggleHideWidgets: () => void; + shouldHideWidgets?: boolean; +} + +export const WidgetsToggle = ({ + handleToggleHideWidgets, + shouldHideWidgets = false, +}: WidgetsToggleDeps) => ( + <EuiButtonEmpty + onClick={handleToggleHideWidgets} + iconType={shouldHideWidgets ? 'eye' : 'eyeClosed'} + data-test-subj={TOGGLE_TEST_ID} + > + {shouldHideWidgets ? WIDGET_TOGGLE_SHOW : WIDGET_TOGGLE_HIDE} + </EuiButtonEmpty> +); diff --git a/x-pack/plugins/kubernetes_security/public/hooks/index.ts b/x-pack/plugins/kubernetes_security/public/hooks/index.ts index 1f63ff5b670e5..14d956be9b629 100644 --- a/x-pack/plugins/kubernetes_security/public/hooks/index.ts +++ b/x-pack/plugins/kubernetes_security/public/hooks/index.ts @@ -7,3 +7,4 @@ export { useEuiTheme } from './use_eui_theme'; export { useSetFilter } from './use_filter'; +export { useLastUpdated } from './use_last_updated'; diff --git a/x-pack/plugins/kubernetes_security/public/hooks/use_last_updated.ts b/x-pack/plugins/kubernetes_security/public/hooks/use_last_updated.ts new file mode 100644 index 0000000000000..7879b5a0a1a4a --- /dev/null +++ b/x-pack/plugins/kubernetes_security/public/hooks/use_last_updated.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMemo } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { CoreStart } from '@kbn/core/public'; +import type { GlobalFilter, StartPlugins } from '../types'; + +export const useLastUpdated = (globalFilter: GlobalFilter) => { + const { timelines: timelinesUi } = useKibana<CoreStart & StartPlugins>().services; + + // Only reset updated at on refresh or after globalFilter gets updated + // eslint-disable-next-line react-hooks/exhaustive-deps + const updatedAt = useMemo(() => Date.now(), [globalFilter]); + + return timelinesUi.getLastUpdated({ + updatedAt: updatedAt || Date.now(), + }); +}; From 934f3e78a69e76cf5ef18fece9b7cd05881e626b Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper <Zacqary@users.noreply.github.com> Date: Fri, 8 Jul 2022 13:14:03 -0500 Subject: [PATCH 16/21] [RAM] Expose isRuleSnoozed from @kbn/alerting-plugin (#135949) * [RAM] Expose isRuleSnoozed/getRuleSnoozeEndTime from @kbn/alerting-plugin * Remove getRuleSNoozeEndTime from exports Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerting/server/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 93972dcb8df9d..94e44b110de7e 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -40,7 +40,7 @@ export type { BulkEditOptionsIds, } from './rules_client'; export type { PublicAlert as Alert } from './alert'; -export { parseDuration } from './lib'; +export { parseDuration, isRuleSnoozed } from './lib'; export { getEsErrorMessage } from './lib/errors'; export type { AlertingRulesConfig } from './config'; export { From ed6001dff19ce49ae7790c9501435b3c3ee6d405 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni <rashmi.kulkarni@elastic.co> Date: Fri, 8 Jul 2022 12:12:12 -0700 Subject: [PATCH 17/21] timepicker (#135974) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test/functional/apps/visualize/hybrid_visualization.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/visualize/hybrid_visualization.ts b/x-pack/test/functional/apps/visualize/hybrid_visualization.ts index 41c393398b256..38af8ab61cf93 100644 --- a/x-pack/test/functional/apps/visualize/hybrid_visualization.ts +++ b/x-pack/test/functional/apps/visualize/hybrid_visualization.ts @@ -23,6 +23,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/hybrid/kibana'); await esArchiver.unload('x-pack/test/functional/es_archives/hybrid/logstash'); await esArchiver.unload('x-pack/test/functional/es_archives/hybrid/rollup'); + await PageObjects.common.unsetTime(); }); it('should render histogram line chart', async () => { @@ -77,12 +78,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ['2019-08-22 00:00', 'php', '11'], ['2019-08-22 16:00', 'jpg', '3'], ]; - const fromTime = 'Aug 19, 2019 @ 01:55:07.240'; - const toTime = 'Aug 22, 2019 @ 23:09:36.205'; + const from = 'Aug 19, 2019 @ 01:55:07.240'; + const to = 'Aug 22, 2019 @ 23:09:36.205'; + await PageObjects.common.setTime({ from, to }); await PageObjects.common.navigateToApp('visualize'); await PageObjects.visualize.openSavedVisualization('hybrid_histogram_line_chart'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); await PageObjects.visChart.waitForVisualizationRenderingStabilized(); await inspector.open(); await inspector.setTablePageSize(50); From 88d9a8b61d85d9d651ae38b0877f5c198cf44cdb Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Fri, 8 Jul 2022 12:28:57 -0700 Subject: [PATCH 18/21] Fix state filter not filtering disabled snooze correctly (#134108) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/rule_api/aggregate.test.ts | 6 +-- .../lib/rule_api/map_filters_to_kql.test.ts | 16 ++++---- .../lib/rule_api/map_filters_to_kql.ts | 40 ++++++++----------- .../application/lib/rule_api/rules.test.ts | 8 ++-- .../apps/triggers_actions_ui/alerts_list.ts | 25 +++++++++--- 5 files changed, 52 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index 104f0507aef8e..a75567e42d326 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -243,7 +243,7 @@ describe('loadRuleAggregations', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", + "filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now))", "search": undefined, "search_fields": undefined, }, @@ -262,7 +262,7 @@ describe('loadRuleAggregations', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", + "filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)", "search": undefined, "search_fields": undefined, }, @@ -281,7 +281,7 @@ describe('loadRuleAggregations', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", + "filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)", "search": undefined, "search_fields": undefined, }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index 8d744c84d6f77..58f8e9aa00f7c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -46,29 +46,29 @@ describe('mapFiltersToKql', () => { ruleStatusesFilter: ['enabled'], }) ).toEqual([ - 'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', + '(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now))', ]); expect( mapFiltersToKql({ ruleStatusesFilter: ['disabled'], }) - ).toEqual([ - 'alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', - ]); + ).toEqual(['alert.attributes.enabled: false']); expect( mapFiltersToKql({ ruleStatusesFilter: ['snoozed'], }) - ).toEqual(['(alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)']); + ).toEqual([ + '((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)', + ]); expect( mapFiltersToKql({ ruleStatusesFilter: ['enabled', 'snoozed'], }) ).toEqual([ - 'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', + '(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)', ]); expect( @@ -76,7 +76,7 @@ describe('mapFiltersToKql', () => { ruleStatusesFilter: ['disabled', 'snoozed'], }) ).toEqual([ - 'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', + 'alert.attributes.enabled: false or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)', ]); expect( @@ -84,7 +84,7 @@ describe('mapFiltersToKql', () => { ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'], }) ).toEqual([ - 'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)', + '(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or alert.attributes.enabled: false or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)', ]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index 6629024e3eb11..47e36cbc87e68 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -7,19 +7,6 @@ import { RuleStatus } from '../../../types'; -const getEnablementFilter = (ruleStatusFilter: RuleStatus[] = []) => { - const enablementFilters = ruleStatusFilter.reduce<string[]>((result, filter) => { - if (filter === 'enabled') { - return [...result, 'true']; - } - if (filter === 'disabled') { - return [...result, 'false']; - } - return result; - }, []); - return `alert.attributes.enabled:(${enablementFilters.join(' or ')})`; -}; - export const mapFiltersToKql = ({ typesFilter, actionTypesFilter, @@ -56,20 +43,27 @@ export const mapFiltersToKql = ({ } if (ruleStatusesFilter && ruleStatusesFilter.length) { - const enablementFilter = getEnablementFilter(ruleStatusesFilter); const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)`; - const hasEnablement = - ruleStatusesFilter.includes('enabled') || ruleStatusesFilter.includes('disabled'); - const hasSnoozed = ruleStatusesFilter.includes('snoozed'); + const enabledFilter = `(alert.attributes.enabled: true AND NOT ${snoozedFilter})`; + const disabledFilter = `alert.attributes.enabled: false`; + + const result = []; - if (hasEnablement && !hasSnoozed) { - filters.push(`${enablementFilter} and not ${snoozedFilter}`); - } else if (!hasEnablement && hasSnoozed) { - filters.push(snoozedFilter); - } else { - filters.push(`${enablementFilter} or ${snoozedFilter}`); + if (ruleStatusesFilter.includes('enabled')) { + result.push(enabledFilter); } + + if (ruleStatusesFilter.includes('disabled')) { + result.push(disabledFilter); + } + + if (ruleStatusesFilter.includes('snoozed')) { + result.push(`(${snoozedFilter} AND NOT ${disabledFilter})`); + } + + filters.push(result.join(' or ')); } + if (tagsFilter && tagsFilter.length) { filters.push(`alert.attributes.tags:(${tagsFilter.join(' or ')})`); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index e06ee24464d78..26557bdf96660 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -240,7 +240,7 @@ describe('loadRules', () => { `); }); - test('should call find API with ruleStatusesilter', async () => { + test('should call find API with ruleStatusesFilter', async () => { const resolvedValue = { page: 1, per_page: 10, @@ -266,7 +266,7 @@ describe('loadRules', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", + "filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)", "page": 1, "per_page": 10, "search": undefined, @@ -295,7 +295,7 @@ describe('loadRules', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", + "filter": "alert.attributes.enabled: false", "page": 1, "per_page": 10, "search": undefined, @@ -324,7 +324,7 @@ describe('loadRules', () => { Object { "query": Object { "default_search_operator": "AND", - "filter": "alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)", + "filter": "(alert.attributes.enabled: true AND NOT (alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now)) or alert.attributes.enabled: false or ((alert.attributes.muteAll:true OR alert.attributes.isSnoozedUntil > now) AND NOT alert.attributes.enabled: false)", "page": 1, "per_page": 10, "search": undefined, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index d63390b0a6851..415322024d738 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -580,18 +580,33 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { supertest, objectRemover, }); + const snoozedAndDisabledAlert = await createAlert({ + supertest, + objectRemover, + }); await disableAlert({ supertest, alertId: disabledAlert.id, }); + await snoozeAlert({ supertest, alertId: snoozedAlert.id, }); + await snoozeAlert({ + supertest, + alertId: snoozedAndDisabledAlert.id, + }); + + await disableAlert({ + supertest, + alertId: snoozedAndDisabledAlert.id, + }); + await refreshAlertsList(); - await assertRulesLength(3); + await assertRulesLength(4); // Select enabled await testSubjects.click('ruleStatusFilterButton'); @@ -603,7 +618,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('ruleStatusFilterOption-enabled'); await testSubjects.click('ruleStatusFilterOption-disabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(1); + await assertRulesLength(2); // Select snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); @@ -614,12 +629,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Select disabled and snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(2); + await assertRulesLength(3); - // Select all 3 + // Select all 4 await testSubjects.click('ruleStatusFilterOption-enabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(3); + await assertRulesLength(4); }); it('should filter alerts by the tag', async () => { From 3c13cfe00c3c42f6aa6e053a62e893e05915ab13 Mon Sep 17 00:00:00 2001 From: Justin Kambic <jk@elastic.co> Date: Fri, 8 Jul 2022 15:32:04 -0400 Subject: [PATCH 19/21] Move `use_status_by_location` hook to main hooks directory and allow it to take arbitrary monitor IDs as an arg. (#135957) --- .../tabs_content/locations_status.tsx | 2 +- .../hooks/use_status_by_location.tsx | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) rename x-pack/plugins/synthetics/public/{apps/synthetics/components/monitor_summary => }/hooks/use_status_by_location.tsx (82%) diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx index 0f7ef9dca9e55..8bf7a3975ed0c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/tabs_content/locations_status.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiBadge, EuiBadgeGroup, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import { useTheme } from '@kbn/observability-plugin/public'; -import { useStatusByLocation } from '../hooks/use_status_by_location'; +import { useStatusByLocation } from '../../../../../hooks/use_status_by_location'; export const LocationsStatus = () => { const { locations, loading } = useStatusByLocation(); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/hooks/use_status_by_location.tsx b/x-pack/plugins/synthetics/public/hooks/use_status_by_location.tsx similarity index 82% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/hooks/use_status_by_location.tsx rename to x-pack/plugins/synthetics/public/hooks/use_status_by_location.tsx index d3da5bc1b35ee..649c53d09e6f0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_summary/hooks/use_status_by_location.tsx +++ b/x-pack/plugins/synthetics/public/hooks/use_status_by_location.tsx @@ -8,16 +8,16 @@ import { useEsSearch } from '@kbn/observability-plugin/public'; import { useParams } from 'react-router-dom'; import { useMemo } from 'react'; -import { Ping } from '../../../../../../common/runtime_types'; +import { Ping } from '../../common/runtime_types'; import { EXCLUDE_RUN_ONCE_FILTER, getTimeSpanFilter, SUMMARY_FILTER, -} from '../../../../../../common/constants/client_defaults'; -import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; -import { SYNTHETICS_INDEX_PATTERN, UNNAMED_LOCATION } from '../../../../../../common/constants'; +} from '../../common/constants/client_defaults'; +import { SYNTHETICS_INDEX_PATTERN, UNNAMED_LOCATION } from '../../common/constants'; +import { useSyntheticsRefreshContext } from '../apps/synthetics/contexts'; -export function useStatusByLocation() { +export function useStatusByLocation(monitorIdArg?: string) { const { lastRefresh } = useSyntheticsRefreshContext(); const { monitorId } = useParams<{ monitorId: string }>(); @@ -35,7 +35,7 @@ export function useStatusByLocation() { getTimeSpanFilter(), { term: { - config_id: monitorId, + config_id: monitorIdArg ?? monitorId, }, }, ], @@ -60,7 +60,7 @@ export function useStatusByLocation() { }, }, }, - [lastRefresh, monitorId], + [lastRefresh, monitorId, monitorIdArg], { name: 'getMonitorStatusByLocation' } ); From 6b018f797b97276644e27f58dbfac58c2858d2f3 Mon Sep 17 00:00:00 2001 From: Nathan Reese <reese.nathan@elastic.co> Date: Fri, 8 Jul 2022 13:34:56 -0600 Subject: [PATCH 20/21] [maps] fix Tooltip loses pages on refresh (#135593) * [maps] fix Tooltip loses pages on refresh * increase debounce * fix jest test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/actions/data_request_actions.ts | 21 ------- x-pack/plugins/maps/public/actions/index.ts | 1 + .../maps/public/actions/layer_actions.ts | 6 -- .../maps/public/actions/map_actions.ts | 9 --- .../maps/public/actions/tooltip_actions.ts | 55 ++----------------- .../mb_map/tooltip_control/index.ts | 4 ++ .../tooltip_control/tooltip_control.test.tsx | 3 +- .../tooltip_control/tooltip_control.tsx | 40 +++++++++++++- 8 files changed, 51 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 8c521fa40b0cd..b5ce42ebefc09 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -35,7 +35,6 @@ import { getInspectorAdapters, ResultMeta, } from '../reducers/non_serializable_instances'; -import { updateTooltipStateForLayer } from './tooltip_actions'; import { LAYER_DATA_LOAD_ENDED, LAYER_DATA_LOAD_ERROR, @@ -307,12 +306,6 @@ function endDataLoad( }); } - if (dataId === SOURCE_DATA_REQUEST_ID) { - if (layer) { - dispatch(updateTooltipStateForLayer(layer, features)); - } - } - dispatch({ type: LAYER_DATA_LOAD_ENDED, layerId, @@ -352,13 +345,6 @@ function onDataLoadError( }); } - if (dataId === SOURCE_DATA_REQUEST_ID) { - const layer = getLayerById(layerId, getState()); - if (layer) { - dispatch(updateTooltipStateForLayer(layer)); - } - } - dispatch({ type: LAYER_DATA_LOAD_ERROR, layerId, @@ -382,13 +368,6 @@ export function updateSourceDataRequest(layerId: string, newData: object) { newData, }); - if ('features' in newData) { - const layer = getLayerById(layerId, getState()); - if (layer) { - dispatch(updateTooltipStateForLayer(layer, (newData as FeatureCollection).features)); - } - } - dispatch(updateStyleMeta(layerId)); }; } diff --git a/x-pack/plugins/maps/public/actions/index.ts b/x-pack/plugins/maps/public/actions/index.ts index 2568a787b3941..96db1cebe7d39 100644 --- a/x-pack/plugins/maps/public/actions/index.ts +++ b/x-pack/plugins/maps/public/actions/index.ts @@ -22,4 +22,5 @@ export { openOnClickTooltip, closeOnHoverTooltip, openOnHoverTooltip, + updateOpenTooltips, } from './tooltip_actions'; diff --git a/x-pack/plugins/maps/public/actions/layer_actions.ts b/x-pack/plugins/maps/public/actions/layer_actions.ts index fee8754b48d7e..4fb817c699e9e 100644 --- a/x-pack/plugins/maps/public/actions/layer_actions.ts +++ b/x-pack/plugins/maps/public/actions/layer_actions.ts @@ -50,7 +50,6 @@ import { syncDataForLayerId, updateStyleMeta, } from './data_request_actions'; -import { updateTooltipStateForLayer } from './tooltip_actions'; import { Attribution, JoinDescriptor, @@ -243,10 +242,6 @@ export function setLayerVisibility(layerId: string, makeVisible: boolean) { return; } - if (!makeVisible) { - dispatch(updateTooltipStateForLayer(layer)); - } - dispatch({ type: SET_LAYER_VISIBILITY, layerId, @@ -597,7 +592,6 @@ function removeLayerFromLayerList(layerId: string) { layerGettingRemoved.getInFlightRequestTokens().forEach((requestToken) => { dispatch(cancelRequest(requestToken)); }); - dispatch(updateTooltipStateForLayer(layerGettingRemoved)); clearInspectorAdapters(layerGettingRemoved, getInspectorAdapters(getState())); dispatch({ type: REMOVE_LAYER, diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 446e0211cc295..e02ca86324083 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -70,7 +70,6 @@ import { Timeslice, } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; -import { updateTooltipStateForLayer } from './tooltip_actions'; import { isVectorLayer, IVectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE, pushDeletedFeatureId, clearDeletedFeatureIds } from './ui_actions'; import { expandToTileBoundaries, getTilesForExtent } from '../classes/util/geo_tile_utils'; @@ -232,14 +231,6 @@ export function mapExtentChanged(mapExtentState: MapExtentState) { } as MapViewContext, }); - if (prevZoom !== nextZoom) { - getLayerList(getState()).map((layer) => { - if (!layer.showAtZoomLevel(nextZoom)) { - dispatch(updateTooltipStateForLayer(layer)); - } - }); - } - dispatch(syncDataForAllLayers(false)); }; } diff --git a/x-pack/plugins/maps/public/actions/tooltip_actions.ts b/x-pack/plugins/maps/public/actions/tooltip_actions.ts index f1842ade4277e..22deb1fd1e930 100644 --- a/x-pack/plugins/maps/public/actions/tooltip_actions.ts +++ b/x-pack/plugins/maps/public/actions/tooltip_actions.ts @@ -7,14 +7,10 @@ import _ from 'lodash'; import { Dispatch } from 'redux'; -import { Feature } from 'geojson'; import { getOpenTooltips } from '../selectors/map_selectors'; import { SET_OPEN_TOOLTIPS } from './map_action_constants'; -import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../common/constants'; -import { TooltipFeature, TooltipState } from '../../common/descriptor_types'; +import { TooltipState } from '../../common/descriptor_types'; import { MapStoreState } from '../reducers/store'; -import { ILayer } from '../classes/layers/layer'; -import { IVectorLayer, isVectorLayer } from '../classes/layers/vector_layer'; export function closeOnClickTooltip(tooltipId: string) { return (dispatch: Dispatch, getState: () => MapStoreState) => { @@ -64,50 +60,9 @@ export function openOnHoverTooltip(tooltipState: TooltipState) { }; } -export function updateTooltipStateForLayer(layer: ILayer, layerFeatures: Feature[] = []) { - return (dispatch: Dispatch, getState: () => MapStoreState) => { - if (!isVectorLayer(layer)) { - return; - } - - const openTooltips = getOpenTooltips(getState()) - .map((tooltipState) => { - const nextFeatures: TooltipFeature[] = []; - tooltipState.features.forEach((tooltipFeature) => { - if (tooltipFeature.layerId !== layer.getId()) { - // feature from another layer, keep it - nextFeatures.push(tooltipFeature); - } - - const updatedFeature = layerFeatures.find((layerFeature) => { - const isVisible = - layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME] !== undefined - ? layerFeature.properties![FEATURE_VISIBLE_PROPERTY_NAME] - : true; - return ( - isVisible && (layer as IVectorLayer).getFeatureId(layerFeature) === tooltipFeature.id - ); - }); - - if (updatedFeature) { - nextFeatures.push({ - ...tooltipFeature, - mbProperties: { - ...updatedFeature.properties, - }, - }); - } - }); - - return { ...tooltipState, features: nextFeatures }; - }) - .filter((tooltipState) => { - return tooltipState.features.length > 0; - }); - - dispatch({ - type: SET_OPEN_TOOLTIPS, - openTooltips, - }); +export function updateOpenTooltips(openTooltips: TooltipState[]) { + return { + type: SET_OPEN_TOOLTIPS, + openTooltips, }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts index a9281898902e4..95c4e6bf43b83 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/index.ts @@ -15,6 +15,7 @@ import { openOnClickTooltip, closeOnHoverTooltip, openOnHoverTooltip, + updateOpenTooltips, } from '../../../actions'; import { getLayerList, @@ -52,6 +53,9 @@ function mapDispatchToProps(dispatch: ThunkDispatch<MapStoreState, void, AnyActi openOnHoverTooltip(tooltipState: TooltipState) { dispatch(openOnHoverTooltip(tooltipState)); }, + updateOpenTooltips(openTooltips: TooltipState[]) { + dispatch(updateOpenTooltips(openTooltips)); + }, }; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx index fb5e0bf099e13..736d7810757c5 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx @@ -89,6 +89,7 @@ const defaultProps = { openOnClickTooltip: () => {}, closeOnHoverTooltip: () => {}, openOnHoverTooltip: () => {}, + updateOpenTooltips: () => {}, layerList: [mockLayer], isDrawingFilter: false, addFilters: async () => {}, @@ -162,7 +163,7 @@ describe('TooltipControl', () => { test('should un-register all map callbacks on unmount', () => { const component = mount(<TooltipControl {...defaultProps} />); - expect(Object.keys(mockMbMapHandlers).length).toBe(4); + expect(Object.keys(mockMbMapHandlers).length).toBe(5); component.unmount(); expect(Object.keys(mockMbMapHandlers).length).toBe(0); diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index 87e39174f4922..41a69b4d3c065 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -12,6 +12,7 @@ import { Map as MbMap, MapGeoJSONFeature, MapMouseEvent, + MapSourceDataEvent, Point2D, PointLike, } from '@kbn/mapbox-gl'; @@ -19,7 +20,7 @@ import uuid from 'uuid/v4'; import { Geometry } from 'geojson'; import { Filter } from '@kbn/es-query'; import { ActionExecutionContext, Action } from '@kbn/ui-actions-plugin/public'; -import { LON_INDEX, RawValue } from '../../../../common/constants'; +import { LON_INDEX, RawValue, SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; import { TooltipFeature, TooltipState } from '../../../../common/descriptor_types'; import { TooltipPopover } from './tooltip_popover'; import { ILayer } from '../../../classes/layers/layer'; @@ -63,6 +64,7 @@ export interface Props { onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; openTooltips: TooltipState[]; renderTooltipContent?: RenderToolTipContent; + updateOpenTooltips: (openTooltips: TooltipState[]) => void; } export class TooltipControl extends Component<Props, {}> { @@ -73,6 +75,7 @@ export class TooltipControl extends Component<Props, {}> { this.props.mbMap.on('mousemove', this._updateHoverTooltipState); this.props.mbMap.on('click', this._lockTooltip); this.props.mbMap.on('remove', this._setIsMapRemoved); + this.props.mbMap.on('sourcedata', this._onSourceData); } componentWillUnmount() { @@ -80,12 +83,47 @@ export class TooltipControl extends Component<Props, {}> { this.props.mbMap.off('mousemove', this._updateHoverTooltipState); this.props.mbMap.off('click', this._lockTooltip); this.props.mbMap.off('remove', this._setIsMapRemoved); + this.props.mbMap.off('sourcedata', this._onSourceData); } _setIsMapRemoved = () => { this._isMapRemoved = true; }; + _onSourceData = (e: MapSourceDataEvent) => { + if ( + e.sourceId && + e.sourceId !== SPATIAL_FILTERS_LAYER_ID && + e.dataType === 'source' && + (e.source.type === 'vector' || e.source.type === 'geojson') + ) { + // map features changed, update tooltip state to match new map state. + this._updateOpentooltips(); + } + }; + + _updateOpentooltips = _.debounce(() => { + if (this.props.openTooltips.length === 0) { + return; + } + + const openTooltips = this.props.openTooltips + .map((tooltipState) => { + const mbFeatures = this._getMbFeaturesUnderPointer( + this.props.mbMap.project(tooltipState.location) + ); + return { + ...tooltipState, + features: this._getTooltipFeatures(mbFeatures, tooltipState.isLocked, tooltipState.id), + }; + }) + .filter((tooltipState) => { + return tooltipState.features.length > 0; + }); + + this.props.updateOpenTooltips(openTooltips); + }, 300); + _onMouseout = () => { this._updateHoverTooltipState.cancel(); if (!this.props.hasLockedTooltips) { From 0a0b0a18e2c53320e0932d458ac3c59543719ddf Mon Sep 17 00:00:00 2001 From: Yara Tercero <yctercero@users.noreply.github.com> Date: Fri, 8 Jul 2022 13:34:37 -0700 Subject: [PATCH 21/21] [Security Solution] - Update exceptions item UI (#135255) ## Summary Addresses https://github.com/elastic/kibana/issues/135254 --- .../exceptions/add_exception.spec.ts | 8 +- .../exceptions/exceptions_flyout.spec.ts | 13 +- .../cypress/screens/exceptions.ts | 2 - .../cypress/screens/rule_details.ts | 7 +- .../cypress/tasks/rule_details.ts | 10 + .../exception_item/exception_details.test.tsx | 252 ------------- .../exception_item/exception_details.tsx | 121 ------ .../exception_item/exception_entries.test.tsx | 193 ---------- .../exception_item/exception_entries.tsx | 218 ----------- .../viewer/exception_item/index.stories.tsx | 162 -------- .../viewer/exception_item/index.tsx | 127 ------- .../exception_item_card_conditions.test.tsx | 111 ++++++ .../exception_item_card_conditions.tsx | 160 ++++++++ .../exception_item_card_header.test.tsx | 128 +++++++ .../exception_item_card_header.tsx | 82 ++++ .../exception_item_card_meta.test.tsx | 51 +++ .../exception_item_card_meta.tsx | 111 ++++++ .../index.test.tsx | 115 +++--- .../viewer/exception_item_card/index.tsx | 131 +++++++ .../exception_item_card/translations.ts | 140 +++++++ .../viewer/exceptions_viewer_items.test.tsx | 186 +++------ .../viewer/exceptions_viewer_items.tsx | 21 +- .../exceptions/viewer/helpers.test.tsx | 354 ------------------ .../components/exceptions/viewer/helpers.tsx | 166 -------- .../components/exceptions/viewer/index.tsx | 5 +- 25 files changed, 1051 insertions(+), 1823 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx rename x-pack/plugins/security_solution/public/common/components/exceptions/viewer/{exception_item => exception_item_card}/index.test.tsx (57%) create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts index d41e86fb9c96d..814f29622f51a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_exception.spec.ts @@ -14,7 +14,7 @@ import { addExceptionFromFirstAlert, goToClosedAlerts, goToOpenedAlerts } from ' import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; -import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; +import { esArchiverLoad, esArchiverUnload, esArchiverResetKibana } from '../../tasks/es_archiver'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { addsException, @@ -26,15 +26,15 @@ import { } from '../../tasks/rule_details'; import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -import { cleanKibana, deleteAlertsAndRules } from '../../tasks/common'; +import { deleteAlertsAndRules } from '../../tasks/common'; describe('Adds rule exception', () => { const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; before(() => { - cleanKibana(); - login(); + esArchiverResetKibana(); esArchiverLoad('exceptions'); + login(); }); beforeEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index 75d7696140368..9e08313e7e73f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -13,7 +13,11 @@ import { createCustomRule } from '../../tasks/api_calls/rules'; import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { esArchiverLoad, esArchiverResetKibana, esArchiverUnload } from '../../tasks/es_archiver'; import { login, visitWithoutDateRange } from '../../tasks/login'; -import { openExceptionFlyoutFromRuleSettings, goToExceptionsTab } from '../../tasks/rule_details'; +import { + openExceptionFlyoutFromRuleSettings, + goToExceptionsTab, + editException, +} from '../../tasks/rule_details'; import { addExceptionEntryFieldMatchAnyValue, addExceptionEntryFieldValue, @@ -32,7 +36,6 @@ import { EXCEPTION_ITEM_CONTAINER, ADD_EXCEPTIONS_BTN, EXCEPTION_FIELD_LIST, - EDIT_EXCEPTIONS_BTN, EXCEPTION_EDIT_FLYOUT_SAVE_BTN, EXCEPTION_FLYOUT_VERSION_CONFLICT, EXCEPTION_FLYOUT_LIST_DELETED_ERROR, @@ -302,8 +305,7 @@ describe('Exceptions flyout', () => { context('When updating an item with version conflict', () => { it('Displays version conflict error', () => { - cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible'); - cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true }); + editException(); // update exception item via api updateExceptionListItem('simple_list_item', { @@ -334,8 +336,7 @@ describe('Exceptions flyout', () => { context('When updating an item for a list that has since been deleted', () => { it('Displays missing exception list error', () => { - cy.get(EDIT_EXCEPTIONS_BTN).should('be.visible'); - cy.get(EDIT_EXCEPTIONS_BTN).click({ force: true }); + editException(); // delete exception list via api deleteExceptionList(getExceptionList().list_id, getExceptionList().namespace_type); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index 58c070b194002..4a98d59bc5a0f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -5,8 +5,6 @@ * 2.0. */ -export const EDIT_EXCEPTIONS_BTN = '[data-test-subj="exceptionsViewerEditBtn"]'; - export const ADD_EXCEPTIONS_BTN = '[data-test-subj="exceptionsHeaderAddExceptionBtn"]'; export const CLOSE_ALERTS_CHECKBOX = diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index d58451f6f1520..e8fd18c4449b1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -78,7 +78,12 @@ export const RISK_SCORE_OVERRIDE_DETAILS = 'Risk score override'; export const REFERENCE_URLS_DETAILS = 'Reference URLs'; -export const REMOVE_EXCEPTION_BTN = '[data-test-subj="exceptionsViewerDeleteBtn"]'; +export const EXCEPTION_ITEM_ACTIONS_BUTTON = + 'button[data-test-subj="exceptionItemCardHeader-actionButton"]'; + +export const REMOVE_EXCEPTION_BTN = '[data-test-subj="exceptionItemCardHeader-actionItem-delete"]'; + +export const EDIT_EXCEPTION_BTN = '[data-test-subj="exceptionItemCardHeader-actionItem-edit"]'; export const RULE_SWITCH = '[data-test-subj="ruleSwitch"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index 7fda21016205a..159f62778f74e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -29,6 +29,8 @@ import { INDEX_PATTERNS_DETAILS, DETAILS_TITLE, DETAILS_DESCRIPTION, + EXCEPTION_ITEM_ACTIONS_BUTTON, + EDIT_EXCEPTION_BTN, } from '../screens/rule_details'; import { addsFields, closeFieldsBrowser, filterFieldsBrowser } from './fields_browser'; @@ -96,7 +98,15 @@ export const goToExceptionsTab = () => { .should('be.visible'); }; +export const editException = () => { + cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click(); + + cy.get(EDIT_EXCEPTION_BTN).click(); +}; + export const removeException = () => { + cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click(); + cy.get(REMOVE_EXCEPTION_BTN).click(); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx deleted file mode 100644 index 20c58985344f8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.test.tsx +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { mount } from 'enzyme'; -import moment from 'moment-timezone'; - -import { ExceptionDetails } from './exception_details'; -import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock'; -import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { - euiColorLightestShade: '#ece', - }, -}); - -describe('ExceptionDetails', () => { - beforeEach(() => { - moment.tz.setDefault('UTC'); - }); - - afterEach(() => { - moment.tz.setDefault('Browser'); - }); - - test('it renders no comments button if no comments exist', () => { - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = []; - - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={false} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]')).toHaveLength(0); - }); - - test('it renders comments button if comments exist', () => { - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsArrayMock(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={false} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect( - wrapper.find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]') - ).toHaveLength(1); - }); - - test('it renders correct number of comments', () => { - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = [getCommentsArrayMock()[0]]; - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={false} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual( - 'Show (1) Comment' - ); - }); - - test('it renders comments plural if more than one', () => { - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsArrayMock(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={false} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual( - 'Show (2) Comments' - ); - }); - - test('it renders comments show text if "showComments" is false', () => { - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsArrayMock(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={false} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual( - 'Show (2) Comments' - ); - }); - - test('it renders comments hide text if "showComments" is true', () => { - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsArrayMock(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={true} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0).text()).toEqual( - 'Hide (2) Comments' - ); - }); - - test('it invokes "onCommentsClick" when comments button clicked', () => { - const mockOnCommentsClick = jest.fn(); - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsArrayMock(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={true} - onCommentsClick={mockOnCommentsClick} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - const commentsBtn = wrapper.find('[data-test-subj="exceptionsViewerItemCommentsBtn"]').at(0); - commentsBtn.simulate('click'); - - expect(mockOnCommentsClick).toHaveBeenCalledTimes(1); - }); - - test('it renders the operating system if one is specified in the exception item', () => { - const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={true} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('EuiDescriptionListTitle').at(0).text()).toEqual('OS'); - expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('Linux'); - }); - - test('it renders the exception item creator', () => { - const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={true} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('EuiDescriptionListTitle').at(1).text()).toEqual('Date created'); - expect(wrapper.find('EuiDescriptionListDescription').at(1).text()).toEqual( - 'April 20th 2020 @ 15:25:31' - ); - }); - - test('it renders the exception item creation timestamp', () => { - const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={true} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('EuiDescriptionListTitle').at(2).text()).toEqual('Created by'); - expect(wrapper.find('EuiDescriptionListDescription').at(2).text()).toEqual('some user'); - }); - - test('it renders the description if one is included on the exception item', () => { - const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={true} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - expect(wrapper.find('EuiDescriptionListTitle').at(3).text()).toEqual('Description'); - expect(wrapper.find('EuiDescriptionListDescription').at(3).text()).toEqual('some description'); - }); - - test('it renders with Name and Modified info when showName and showModified props are true', () => { - const exceptionItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - exceptionItem.comments = []; - - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionDetails - showComments={false} - onCommentsClick={jest.fn()} - exceptionItem={exceptionItem} - showName={true} - showModified={true} - /> - </ThemeProvider> - ); - - expect(wrapper.find('EuiDescriptionListTitle').at(0).text()).toEqual('Name'); - expect(wrapper.find('EuiDescriptionListDescription').at(0).text()).toEqual('some name'); - - expect(wrapper.find('EuiDescriptionListTitle').at(4).text()).toEqual('Date modified'); - expect(wrapper.find('EuiDescriptionListDescription').at(4).text()).toEqual( - 'April 20th 2020 @ 15:25:31' - ); - - expect(wrapper.find('EuiDescriptionListTitle').at(5).text()).toEqual('Modified by'); - expect(wrapper.find('EuiDescriptionListDescription').at(5).text()).toEqual('some user'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx deleted file mode 100644 index 429f9672aece5..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiFlexItem, - EuiFlexGroup, - EuiDescriptionList, - EuiButtonEmpty, - EuiDescriptionListTitle, - EuiToolTip, -} from '@elastic/eui'; -import React, { useMemo, Fragment } from 'react'; -import styled, { css } from 'styled-components'; - -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import type { DescriptionListItem } from '../../types'; -import { getDescriptionListContent } from '../helpers'; -import * as i18n from '../../translations'; - -const MyExceptionDetails = styled(EuiFlexItem)` - ${({ theme }) => css` - background-color: ${theme.eui.euiColorLightestShade}; - padding: ${theme.eui.euiSize}; - .eventFiltersDescriptionList { - margin: ${theme.eui.euiSize} ${theme.eui.euiSize} 0 ${theme.eui.euiSize}; - } - .eventFiltersDescriptionListTitle { - width: 40%; - margin-top: 0; - margin-bottom: ${theme.eui.euiSizeS}; - } - .eventFiltersDescriptionListDescription { - width: 60%; - margin-top: 0; - margin-bottom: ${theme.eui.euiSizeS}; - } - `} -`; - -const StyledCommentsSection = styled(EuiFlexItem)` - ${({ theme }) => css` - &&& { - margin: ${theme.eui.euiSizeXS} ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSize}; - } - `} -`; - -const ExceptionDetailsComponent = ({ - showComments, - showModified = false, - showName = false, - onCommentsClick, - exceptionItem, -}: { - showComments: boolean; - showModified?: boolean; - showName?: boolean; - exceptionItem: ExceptionListItemSchema; - onCommentsClick: () => void; -}): JSX.Element => { - const descriptionListItems = useMemo( - (): DescriptionListItem[] => getDescriptionListContent(exceptionItem, showModified, showName), - [exceptionItem, showModified, showName] - ); - - const commentsSection = useMemo((): JSX.Element => { - const { comments } = exceptionItem; - if (comments.length > 0) { - return ( - <EuiButtonEmpty - onClick={onCommentsClick} - flush="left" - size="xs" - data-test-subj="exceptionsViewerItemCommentsBtn" - > - {!showComments - ? i18n.COMMENTS_SHOW(comments.length) - : i18n.COMMENTS_HIDE(comments.length)} - </EuiButtonEmpty> - ); - } else { - return <></>; - } - }, [showComments, onCommentsClick, exceptionItem]); - - return ( - <MyExceptionDetails grow={2}> - <EuiFlexGroup direction="column" alignItems="flexStart"> - <EuiFlexItem grow={1} className="eventFiltersDescriptionList"> - <EuiDescriptionList - compressed - type="responsiveColumn" - data-test-subj="exceptionsViewerItemDetails" - > - {descriptionListItems.map((item) => ( - <Fragment key={`${item.title}`}> - <EuiToolTip content={item.title} anchorClassName="eventFiltersDescriptionListTitle"> - <EuiDescriptionListTitle className="eui-textTruncate eui-fullWidth"> - {item.title} - </EuiDescriptionListTitle> - </EuiToolTip> - {item.description} - </Fragment> - ))} - </EuiDescriptionList> - </EuiFlexItem> - <StyledCommentsSection grow={false}>{commentsSection}</StyledCommentsSection> - </EuiFlexGroup> - </MyExceptionDetails> - ); -}; - -ExceptionDetailsComponent.displayName = 'ExceptionDetailsComponent'; - -export const ExceptionDetails = React.memo(ExceptionDetailsComponent); - -ExceptionDetails.displayName = 'ExceptionDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx deleted file mode 100644 index a53be08380698..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.test.tsx +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { mount } from 'enzyme'; - -import { ExceptionEntries } from './exception_entries'; -import { getFormattedEntryMock } from '../../exceptions.mock'; -import { getEmptyValue } from '../../../empty_value'; -import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { euiSize: '10px', euiColorPrimary: '#ece', euiColorDanger: '#ece' }, -}); - -describe('ExceptionEntries', () => { - test('it does NOT render the and badge if only one exception item entry exists', () => { - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={false} - entries={[getFormattedEntryMock()]} - onDelete={jest.fn()} - onEdit={jest.fn()} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(0); - }); - - test('it renders the and badge if more than one exception item exists', () => { - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={false} - entries={[getFormattedEntryMock(), getFormattedEntryMock()]} - onDelete={jest.fn()} - onEdit={jest.fn()} - /> - </ThemeProvider> - ); - - expect(wrapper.find('[data-test-subj="exceptionsViewerAndBadge"]')).toHaveLength(1); - }); - - test('it invokes "onEdit" when edit button clicked', () => { - const mockOnEdit = jest.fn(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={false} - entries={[getFormattedEntryMock()]} - onDelete={jest.fn()} - onEdit={mockOnEdit} - /> - </ThemeProvider> - ); - const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0); - editBtn.simulate('click'); - - expect(mockOnEdit).toHaveBeenCalledTimes(1); - }); - - test('it invokes "onDelete" when delete button clicked', () => { - const mockOnDelete = jest.fn(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={false} - entries={[getFormattedEntryMock()]} - onDelete={mockOnDelete} - onEdit={jest.fn()} - /> - </ThemeProvider> - ); - const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0); - deleteBtn.simulate('click'); - - expect(mockOnDelete).toHaveBeenCalledTimes(1); - }); - - test('it does not render edit button if "disableActions" is "true"', () => { - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={true} - entries={[getFormattedEntryMock()]} - onDelete={jest.fn()} - onEdit={jest.fn()} - /> - </ThemeProvider> - ); - const editBtns = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button'); - - expect(editBtns).toHaveLength(0); - }); - - test('it does not render delete button if "disableActions" is "true"', () => { - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={true} - entries={[getFormattedEntryMock()]} - onDelete={jest.fn()} - onEdit={jest.fn()} - /> - </ThemeProvider> - ); - const deleteBtns = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0); - - expect(deleteBtns).toHaveLength(0); - }); - - test('it renders nested entry', () => { - const parentEntry = getFormattedEntryMock(); - parentEntry.operator = undefined; - parentEntry.value = undefined; - - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={false} - entries={[parentEntry, getFormattedEntryMock(true)]} - onDelete={jest.fn()} - onEdit={jest.fn()} - /> - </ThemeProvider> - ); - - const parentField = wrapper - .find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent') - .at(0); - const parentOperator = wrapper - .find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent') - .at(0); - const parentValue = wrapper - .find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent') - .at(0); - - const nestedField = wrapper - .find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent') - .at(1); - const nestedOperator = wrapper - .find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent') - .at(1); - const nestedValue = wrapper - .find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent') - .at(1); - - expect(parentField.text()).toEqual('host.name'); - expect(parentOperator.text()).toEqual(getEmptyValue()); - expect(parentValue.text()).toEqual(getEmptyValue()); - - expect(nestedField.exists('.euiToolTipAnchor')).toBeTruthy(); - expect(nestedField.text()).toContain('host.name'); - expect(nestedOperator.text()).toEqual('is'); - expect(nestedValue.text()).toEqual('some name'); - }); - - test('it renders non-nested entries', () => { - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionEntries - disableActions={false} - entries={[getFormattedEntryMock()]} - onDelete={jest.fn()} - onEdit={jest.fn()} - /> - </ThemeProvider> - ); - - const field = wrapper - .find('[data-test-subj="exceptionFieldNameCell"] .euiTableCellContent') - .at(0); - const operator = wrapper - .find('[data-test-subj="exceptionFieldOperatorCell"] .euiTableCellContent') - .at(0); - const value = wrapper - .find('[data-test-subj="exceptionFieldValueCell"] .euiTableCellContent') - .at(0); - - expect(field.exists('.euiToolTipAnchor')).toBeFalsy(); - expect(field.text()).toEqual('host.name'); - expect(operator.text()).toEqual('is'); - expect(value.text()).toEqual('some name'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx deleted file mode 100644 index 4db00bea5c932..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiBasicTable, - EuiIconTip, - EuiFlexItem, - EuiFlexGroup, - EuiButton, - EuiTableFieldDataColumnType, - EuiHideFor, - EuiBadge, - EuiBadgeGroup, - EuiToolTip, -} from '@elastic/eui'; -import React, { useMemo } from 'react'; -import styled, { css } from 'styled-components'; -import { transparentize } from 'polished'; - -import { AndOrBadge } from '../../../and_or_badge'; -import { getEmptyValue } from '../../../empty_value'; -import * as i18n from '../../translations'; -import { FormattedEntry } from '../../types'; - -const MyEntriesDetails = styled(EuiFlexItem)` - ${({ theme }) => css` - padding: ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSizeL} ${theme.eui.euiSizeXS}; - &&& { - margin-left: 0; - } - `} -`; - -const MyEditButton = styled(EuiButton)` - ${({ theme }) => css` - background-color: ${transparentize(0.9, theme.eui.euiColorPrimary)}; - border: none; - font-weight: ${theme.eui.euiFontWeightSemiBold}; - `} -`; - -const MyRemoveButton = styled(EuiButton)` - ${({ theme }) => css` - background-color: ${transparentize(0.9, theme.eui.euiColorDanger)}; - border: none; - font-weight: ${theme.eui.euiFontWeightSemiBold}; - `} -`; - -const MyAndOrBadgeContainer = styled(EuiFlexItem)` - ${({ theme }) => css` - padding: ${theme.eui.euiSizeXL} ${theme.eui.euiSize} ${theme.eui.euiSizeS} 0; - `} -`; - -const MyActionButton = styled(EuiFlexItem)` - align-self: flex-end; -`; - -const MyNestedValueContainer = styled.div` - margin-left: ${({ theme }) => theme.eui.euiSizeL}; -`; - -const MyNestedValue = styled.span` - margin-left: ${({ theme }) => theme.eui.euiSizeS}; -`; - -const ValueBadgeGroup = styled(EuiBadgeGroup)` - width: 100%; -`; - -interface ExceptionEntriesComponentProps { - entries: FormattedEntry[]; - disableActions: boolean; - onDelete: () => void; - onEdit: () => void; -} - -const ExceptionEntriesComponent = ({ - entries, - disableActions, - onDelete, - onEdit, -}: ExceptionEntriesComponentProps): JSX.Element => { - const columns = useMemo( - (): Array<EuiTableFieldDataColumnType<FormattedEntry>> => [ - { - field: 'fieldName', - name: 'Field', - sortable: false, - truncateText: true, - textOnly: true, - 'data-test-subj': 'exceptionFieldNameCell', - width: '30%', - render: (value: string | null, data: FormattedEntry) => { - if (value != null && data.isNested) { - return ( - <MyNestedValueContainer> - <EuiIconTip type="nested" size="s" /> - <MyNestedValue>{value}</MyNestedValue> - </MyNestedValueContainer> - ); - } else { - return value ?? getEmptyValue(); - } - }, - }, - { - field: 'operator', - name: 'Operator', - sortable: false, - truncateText: true, - 'data-test-subj': 'exceptionFieldOperatorCell', - width: '20%', - render: (value: string | null) => value ?? getEmptyValue(), - }, - { - field: 'value', - name: 'Value', - sortable: false, - truncateText: true, - 'data-test-subj': 'exceptionFieldValueCell', - width: '60%', - render: (values: string | string[] | null) => { - if (Array.isArray(values)) { - return ( - <ValueBadgeGroup gutterSize="xs"> - {values.map((value) => { - return ( - <EuiBadge color="#DDD" key={value}> - {value} - </EuiBadge> - ); - })} - </ValueBadgeGroup> - ); - } else { - return values ? ( - <EuiToolTip content={values} anchorClassName="eui-textTruncate"> - <span>{values}</span> - </EuiToolTip> - ) : ( - getEmptyValue() - ); - } - }, - }, - ], - [] - ); - - return ( - <MyEntriesDetails grow={5}> - <EuiFlexGroup direction="column" gutterSize="m"> - <EuiFlexItem grow={false}> - <EuiFlexGroup direction="row" gutterSize="none"> - {entries.length > 1 && ( - <EuiHideFor sizes={['xs', 's']}> - <MyAndOrBadgeContainer grow={false}> - <AndOrBadge - type="and" - includeAntennas - data-test-subj="exceptionsViewerAndBadge" - /> - </MyAndOrBadgeContainer> - </EuiHideFor> - )} - <EuiFlexItem grow={1}> - <EuiBasicTable - isSelectable={false} - itemId="id" - columns={columns} - items={entries} - responsive - /> - </EuiFlexItem> - </EuiFlexGroup> - </EuiFlexItem> - {!disableActions && ( - <EuiFlexItem grow={1}> - <EuiFlexGroup gutterSize="s" justifyContent="flexEnd"> - <MyActionButton grow={false}> - <MyEditButton - size="s" - color="primary" - onClick={onEdit} - data-test-subj="exceptionsViewerEditBtn" - > - {i18n.EDIT} - </MyEditButton> - </MyActionButton> - <MyActionButton grow={false}> - <MyRemoveButton - size="s" - color="danger" - onClick={onDelete} - data-test-subj="exceptionsViewerDeleteBtn" - > - {i18n.REMOVE} - </MyRemoveButton> - </MyActionButton> - </EuiFlexGroup> - </EuiFlexItem> - )} - </EuiFlexGroup> - </MyEntriesDetails> - ); -}; - -ExceptionEntriesComponent.displayName = 'ExceptionEntriesComponent'; - -export const ExceptionEntries = React.memo(ExceptionEntriesComponent); - -ExceptionEntries.displayName = 'ExceptionEntries'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx deleted file mode 100644 index 30ca4428aa008..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { storiesOf, addDecorator } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-theme'; - -import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock'; -import { ExceptionItem } from '.'; - -addDecorator((storyFn) => ( - <ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>{storyFn()}</ThemeProvider> -)); - -storiesOf('Components/ExceptionItem', module) - .add('with os', () => { - const payload = getExceptionListItemSchemaMock(); - payload.description = ''; - payload.comments = []; - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: 'included', - value: 'Elastic, N.V.', - }, - ]; - - return ( - <ExceptionItem - disableActions={false} - loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - exceptionItem={payload} - onDeleteException={action('onClick')} - onEditException={action('onClick')} - /> - ); - }) - .add('with description', () => { - const payload = getExceptionListItemSchemaMock(); - payload.comments = []; - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: 'included', - value: 'Elastic, N.V.', - }, - ]; - - return ( - <ExceptionItem - disableActions={false} - loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - exceptionItem={payload} - onDeleteException={action('onClick')} - onEditException={action('onClick')} - /> - ); - }) - .add('with comments', () => { - const payload = getExceptionListItemSchemaMock(); - payload.description = ''; - payload.comments = getCommentsArrayMock(); - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: 'included', - value: 'Elastic, N.V.', - }, - ]; - - return ( - <ExceptionItem - disableActions={false} - loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - exceptionItem={payload} - onDeleteException={action('onClick')} - onEditException={action('onClick')} - /> - ); - }) - .add('with nested entries', () => { - const payload = getExceptionListItemSchemaMock(); - payload.description = ''; - payload.comments = []; - - return ( - <ExceptionItem - disableActions={false} - loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - exceptionItem={payload} - onDeleteException={action('onClick')} - onEditException={action('onClick')} - /> - ); - }) - .add('with everything', () => { - const payload = getExceptionListItemSchemaMock(); - payload.comments = getCommentsArrayMock(); - return ( - <ExceptionItem - disableActions={false} - loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - exceptionItem={payload} - onDeleteException={action('onClick')} - onEditException={action('onClick')} - /> - ); - }) - .add('with loadingItemIds', () => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { id, namespace_type, ...rest } = getExceptionListItemSchemaMock(); - - return ( - <ExceptionItem - disableActions={false} - loadingItemIds={[{ id, namespaceType: namespace_type }]} - commentsAccordionId={'accordion--comments'} - exceptionItem={{ id, namespace_type, ...rest }} - onDeleteException={action('onClick')} - onEditException={action('onClick')} - /> - ); - }) - .add('with actions disabled', () => { - const payload = getExceptionListItemSchemaMock(); - payload.description = ''; - payload.comments = getCommentsArrayMock(); - payload.entries = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: 'included', - value: 'Elastic, N.V.', - }, - ]; - - return ( - <ExceptionItem - disableActions - loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - exceptionItem={payload} - onDeleteException={action('onClick')} - onEditException={action('onClick')} - /> - ); - }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx deleted file mode 100644 index f47f802e558ca..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiPanel, - EuiFlexGroup, - EuiCommentProps, - EuiCommentList, - EuiAccordion, - EuiFlexItem, -} from '@elastic/eui'; -import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import styled from 'styled-components'; - -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { ExceptionDetails } from './exception_details'; -import { ExceptionEntries } from './exception_entries'; -import { getFormattedComments } from '../../helpers'; -import { getFormattedEntries } from '../helpers'; -import type { FormattedEntry, ExceptionListItemIdentifiers } from '../../types'; - -const MyFlexItem = styled(EuiFlexItem)` - &.comments--show { - padding: ${({ theme }) => theme.eui.euiSize}; - border-top: ${({ theme }) => `${theme.eui.euiBorderThin}`}; - } -`; - -export interface ExceptionItemProps { - loadingItemIds: ExceptionListItemIdentifiers[]; - exceptionItem: ExceptionListItemSchema; - commentsAccordionId: string; - onDeleteException: (arg: ExceptionListItemIdentifiers) => void; - onEditException: (item: ExceptionListItemSchema) => void; - showName?: boolean; - showModified?: boolean; - disableActions: boolean; - 'data-test-subj'?: string; -} - -const ExceptionItemComponent = ({ - disableActions, - loadingItemIds, - exceptionItem, - commentsAccordionId, - onDeleteException, - onEditException, - showModified = false, - showName = false, - 'data-test-subj': dataTestSubj, -}: ExceptionItemProps): JSX.Element => { - const [entryItems, setEntryItems] = useState<FormattedEntry[]>([]); - const [showComments, setShowComments] = useState(false); - - useEffect((): void => { - const formattedEntries = getFormattedEntries(exceptionItem.entries); - setEntryItems(formattedEntries); - }, [exceptionItem.entries]); - - const handleDelete = useCallback((): void => { - onDeleteException({ - id: exceptionItem.id, - namespaceType: exceptionItem.namespace_type, - }); - }, [onDeleteException, exceptionItem.id, exceptionItem.namespace_type]); - - const handleEdit = useCallback((): void => { - onEditException(exceptionItem); - }, [onEditException, exceptionItem]); - - const onCommentsClick = useCallback((): void => { - setShowComments(!showComments); - }, [setShowComments, showComments]); - - const formattedComments = useMemo((): EuiCommentProps[] => { - return getFormattedComments(exceptionItem.comments); - }, [exceptionItem.comments]); - - const disableItemActions = useMemo((): boolean => { - const foundItems = loadingItemIds.filter(({ id }) => id === exceptionItem.id); - return foundItems.length > 0; - }, [loadingItemIds, exceptionItem.id]); - - return ( - <EuiPanel paddingSize="none" data-test-subj={dataTestSubj} hasBorder hasShadow={false}> - <EuiFlexGroup direction="column" gutterSize="none"> - <EuiFlexItem> - <EuiFlexGroup direction="row"> - <ExceptionDetails - showComments={showComments} - exceptionItem={exceptionItem} - onCommentsClick={onCommentsClick} - showModified={showModified} - showName={showName} - /> - <ExceptionEntries - disableActions={disableItemActions || disableActions} - entries={entryItems} - onDelete={handleDelete} - onEdit={handleEdit} - /> - </EuiFlexGroup> - </EuiFlexItem> - <MyFlexItem className={showComments ? 'comments--show' : ''}> - <EuiAccordion - id={commentsAccordionId} - arrowDisplay="none" - forceState={showComments ? 'open' : 'closed'} - data-test-subj="exceptionsViewerCommentAccordion" - > - <EuiCommentList comments={formattedComments} /> - </EuiAccordion> - </MyFlexItem> - </EuiFlexGroup> - </EuiPanel> - ); -}; - -ExceptionItemComponent.displayName = 'ExceptionItemComponent'; - -export const ExceptionItem = React.memo(ExceptionItemComponent); - -ExceptionItem.displayName = 'ExceptionItem'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx new file mode 100644 index 0000000000000..dd0249958949f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { ExceptionItemCardConditions } from './exception_item_card_conditions'; + +describe('ExceptionItemCardConditions', () => { + it('it includes os condition if one exists', () => { + const wrapper = mount( + <TestProviders> + <ExceptionItemCardConditions + os={['linux']} + entries={[ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'host', + }, + { + field: 'threat.indicator.port', + operator: 'included', + type: 'exists', + }, + { + entries: [ + { + field: 'valid', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + field: 'file.Ext.code_signature', + type: 'nested', + }, + ]} + dataTestSubj="exceptionItemConditions" + /> + </TestProviders> + ); + + // Text is gonna look a bit off unformatted + expect(wrapper.find('[data-test-subj="exceptionItemConditions-os"]').at(0).text()).toEqual( + ' OSIS Linux' + ); + expect( + wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(0).text() + ).toEqual(' host.nameIS host'); + expect( + wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(1).text() + ).toEqual('AND threat.indicator.portexists '); + expect( + wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(2).text() + ).toEqual('AND file.Ext.code_signature validIS true'); + }); + + it('it renders item conditions', () => { + const wrapper = mount( + <TestProviders> + <ExceptionItemCardConditions + entries={[ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'host', + }, + { + field: 'threat.indicator.port', + operator: 'included', + type: 'exists', + }, + { + entries: [ + { + field: 'valid', + operator: 'included', + type: 'match', + value: 'true', + }, + ], + field: 'file.Ext.code_signature', + type: 'nested', + }, + ]} + dataTestSubj="exceptionItemConditions" + /> + </TestProviders> + ); + + // Text is gonna look a bit off unformatted + expect(wrapper.find('[data-test-subj="exceptionItemConditions-os"]').exists()).toBeFalsy(); + expect( + wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(0).text() + ).toEqual(' host.nameIS host'); + expect( + wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(1).text() + ).toEqual('AND threat.indicator.portexists '); + expect( + wrapper.find('[data-test-subj="exceptionItemConditions-condition"]').at(2).text() + ).toEqual('AND file.Ext.code_signature validIS true'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx new file mode 100644 index 0000000000000..24cbdd5061943 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useCallback } from 'react'; +import { EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import styled from 'styled-components'; +import { + ExceptionListItemSchema, + ListOperatorTypeEnum, + NonEmptyNestedEntriesArray, +} from '@kbn/securitysolution-io-ts-list-types'; + +import * as i18n from './translations'; + +const OS_LABELS = Object.freeze({ + linux: i18n.OS_LINUX, + mac: i18n.OS_MAC, + macos: i18n.OS_MAC, + windows: i18n.OS_WINDOWS, +}); + +const OPERATOR_TYPE_LABELS_INCLUDED = Object.freeze({ + [ListOperatorTypeEnum.NESTED]: i18n.CONDITION_OPERATOR_TYPE_NESTED, + [ListOperatorTypeEnum.MATCH_ANY]: i18n.CONDITION_OPERATOR_TYPE_MATCH_ANY, + [ListOperatorTypeEnum.MATCH]: i18n.CONDITION_OPERATOR_TYPE_MATCH, + [ListOperatorTypeEnum.WILDCARD]: i18n.CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES, + [ListOperatorTypeEnum.EXISTS]: i18n.CONDITION_OPERATOR_TYPE_EXISTS, + [ListOperatorTypeEnum.LIST]: i18n.CONDITION_OPERATOR_TYPE_LIST, +}); + +const OPERATOR_TYPE_LABELS_EXCLUDED = Object.freeze({ + [ListOperatorTypeEnum.MATCH_ANY]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH_ANY, + [ListOperatorTypeEnum.MATCH]: i18n.CONDITION_OPERATOR_TYPE_NOT_MATCH, +}); + +const EuiFlexGroupNested = styled(EuiFlexGroup)` + margin-left: ${({ theme }) => theme.eui.euiSizeXL}; +`; + +const EuiFlexItemNested = styled(EuiFlexItem)` + margin-bottom: 6px !important; + margin-top: 6px !important; +`; + +const StyledCondition = styled('span')` + margin-right: 6px; +`; + +export interface CriteriaConditionsProps { + entries: ExceptionListItemSchema['entries']; + dataTestSubj: string; + os?: ExceptionListItemSchema['os_types']; +} + +export const ExceptionItemCardConditions = memo<CriteriaConditionsProps>( + ({ os, entries, dataTestSubj }) => { + const osLabel = useMemo(() => { + if (os != null && os.length > 0) { + return os + .map((osValue) => OS_LABELS[osValue as keyof typeof OS_LABELS] ?? osValue) + .join(', '); + } + + return null; + }, [os]); + + const getEntryValue = (type: string, value: string | string[] | undefined) => { + if (type === 'match_any' && Array.isArray(value)) { + return value.map((currentValue) => <EuiBadge color="hollow">{currentValue}</EuiBadge>); + } + return value ?? ''; + }; + + const getEntryOperator = (type: string, operator: string) => { + if (type === 'nested') return ''; + return operator === 'included' + ? OPERATOR_TYPE_LABELS_INCLUDED[type as keyof typeof OPERATOR_TYPE_LABELS_INCLUDED] ?? type + : OPERATOR_TYPE_LABELS_EXCLUDED[type as keyof typeof OPERATOR_TYPE_LABELS_EXCLUDED] ?? type; + }; + + const getNestedEntriesContent = useCallback( + (type: string, nestedEntries: NonEmptyNestedEntriesArray) => { + if (type === 'nested' && nestedEntries.length) { + return nestedEntries.map((entry) => { + const { field: nestedField, type: nestedType, operator: nestedOperator } = entry; + const nestedValue = 'value' in entry ? entry.value : ''; + + return ( + <EuiFlexGroupNested + data-test-subj={`${dataTestSubj}-nestedCondition`} + key={nestedField + nestedType + nestedValue} + direction="row" + alignItems="center" + gutterSize="m" + responsive={false} + > + <EuiFlexItemNested grow={false}> + <EuiToken iconType="tokenNested" size="s" /> + </EuiFlexItemNested> + <EuiFlexItemNested grow={false}> + <EuiExpression description={''} value={nestedField} color="subdued" /> + </EuiFlexItemNested> + <EuiFlexItemNested grow={false}> + <EuiExpression + description={getEntryOperator(nestedType, nestedOperator)} + value={getEntryValue(nestedType, nestedValue)} + /> + </EuiFlexItemNested> + </EuiFlexGroupNested> + ); + }); + } + }, + [dataTestSubj] + ); + + return ( + <div data-test-subj={dataTestSubj}> + {osLabel != null && ( + <div data-test-subj={`${dataTestSubj}-os`}> + <strong> + <EuiExpression description={''} value={i18n.CONDITION_OS} /> + <EuiExpression description={i18n.CONDITION_OPERATOR_TYPE_MATCH} value={osLabel} /> + </strong> + </div> + )} + {entries.map((entry, index) => { + const { field, type } = entry; + const value = 'value' in entry ? entry.value : ''; + const nestedEntries = 'entries' in entry ? entry.entries : []; + const operator = 'operator' in entry ? entry.operator : ''; + + return ( + <div data-test-subj={`${dataTestSubj}-condition`} key={field + type + value + index}> + <div className="eui-xScroll"> + <EuiExpression + description={ + index === 0 ? '' : <StyledCondition>{i18n.CONDITION_AND}</StyledCondition> + } + value={field} + color={index === 0 ? 'primary' : 'subdued'} + /> + <EuiExpression + description={getEntryOperator(type, operator)} + value={getEntryValue(type, value)} + /> + </div> + {nestedEntries != null && getNestedEntriesContent(type, nestedEntries)} + </div> + ); + })} + </div> + ); + } +); +ExceptionItemCardConditions.displayName = 'ExceptionItemCardConditions'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx new file mode 100644 index 0000000000000..fe8811152e2e1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; +import { ThemeProvider } from 'styled-components'; + +import * as i18n from './translations'; +import { ExceptionItemCardHeader } from './exception_item_card_header'; +import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; + +const mockTheme = getMockTheme({ + eui: { + euiSize: '10px', + euiColorPrimary: '#ece', + euiColorDanger: '#ece', + }, +}); + +describe('ExceptionItemCardHeader', () => { + it('it renders item name', () => { + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ExceptionItemCardHeader + item={getExceptionListItemSchemaMock()} + dataTestSubj="exceptionItemHeader" + actions={[ + { + key: 'edit', + icon: 'pencil', + label: i18n.EXCEPTION_ITEM_EDIT_BUTTON, + onClick: jest.fn(), + }, + { + key: 'delete', + icon: 'trash', + label: i18n.EXCEPTION_ITEM_DELETE_BUTTON, + onClick: jest.fn(), + }, + ]} + /> + </ThemeProvider> + ); + + expect(wrapper.find('[data-test-subj="exceptionItemHeader-title"]').at(0).text()).toEqual( + 'some name' + ); + }); + + it('it displays actions', () => { + const handleEdit = jest.fn(); + const handleDelete = jest.fn(); + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ExceptionItemCardHeader + actions={[ + { + key: 'edit', + icon: 'pencil', + label: i18n.EXCEPTION_ITEM_EDIT_BUTTON, + onClick: handleEdit, + }, + { + key: 'delete', + icon: 'trash', + label: i18n.EXCEPTION_ITEM_DELETE_BUTTON, + onClick: handleDelete, + }, + ]} + item={getExceptionListItemSchemaMock()} + dataTestSubj="exceptionItemHeader" + /> + </ThemeProvider> + ); + + // click on popover + wrapper + .find('button[data-test-subj="exceptionItemHeader-actionButton"]') + .at(0) + .simulate('click'); + + wrapper.find('button[data-test-subj="exceptionItemHeader-actionItem-edit"]').simulate('click'); + expect(handleEdit).toHaveBeenCalled(); + + wrapper + .find('button[data-test-subj="exceptionItemHeader-actionItem-delete"]') + .simulate('click'); + expect(handleDelete).toHaveBeenCalled(); + }); + + it('it disables actions if disableActions is true', () => { + const handleEdit = jest.fn(); + const handleDelete = jest.fn(); + const wrapper = mount( + <ThemeProvider theme={mockTheme}> + <ExceptionItemCardHeader + actions={[ + { + key: 'edit', + icon: 'pencil', + label: i18n.EXCEPTION_ITEM_EDIT_BUTTON, + onClick: handleEdit, + }, + { + key: 'delete', + icon: 'trash', + label: i18n.EXCEPTION_ITEM_DELETE_BUTTON, + onClick: handleDelete, + }, + ]} + item={getExceptionListItemSchemaMock()} + disableActions + dataTestSubj="exceptionItemHeader" + /> + </ThemeProvider> + ); + + expect( + wrapper.find('button[data-test-subj="exceptionItemHeader-actionButton"]').at(0).props() + .disabled + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx new file mode 100644 index 0000000000000..3389bd0cb29b9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useState } from 'react'; +import { + EuiButtonIcon, + EuiContextMenuPanelProps, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiTitle, + EuiContextMenuItem, +} from '@elastic/eui'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +export interface ExceptionItemCardHeaderProps { + item: ExceptionListItemSchema; + actions: Array<{ key: string; icon: string; label: string; onClick: () => void }>; + disableActions?: boolean; + dataTestSubj: string; +} + +export const ExceptionItemCardHeader = memo<ExceptionItemCardHeaderProps>( + ({ item, actions, disableActions = false, dataTestSubj }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onItemActionsClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const onClosePopover = () => setIsPopoverOpen(false); + + const itemActions = useMemo((): EuiContextMenuPanelProps['items'] => { + return actions.map((action) => ( + <EuiContextMenuItem + data-test-subj={`${dataTestSubj}-actionItem-${action.key}`} + key={action.key} + icon={action.icon} + onClick={() => { + onClosePopover(); + action.onClick(); + }} + > + {action.label} + </EuiContextMenuItem> + )); + }, [dataTestSubj, actions]); + + return ( + <EuiFlexGroup data-test-subj={dataTestSubj} justifyContent="spaceBetween"> + <EuiFlexItem grow={9}> + <EuiTitle size="xs" textTransform="uppercase" data-test-subj={`${dataTestSubj}-title`}> + <h3>{item.name}</h3> + </EuiTitle> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiPopover + button={ + <EuiButtonIcon + isDisabled={disableActions} + aria-label="Exception item actions menu" + iconType="boxesHorizontal" + onClick={onItemActionsClick} + data-test-subj={`${dataTestSubj}-actionButton`} + /> + } + panelPaddingSize="none" + isOpen={isPopoverOpen} + closePopover={onClosePopover} + data-test-subj={`${dataTestSubj}-items`} + > + <EuiContextMenuPanel size="s" items={itemActions} /> + </EuiPopover> + </EuiFlexItem> + </EuiFlexGroup> + ); + } +); + +ExceptionItemCardHeader.displayName = 'ExceptionItemCardHeader'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx new file mode 100644 index 0000000000000..b5a24ef3e472d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; + +import { TestProviders } from '../../../../mock'; +import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; + +describe('ExceptionItemCardMetaInfo', () => { + it('it renders item creation info', () => { + const wrapper = mount( + <TestProviders> + <ExceptionItemCardMetaInfo + item={getExceptionListItemSchemaMock()} + dataTestSubj="exceptionItemMeta" + /> + </TestProviders> + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value1"]').at(0).text() + ).toEqual('Apr 20, 2020 @ 15:25:31.830'); + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value2"]').at(0).text() + ).toEqual('some user'); + }); + + it('it renders item update info', () => { + const wrapper = mount( + <TestProviders> + <ExceptionItemCardMetaInfo + item={getExceptionListItemSchemaMock()} + dataTestSubj="exceptionItemMeta" + /> + </TestProviders> + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value1"]').at(0).text() + ).toEqual('Apr 20, 2020 @ 15:25:31.830'); + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value2"]').at(0).text() + ).toEqual('some user'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx new file mode 100644 index 0000000000000..4e3ae24900246 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiAvatar, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import styled from 'styled-components'; + +import * as i18n from './translations'; +import { FormattedDate, FormattedRelativePreferenceDate } from '../../../formatted_date'; + +const StyledCondition = styled('div')` + padding-top: 4px !important; +`; +export interface ExceptionItemCardMetaInfoProps { + item: ExceptionListItemSchema; + dataTestSubj: string; +} + +export const ExceptionItemCardMetaInfo = memo<ExceptionItemCardMetaInfoProps>( + ({ item, dataTestSubj }) => { + return ( + <EuiFlexGroup + alignItems="center" + responsive={false} + gutterSize="s" + data-test-subj={dataTestSubj} + > + <EuiFlexItem grow={false}> + <MetaInfoDetails + fieldName="created_by" + label={i18n.EXCEPTION_ITEM_CREATED_LABEL} + value1={<FormattedDate fieldName="created_by" value={item.created_at} />} + value2={item.created_by} + dataTestSubj={`${dataTestSubj}-createdBy`} + /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <MetaInfoDetails + fieldName="updated_by" + label={i18n.EXCEPTION_ITEM_UPDATED_LABEL} + value1={ + <StyledCondition> + <FormattedRelativePreferenceDate + value={item.updated_at} + tooltipFieldName="updated_by" + tooltipAnchorClassName="eui-textTruncate" + /> + </StyledCondition> + } + value2={item.updated_by} + dataTestSubj={`${dataTestSubj}-updatedBy`} + /> + </EuiFlexItem> + </EuiFlexGroup> + ); + } +); +ExceptionItemCardMetaInfo.displayName = 'ExceptionItemCardMetaInfo'; + +interface MetaInfoDetailsProps { + fieldName: string; + label: string; + value1: JSX.Element | string; + value2: string; + dataTestSubj: string; +} + +const MetaInfoDetails = memo<MetaInfoDetailsProps>(({ label, value1, value2, dataTestSubj }) => { + return ( + <EuiFlexGroup alignItems="center" gutterSize="s" wrap={false} responsive={false}> + <EuiFlexItem grow={false}> + <EuiBadge color="default" style={{ fontFamily: 'Inter' }}> + {label} + </EuiBadge> + </EuiFlexItem> + <EuiFlexItem grow={false} data-test-subj={`${dataTestSubj}-value1`}> + <EuiText size="xs" style={{ fontFamily: 'Inter' }}> + {value1} + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiText size="xs" style={{ fontStyle: 'italic', fontFamily: 'Inter' }}> + {i18n.EXCEPTION_ITEM_META_BY} + </EuiText> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup responsive={false} gutterSize="xs" alignItems="center" wrap={false}> + <EuiFlexItem grow={false}> + <EuiAvatar initialsLength={2} name={value2.toUpperCase()} size="s" /> + </EuiFlexItem> + <EuiFlexItem grow={false} className="eui-textTruncate"> + <EuiText + size="xs" + style={{ fontFamily: 'Inter' }} + data-test-subj={`${dataTestSubj}-value2`} + > + {value2} + </EuiText> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); +}); + +MetaInfoDetails.displayName = 'MetaInfoDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx similarity index 57% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx rename to x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx index e1afc8f44b354..46a0f74642c08 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; -import { ExceptionItem } from '.'; +import { ExceptionItemCard } from '.'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock'; import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; @@ -25,75 +25,72 @@ const mockTheme = getMockTheme({ }, }); -describe('ExceptionItem', () => { - it('it renders ExceptionDetails and ExceptionEntries', () => { - const exceptionItem = getExceptionListItemSchemaMock(); +describe('ExceptionItemCard', () => { + it('it renders header, item meta information and conditions', () => { + const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: [] }; const wrapper = mount( <ThemeProvider theme={mockTheme}> - <ExceptionItem + <ExceptionItemCard disableActions={false} loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} onDeleteException={jest.fn()} onEditException={jest.fn()} exceptionItem={exceptionItem} + dataTestSubj="item" /> </ThemeProvider> ); - expect(wrapper.find('ExceptionDetails')).toHaveLength(1); - expect(wrapper.find('ExceptionEntries')).toHaveLength(1); + expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1); + expect(wrapper.find('ExceptionItemCardMetaInfo')).toHaveLength(1); + expect(wrapper.find('ExceptionItemCardConditions')).toHaveLength(1); + expect( + wrapper.find('[data-test-subj="exceptionsViewerCommentAccordion"]').exists() + ).toBeFalsy(); }); - it('it renders ExceptionDetails with Name and Modified info when showName and showModified are true ', () => { - const exceptionItem = getExceptionListItemSchemaMock(); + it('it renders header, item meta information, conditions, and comments if any exist', () => { + const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: getCommentsArrayMock() }; const wrapper = mount( <ThemeProvider theme={mockTheme}> - <ExceptionItem + <ExceptionItemCard disableActions={false} loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} onDeleteException={jest.fn()} onEditException={jest.fn()} exceptionItem={exceptionItem} - showModified={true} - showName={true} + dataTestSubj="item" /> </ThemeProvider> ); - expect(wrapper.find('ExceptionDetails').props()).toEqual( - expect.objectContaining({ - showModified: true, - showName: true, - }) - ); + expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1); + expect(wrapper.find('ExceptionItemCardMetaInfo')).toHaveLength(1); + expect(wrapper.find('ExceptionItemCardConditions')).toHaveLength(1); + expect( + wrapper.find('[data-test-subj="exceptionsViewerCommentAccordion"]').exists() + ).toBeTruthy(); }); it('it does not render edit or delete action buttons when "disableActions" is "true"', () => { - const mockOnEditException = jest.fn(); const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( <ThemeProvider theme={mockTheme}> - <ExceptionItem + <ExceptionItemCard disableActions loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} onDeleteException={jest.fn()} - onEditException={mockOnEditException} + onEditException={jest.fn()} exceptionItem={exceptionItem} + dataTestSubj="item" /> </ThemeProvider> ); - const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button'); - const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button'); - - expect(editBtn).toHaveLength(0); - expect(deleteBtn).toHaveLength(0); + expect(wrapper.find('button[data-test-subj="item-actionButton"]').exists()).toBeFalsy(); }); it('it invokes "onEditException" when edit button clicked', () => { @@ -102,19 +99,25 @@ describe('ExceptionItem', () => { const wrapper = mount( <ThemeProvider theme={mockTheme}> - <ExceptionItem + <ExceptionItemCard disableActions={false} loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} onDeleteException={jest.fn()} onEditException={mockOnEditException} exceptionItem={exceptionItem} + dataTestSubj="item" /> </ThemeProvider> ); - const editBtn = wrapper.find('[data-test-subj="exceptionsViewerEditBtn"] button').at(0); - editBtn.simulate('click'); + // click on popover + wrapper + .find('button[data-test-subj="exceptionItemCardHeader-actionButton"]') + .at(0) + .simulate('click'); + wrapper + .find('button[data-test-subj="exceptionItemCardHeader-actionItem-edit"]') + .simulate('click'); expect(mockOnEditException).toHaveBeenCalledWith(getExceptionListItemSchemaMock()); }); @@ -125,19 +128,25 @@ describe('ExceptionItem', () => { const wrapper = mount( <ThemeProvider theme={mockTheme}> - <ExceptionItem + <ExceptionItemCard disableActions={false} loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} onDeleteException={mockOnDeleteException} onEditException={jest.fn()} exceptionItem={exceptionItem} + dataTestSubj="item" /> </ThemeProvider> ); - const deleteBtn = wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0); - deleteBtn.simulate('click'); + // click on popover + wrapper + .find('button[data-test-subj="exceptionItemCardHeader-actionButton"]') + .at(0) + .simulate('click'); + wrapper + .find('button[data-test-subj="exceptionItemCardHeader-actionItem-delete"]') + .simulate('click'); expect(mockOnDeleteException).toHaveBeenCalledWith({ id: '1', @@ -146,47 +155,21 @@ describe('ExceptionItem', () => { }); it('it renders comment accordion closed to begin with', () => { - const mockOnDeleteException = jest.fn(); const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( <ThemeProvider theme={mockTheme}> - <ExceptionItem + <ExceptionItemCard disableActions={false} loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - onDeleteException={mockOnDeleteException} + onDeleteException={jest.fn()} onEditException={jest.fn()} exceptionItem={exceptionItem} + dataTestSubj="item" /> </ThemeProvider> ); expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(0); }); - - it('it renders comment accordion open when showComments is true', () => { - const mockOnDeleteException = jest.fn(); - const exceptionItem = getExceptionListItemSchemaMock(); - exceptionItem.comments = getCommentsArrayMock(); - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionItem - disableActions={false} - loadingItemIds={[]} - commentsAccordionId={'accordion--comments'} - onDeleteException={mockOnDeleteException} - onEditException={jest.fn()} - exceptionItem={exceptionItem} - /> - </ThemeProvider> - ); - - const commentsBtn = wrapper - .find('.euiButtonEmpty[data-test-subj="exceptionsViewerItemCommentsBtn"]') - .at(0); - commentsBtn.simulate('click'); - - expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(1); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx new file mode 100644 index 0000000000000..13e1d679a44f9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiPanel, + EuiFlexGroup, + EuiCommentProps, + EuiCommentList, + EuiAccordion, + EuiFlexItem, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import React, { useMemo, useCallback } from 'react'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; + +import { getFormattedComments } from '../../helpers'; +import type { ExceptionListItemIdentifiers } from '../../types'; +import * as i18n from './translations'; +import { ExceptionItemCardHeader } from './exception_item_card_header'; +import { ExceptionItemCardConditions } from './exception_item_card_conditions'; +import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; + +export interface ExceptionItemProps { + loadingItemIds: ExceptionListItemIdentifiers[]; + exceptionItem: ExceptionListItemSchema; + onDeleteException: (arg: ExceptionListItemIdentifiers) => void; + onEditException: (item: ExceptionListItemSchema) => void; + disableActions: boolean; + dataTestSubj: string; +} + +const ExceptionItemCardComponent = ({ + disableActions, + loadingItemIds, + exceptionItem, + onDeleteException, + onEditException, + dataTestSubj, +}: ExceptionItemProps): JSX.Element => { + const { euiTheme } = useEuiTheme(); + + const handleDelete = useCallback((): void => { + onDeleteException({ + id: exceptionItem.id, + namespaceType: exceptionItem.namespace_type, + }); + }, [onDeleteException, exceptionItem.id, exceptionItem.namespace_type]); + + const handleEdit = useCallback((): void => { + onEditException(exceptionItem); + }, [onEditException, exceptionItem]); + + const formattedComments = useMemo((): EuiCommentProps[] => { + return getFormattedComments(exceptionItem.comments); + }, [exceptionItem.comments]); + + const disableItemActions = useMemo((): boolean => { + const foundItems = loadingItemIds.some(({ id }) => id === exceptionItem.id); + return disableActions || foundItems; + }, [loadingItemIds, exceptionItem.id, disableActions]); + + return ( + <EuiPanel paddingSize="l" data-test-subj={dataTestSubj} hasBorder hasShadow={false}> + <EuiFlexGroup gutterSize="m" direction="column"> + <EuiFlexItem data-test-subj={`${dataTestSubj}-header`}> + <ExceptionItemCardHeader + item={exceptionItem} + actions={[ + { + key: 'edit', + icon: 'pencil', + label: i18n.EXCEPTION_ITEM_EDIT_BUTTON, + onClick: handleEdit, + }, + { + key: 'delete', + icon: 'trash', + label: i18n.EXCEPTION_ITEM_DELETE_BUTTON, + onClick: handleDelete, + }, + ]} + disableActions={disableItemActions} + dataTestSubj="exceptionItemCardHeader" + /> + </EuiFlexItem> + <EuiFlexItem data-test-subj={`${dataTestSubj}-meta`}> + <ExceptionItemCardMetaInfo + item={exceptionItem} + dataTestSubj="exceptionItemCardMetaInfo" + /> + </EuiFlexItem> + <EuiFlexItem> + <ExceptionItemCardConditions + os={exceptionItem.os_types} + entries={exceptionItem.entries} + dataTestSubj="exceptionItemCardConditions" + /> + </EuiFlexItem> + {formattedComments.length > 0 && ( + <EuiFlexItem> + <EuiAccordion + id="exceptionItemCardComments" + buttonContent={ + <EuiText size="s" style={{ color: euiTheme.colors.primary }}> + {i18n.exceptionItemCommentsAccordion(formattedComments.length)} + </EuiText> + } + arrowDisplay="none" + data-test-subj="exceptionsViewerCommentAccordion" + > + <EuiPanel hasBorder={false} hasShadow={false} paddingSize="m"> + <EuiCommentList comments={formattedComments} /> + </EuiPanel> + </EuiAccordion> + </EuiFlexItem> + )} + </EuiFlexGroup> + </EuiPanel> + ); +}; + +ExceptionItemCardComponent.displayName = 'ExceptionItemCardComponent'; + +export const ExceptionItemCard = React.memo(ExceptionItemCardComponent); + +ExceptionItemCard.displayName = 'ExceptionItemCard'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts new file mode 100644 index 0000000000000..8d345c23fbf09 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const EXCEPTION_ITEM_EDIT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.editItemButton', + { + defaultMessage: 'Edit item', + } +); + +export const EXCEPTION_ITEM_DELETE_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.deleteItemButton', + { + defaultMessage: 'Delete item', + } +); + +export const EXCEPTION_ITEM_CREATED_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.createdLabel', + { + defaultMessage: 'Created', + } +); + +export const EXCEPTION_ITEM_UPDATED_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.updatedLabel', + { + defaultMessage: 'Updated', + } +); + +export const EXCEPTION_ITEM_META_BY = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy', + { + defaultMessage: 'by', + } +); + +export const exceptionItemCommentsAccordion = (comments: number) => + i18n.translate('xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel', { + values: { comments }, + defaultMessage: 'Show {comments, plural, =1 {comment} other {comments}} ({comments})', + }); + +export const CONDITION_OPERATOR_TYPE_MATCH = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.matchOperator', + { + defaultMessage: 'IS', + } +); + +export const CONDITION_OPERATOR_TYPE_NOT_MATCH = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.matchOperator.not', + { + defaultMessage: 'IS NOT', + } +); + +export const CONDITION_OPERATOR_TYPE_WILDCARD_MATCHES = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.wildcardMatchesOperator', + { + defaultMessage: 'MATCHES', + } +); + +export const CONDITION_OPERATOR_TYPE_NESTED = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.nestedOperator', + { + defaultMessage: 'has', + } +); + +export const CONDITION_OPERATOR_TYPE_MATCH_ANY = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.matchAnyOperator', + { + defaultMessage: 'is one of', + } +); + +export const CONDITION_OPERATOR_TYPE_NOT_MATCH_ANY = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.matchAnyOperator.not', + { + defaultMessage: 'is not one of', + } +); + +export const CONDITION_OPERATOR_TYPE_EXISTS = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator', + { + defaultMessage: 'exists', + } +); + +export const CONDITION_OPERATOR_TYPE_LIST = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.listOperator', + { + defaultMessage: 'included in', + } +); + +export const CONDITION_AND = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.and', + { + defaultMessage: 'AND', + } +); + +export const CONDITION_OS = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.os', + { + defaultMessage: 'OS', + } +); + +export const OS_WINDOWS = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.windows', + { + defaultMessage: 'Windows', + } +); + +export const OS_LINUX = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.linux', + { + defaultMessage: 'Linux', + } +); + +export const OS_MAC = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.conditions.macos', + { + defaultMessage: 'Mac', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx index 90a06a732a283..22c6e7dbf8ecf 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx @@ -13,6 +13,7 @@ import * as i18n from '../translations'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { ExceptionsViewerItems } from './exceptions_viewer_items'; import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; +import { TestProviders } from '../../../mock'; const mockTheme = getMockTheme({ eui: { @@ -25,17 +26,18 @@ const mockTheme = getMockTheme({ describe('ExceptionsViewerItems', () => { it('it renders empty prompt if "showEmpty" is "true"', () => { const wrapper = mount( - <ExceptionsViewerItems - showEmpty - showNoResults={false} - isInitLoading={false} - disableActions={false} - exceptions={[]} - loadingItemIds={[]} - commentsAccordionId="comments-accordion-id" - onDeleteException={jest.fn()} - onEditExceptionItem={jest.fn()} - /> + <TestProviders> + <ExceptionsViewerItems + showEmpty + showNoResults={false} + isInitLoading={false} + disableActions={false} + exceptions={[]} + loadingItemIds={[]} + onDeleteException={jest.fn()} + onEditExceptionItem={jest.fn()} + /> + </TestProviders> ); expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); @@ -50,19 +52,20 @@ describe('ExceptionsViewerItems', () => { it('it renders no search results found prompt if "showNoResults" is "true"', () => { const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionsViewerItems - showEmpty={false} - showNoResults - isInitLoading={false} - disableActions={false} - exceptions={[]} - loadingItemIds={[]} - commentsAccordionId="comments-accordion-id" - onDeleteException={jest.fn()} - onEditExceptionItem={jest.fn()} - /> - </ThemeProvider> + <TestProviders> + <ThemeProvider theme={mockTheme}> + <ExceptionsViewerItems + showEmpty={false} + showNoResults + isInitLoading={false} + disableActions={false} + exceptions={[]} + loadingItemIds={[]} + onDeleteException={jest.fn()} + onEditExceptionItem={jest.fn()} + /> + </ThemeProvider> + </TestProviders> ); expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); @@ -75,19 +78,20 @@ describe('ExceptionsViewerItems', () => { it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionsViewerItems - showEmpty={false} - showNoResults={false} - isInitLoading={false} - disableActions={false} - exceptions={[getExceptionListItemSchemaMock()]} - loadingItemIds={[]} - commentsAccordionId="comments-accordion-id" - onDeleteException={jest.fn()} - onEditExceptionItem={jest.fn()} - /> - </ThemeProvider> + <TestProviders> + <ThemeProvider theme={mockTheme}> + <ExceptionsViewerItems + showEmpty={false} + showNoResults={false} + isInitLoading={false} + disableActions={false} + exceptions={[getExceptionListItemSchemaMock()]} + loadingItemIds={[]} + onDeleteException={jest.fn()} + onEditExceptionItem={jest.fn()} + /> + </ThemeProvider> + </TestProviders> ); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeTruthy(); @@ -96,103 +100,23 @@ describe('ExceptionsViewerItems', () => { it('it does not render exceptions if "isInitLoading" is "true"', () => { const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionsViewerItems - showEmpty={false} - showNoResults={false} - isInitLoading={true} - disableActions={false} - exceptions={[]} - loadingItemIds={[]} - commentsAccordionId="comments-accordion-id" - onDeleteException={jest.fn()} - onEditExceptionItem={jest.fn()} - /> - </ThemeProvider> + <TestProviders> + <ThemeProvider theme={mockTheme}> + <ExceptionsViewerItems + showEmpty={false} + showNoResults={false} + isInitLoading={true} + disableActions={false} + exceptions={[]} + loadingItemIds={[]} + onDeleteException={jest.fn()} + onEditExceptionItem={jest.fn()} + /> + </ThemeProvider> + </TestProviders> ); expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); }); - - it('it does not render or badge for first exception displayed', () => { - const exception1 = getExceptionListItemSchemaMock(); - const exception2 = getExceptionListItemSchemaMock(); - exception2.id = 'newId'; - - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionsViewerItems - showEmpty={false} - showNoResults={false} - isInitLoading={false} - disableActions={false} - exceptions={[exception1, exception2]} - loadingItemIds={[]} - commentsAccordionId="comments-accordion-id" - onDeleteException={jest.fn()} - onEditExceptionItem={jest.fn()} - /> - </ThemeProvider> - ); - - const firstExceptionItem = wrapper.find('[data-test-subj="exceptionItemContainer"]').at(0); - - expect(firstExceptionItem.find('[data-test-subj="exceptionItemOrBadge"]').exists()).toBeFalsy(); - }); - - it('it does render or badge with exception displayed', () => { - const exception1 = getExceptionListItemSchemaMock(); - const exception2 = getExceptionListItemSchemaMock(); - exception2.id = 'newId'; - - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionsViewerItems - showEmpty={false} - showNoResults={false} - isInitLoading={false} - disableActions={false} - exceptions={[exception1, exception2]} - loadingItemIds={[]} - commentsAccordionId="comments-accordion-id" - onDeleteException={jest.fn()} - onEditExceptionItem={jest.fn()} - /> - </ThemeProvider> - ); - - const notFirstExceptionItem = wrapper.find('[data-test-subj="exceptionItemContainer"]').at(1); - - expect( - notFirstExceptionItem.find('[data-test-subj="exceptionItemOrBadge"]').exists() - ).toBeFalsy(); - }); - - it('it invokes "onDeleteException" when delete button is clicked', () => { - const mockOnDeleteException = jest.fn(); - - const wrapper = mount( - <ThemeProvider theme={mockTheme}> - <ExceptionsViewerItems - showEmpty={false} - showNoResults={false} - isInitLoading={false} - disableActions={false} - exceptions={[getExceptionListItemSchemaMock()]} - loadingItemIds={[]} - commentsAccordionId="comments-accordion-id" - onDeleteException={mockOnDeleteException} - onEditExceptionItem={jest.fn()} - /> - </ThemeProvider> - ); - - wrapper.find('[data-test-subj="exceptionsViewerDeleteBtn"] button').at(0).simulate('click'); - - expect(mockOnDeleteException).toHaveBeenCalledWith({ - id: '1', - namespaceType: 'single', - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx index 30b5b3e4d1339..e1d91ed0a0580 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx @@ -6,13 +6,12 @@ */ import React from 'react'; -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from '../translations'; -import { ExceptionItem } from './exception_item'; -import { AndOrBadge } from '../../and_or_badge'; +import { ExceptionItemCard } from './exception_item_card'; import type { ExceptionListItemIdentifiers } from '../types'; const MyFlexItem = styled(EuiFlexItem)` @@ -34,7 +33,6 @@ interface ExceptionsViewerItemsProps { disableActions: boolean; exceptions: ExceptionListItemSchema[]; loadingItemIds: ExceptionListItemIdentifiers[]; - commentsAccordionId: string; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditExceptionItem: (item: ExceptionListItemSchema) => void; } @@ -45,7 +43,6 @@ const ExceptionsViewerItemsComponent: React.FC<ExceptionsViewerItemsProps> = ({ isInitLoading, exceptions, loadingItemIds, - commentsAccordionId, onDeleteException, onEditExceptionItem, disableActions, @@ -79,23 +76,15 @@ const ExceptionsViewerItemsComponent: React.FC<ExceptionsViewerItemsProps> = ({ > {!isInitLoading && exceptions.length > 0 && - exceptions.map((exception, index) => ( + exceptions.map((exception) => ( <MyFlexItem data-test-subj="exceptionItemContainer" grow={false} key={exception.id}> - {index !== 0 ? ( - <> - <AndOrBadge data-test-subj="exceptionItemOrBadge" type="or" /> - <EuiSpacer /> - </> - ) : ( - <EuiSpacer size="s" /> - )} - <ExceptionItem + <ExceptionItemCard disableActions={disableActions} loadingItemIds={loadingItemIds} - commentsAccordionId={commentsAccordionId} exceptionItem={exception} onDeleteException={onDeleteException} onEditException={onEditExceptionItem} + dataTestSubj={`exceptionItemCard-${exception.name}`} /> </MyFlexItem> ))} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx deleted file mode 100644 index aee9ca3d87611..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.test.tsx +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment-timezone'; - -import { getFormattedEntries, formatEntry, getDescriptionListContent } from './helpers'; -import { FormattedEntry } from '../types'; -import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { getEntriesArrayMock } from '@kbn/lists-plugin/common/schemas/types/entries.mock'; -import { getEntryMatchMock } from '@kbn/lists-plugin/common/schemas/types/entry_match.mock'; -import { getEntryMatchAnyMock } from '@kbn/lists-plugin/common/schemas/types/entry_match_any.mock'; -import { getEntryExistsMock } from '@kbn/lists-plugin/common/schemas/types/entry_exists.mock'; - -describe('Exception viewer helpers', () => { - beforeEach(() => { - moment.tz.setDefault('UTC'); - }); - - afterEach(() => { - moment.tz.setDefault('Browser'); - }); - - describe('#getFormattedEntries', () => { - test('it returns empty array if no entries passed', () => { - const result = getFormattedEntries([]); - - expect(result).toEqual([]); - }); - - test('it formats nested entries as expected', () => { - const payload = [getEntryMatchMock()]; - const result = getFormattedEntries(payload); - const expected: FormattedEntry[] = [ - { - fieldName: 'host.name', - isNested: false, - operator: 'is', - value: 'some host name', - }, - ]; - expect(result).toEqual(expected); - }); - - test('it formats "exists" entries as expected', () => { - const payload = [getEntryExistsMock()]; - const result = getFormattedEntries(payload); - const expected: FormattedEntry[] = [ - { - fieldName: 'host.name', - isNested: false, - operator: 'exists', - value: undefined, - }, - ]; - expect(result).toEqual(expected); - }); - - test('it formats non-nested entries as expected', () => { - const payload = [getEntryMatchAnyMock(), getEntryMatchMock()]; - const result = getFormattedEntries(payload); - const expected: FormattedEntry[] = [ - { - fieldName: 'host.name', - isNested: false, - operator: 'is one of', - value: ['some host name'], - }, - { - fieldName: 'host.name', - isNested: false, - operator: 'is', - value: 'some host name', - }, - ]; - expect(result).toEqual(expected); - }); - - test('it formats a mix of nested and non-nested entries as expected', () => { - const payload = getEntriesArrayMock(); - const result = getFormattedEntries(payload); - const expected: FormattedEntry[] = [ - { - fieldName: 'host.name', - isNested: false, - operator: 'is', - value: 'some host name', - }, - { - fieldName: 'host.name', - isNested: false, - operator: 'is one of', - value: ['some host name'], - }, - { - fieldName: 'host.name', - isNested: false, - operator: 'exists', - value: undefined, - }, - { - fieldName: 'parent.field', - isNested: false, - operator: undefined, - value: undefined, - }, - { - fieldName: 'host.name', - isNested: true, - operator: 'is', - value: 'some host name', - }, - { - fieldName: 'host.name', - isNested: true, - operator: 'is one of', - value: ['some host name'], - }, - ]; - expect(result).toEqual(expected); - }); - }); - - describe('#formatEntry', () => { - test('it formats an entry', () => { - const payload = getEntryMatchMock(); - const formattedEntry = formatEntry({ isNested: false, item: payload }); - const expected: FormattedEntry = { - fieldName: 'host.name', - isNested: false, - operator: 'is', - value: 'some host name', - }; - - expect(formattedEntry).toEqual(expected); - }); - - test('it formats as expected when "isNested" is "true"', () => { - const payload = getEntryMatchMock(); - const formattedEntry = formatEntry({ isNested: true, item: payload }); - const expected: FormattedEntry = { - fieldName: 'host.name', - isNested: true, - operator: 'is', - value: 'some host name', - }; - - expect(formattedEntry).toEqual(expected); - }); - }); - - describe('#getDescriptionListContent', () => { - test('it returns formatted description list with os if one is specified', () => { - const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - payload.description = ''; - const result = getDescriptionListContent(payload); - const os = result.find(({ title }) => title === 'OS'); - - expect(os).toMatchInlineSnapshot(` - Object { - "description": <EuiToolTip - anchorClassName="eventFiltersDescriptionListDescription" - content="Linux" - delay="regular" - display="inlineBlock" - position="top" - > - <EuiDescriptionListDescription - className="eui-fullWidth" - > - Linux - </EuiDescriptionListDescription> - </EuiToolTip>, - "title": "OS", - } - `); - }); - - test('it returns formatted description list with a description if one specified', () => { - const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - payload.description = 'Im a description'; - const result = getDescriptionListContent(payload); - const description = result.find(({ title }) => title === 'Description'); - - expect(description).toMatchInlineSnapshot(` - Object { - "description": <EuiToolTip - anchorClassName="eventFiltersDescriptionListDescription" - content="Im a description" - delay="regular" - display="inlineBlock" - position="top" - > - <EuiDescriptionListDescription - className="eui-fullWidth" - > - Im a description - </EuiDescriptionListDescription> - </EuiToolTip>, - "title": "Description", - } - `); - }); - - test('it returns scrolling element when description is longer than 75 charachters', () => { - const payload = getExceptionListItemSchemaMock({ os_types: ['linux'] }); - payload.description = - 'Puppy kitty ipsum dolor sit good dog foot stick canary. Teeth Mittens grooming vaccine walk swimming nest good boy furry tongue heel furry treats fish. Cage run fast kitten dinnertime ball run foot park fleas throw house train licks stick dinnertime window. Yawn litter fish yawn toy pet gate throw Buddy kitty wag tail ball groom crate ferret heel wet nose Rover toys pet supplies. Bird Food treats tongue lick teeth ferret litter box slobbery litter box crate bird small animals yawn small animals shake slobber gimme five toys polydactyl meow. '; - const result = getDescriptionListContent(payload); - const description = result.find(({ title }) => title === 'Description'); - - expect(description).toMatchInlineSnapshot(` - Object { - "description": <EuiDescriptionListDescription - style={ - Object { - "height": 150, - "overflowY": "hidden", - } - } - > - <EuiText - aria-label="" - className="eui-yScrollWithShadows" - role="region" - size="s" - tabIndex={0} - > - Puppy kitty ipsum dolor sit good dog foot stick canary. Teeth Mittens grooming vaccine walk swimming nest good boy furry tongue heel furry treats fish. Cage run fast kitten dinnertime ball run foot park fleas throw house train licks stick dinnertime window. Yawn litter fish yawn toy pet gate throw Buddy kitty wag tail ball groom crate ferret heel wet nose Rover toys pet supplies. Bird Food treats tongue lick teeth ferret litter box slobbery litter box crate bird small animals yawn small animals shake slobber gimme five toys polydactyl meow. - </EuiText> - </EuiDescriptionListDescription>, - "title": "Description", - } - `); - }); - - test('it returns just user and date created if no other fields specified', () => { - const payload = getExceptionListItemSchemaMock(); - payload.description = ''; - const result = getDescriptionListContent(payload); - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "description": <EuiToolTip - anchorClassName="eventFiltersDescriptionListDescription" - content="April 20th 2020 @ 15:25:31" - delay="regular" - display="inlineBlock" - position="top" - > - <EuiDescriptionListDescription - className="eui-fullWidth" - > - April 20th 2020 @ 15:25:31 - </EuiDescriptionListDescription> - </EuiToolTip>, - "title": "Date created", - }, - Object { - "description": <EuiToolTip - anchorClassName="eventFiltersDescriptionListDescription" - content="some user" - delay="regular" - display="inlineBlock" - position="top" - > - <EuiDescriptionListDescription - className="eui-fullWidth" - > - some user - </EuiDescriptionListDescription> - </EuiToolTip>, - "title": "Created by", - }, - ] - `); - }); - - test('it returns Modified By/On info when `includeModified` is true', () => { - const result = getDescriptionListContent( - getExceptionListItemSchemaMock({ os_types: ['linux'] }), - true - ); - const dateModified = result.find(({ title }) => title === 'Date modified'); - const modifiedBy = result.find(({ title }) => title === 'Modified by'); - expect(modifiedBy).toMatchInlineSnapshot(` - Object { - "description": <EuiToolTip - anchorClassName="eventFiltersDescriptionListDescription" - content="some user" - delay="regular" - display="inlineBlock" - position="top" - > - <EuiDescriptionListDescription - className="eui-fullWidth" - > - some user - </EuiDescriptionListDescription> - </EuiToolTip>, - "title": "Modified by", - } - `); - expect(dateModified).toMatchInlineSnapshot(` - Object { - "description": <EuiToolTip - anchorClassName="eventFiltersDescriptionListDescription" - content="April 20th 2020 @ 15:25:31" - delay="regular" - display="inlineBlock" - position="top" - > - <EuiDescriptionListDescription - className="eui-fullWidth" - > - April 20th 2020 @ 15:25:31 - </EuiDescriptionListDescription> - </EuiToolTip>, - "title": "Date modified", - } - `); - }); - - test('it returns Name when `includeName` is true', () => { - const result = getDescriptionListContent( - getExceptionListItemSchemaMock({ os_types: ['linux'] }), - false, - true - ); - const name = result.find(({ title }) => title === 'Name'); - expect(name).toMatchInlineSnapshot(` - Object { - "description": <EuiToolTip - anchorClassName="eventFiltersDescriptionListDescription" - content="some name" - delay="regular" - display="inlineBlock" - position="top" - > - <EuiDescriptionListDescription - className="eui-fullWidth" - > - some name - </EuiDescriptionListDescription> - </EuiToolTip>, - "title": "Name", - } - `); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx deleted file mode 100644 index 37bfeb6166405..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/helpers.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; - -import { entriesNested, ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { - getEntryValue, - getExceptionOperatorSelect, - BuilderEntry, -} from '@kbn/securitysolution-list-utils'; - -import React from 'react'; -import { EuiDescriptionListDescription, EuiText, EuiToolTip } from '@elastic/eui'; -import { formatOperatingSystems } from '../helpers'; -import type { FormattedEntry, DescriptionListItem } from '../types'; -import * as i18n from '../translations'; - -/** - * Helper method for `getFormattedEntries` - */ -export const formatEntry = ({ - isNested, - item, -}: { - isNested: boolean; - item: BuilderEntry; -}): FormattedEntry => { - const operator = getExceptionOperatorSelect(item); - const value = getEntryValue(item); - - return { - fieldName: item.field ?? '', - operator: operator.message, - value, - isNested, - }; -}; - -/** - * Formats ExceptionItem entries into simple field, operator, value - * for use in rendering items in table - * - * @param entries an ExceptionItem's entries - */ -export const getFormattedEntries = (entries: BuilderEntry[]): FormattedEntry[] => { - const formattedEntries = entries.map((item) => { - if (entriesNested.is(item)) { - const parent = { - fieldName: item.field, - operator: undefined, - value: undefined, - isNested: false, - }; - return item.entries.reduce<FormattedEntry[]>( - (acc, nestedEntry) => { - const formattedEntry = formatEntry({ - isNested: true, - item: nestedEntry, - }); - return [...acc, { ...formattedEntry }]; - }, - [parent] - ); - } else { - return formatEntry({ isNested: false, item }); - } - }); - - return formattedEntries.flat(); -}; - -/** - * Formats ExceptionItem details for description list component - * - * @param exceptionItem an ExceptionItem - * @param includeModified if modified information should be included - * @param includeName if the Name should be included - */ -export const getDescriptionListContent = ( - exceptionItem: ExceptionListItemSchema, - includeModified: boolean = false, - includeName: boolean = false -): DescriptionListItem[] => { - const details = [ - ...(includeName - ? [ - { - title: i18n.NAME, - value: exceptionItem.name, - }, - ] - : []), - { - title: i18n.OPERATING_SYSTEM, - value: formatOperatingSystems(exceptionItem.os_types), - }, - { - title: i18n.DATE_CREATED, - value: moment(exceptionItem.created_at).format('MMMM Do YYYY @ HH:mm:ss'), - }, - { - title: i18n.CREATED_BY, - value: exceptionItem.created_by, - }, - ...(includeModified - ? [ - { - title: i18n.DATE_MODIFIED, - value: moment(exceptionItem.updated_at).format('MMMM Do YYYY @ HH:mm:ss'), - }, - { - title: i18n.MODIFIED_BY, - value: exceptionItem.updated_by, - }, - ] - : []), - { - title: i18n.DESCRIPTION, - value: exceptionItem.description, - }, - ]; - - return details.reduce<DescriptionListItem[]>((acc, { value, title }) => { - if (value != null && value.trim() !== '') { - const valueElement = ( - <EuiToolTip content={value} anchorClassName="eventFiltersDescriptionListDescription"> - <EuiDescriptionListDescription className="eui-fullWidth"> - {value} - </EuiDescriptionListDescription> - </EuiToolTip> - ); - if (title === i18n.DESCRIPTION) { - return [ - ...acc, - { - title, - description: - value.length > 75 ? ( - <EuiDescriptionListDescription style={{ height: 150, overflowY: 'hidden' }}> - <EuiText - tabIndex={0} - role="region" - aria-label="" - className="eui-yScrollWithShadows" - size="s" - > - {value} - </EuiText> - </EuiDescriptionListDescription> - ) : ( - valueElement - ), - }, - ]; - } - return [...acc, { title, description: valueElement }]; - } else { - return acc; - } - }, []); -}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 63093c06a9450..6e5d6a1c21fbd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -260,7 +260,6 @@ const ExceptionsViewerComponent = ({ lists: exceptionListsMeta, exception, }); - setCurrentModal('editException'); }, [setCurrentModal, exceptionListsMeta] @@ -328,8 +327,7 @@ const ExceptionsViewerComponent = ({ `security/detections/rules/id/${encodeURI(ruleId)}/edit` ); - const showEmpty: boolean = - !isInitLoading && !loadingList && totalEndpointItems === 0 && totalDetectionsItems === 0; + const showEmpty: boolean = !isInitLoading && !loadingList && exceptions.length === 0; const showNoResults: boolean = exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0); @@ -396,7 +394,6 @@ const ExceptionsViewerComponent = ({ isInitLoading={isInitLoading} exceptions={exceptions} loadingItemIds={loadingItemIds} - commentsAccordionId={commentsAccordionId} onDeleteException={handleDeleteException} onEditExceptionItem={handleEditException} />