From 68810d535b48f52f30d5518156036301dbce1913 Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Tue, 19 Dec 2023 09:48:21 -0500 Subject: [PATCH 01/95] [Serverless Search] Setup FTR tests for Connectors UI (#173561) This PR introduces automation tests for Connectors UI for Serverless Search plugin --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../connector_config/connector_link.tsx | 2 +- .../connectors/connectors_table.tsx | 10 +- .../connectors/edit_description.tsx | 8 +- .../components/connectors/edit_name.tsx | 6 +- .../connectors/edit_service_type.tsx | 8 +- .../components/connectors_overview.tsx | 1 + .../functional/page_objects/index.ts | 2 + .../svl_search_connectors_page.ts | 160 ++++++++++++++++++ .../search/connectors/connectors_overview.ts | 112 ++++++++++++ .../functional/test_suites/search/index.ts | 1 + 10 files changed, 299 insertions(+), 11 deletions(-) create mode 100644 x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts create mode 100644 x-pack/test_serverless/functional/test_suites/search/connectors/connectors_overview.ts diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_link.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_link.tsx index b12f446aa234d..45f43a2b72b35 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_link.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_link.tsx @@ -110,7 +110,7 @@ export const ConnectorLinkElasticsearch: React.FCconnector_id - + {connectorId} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx index 2e7e6f2cf6a42..53d99b37c99a1 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connectors_table.tsx @@ -227,12 +227,17 @@ export const ConnectorsTable: React.FC = () => { filter ? `${connector[filter]}`.toLowerCase().includes(query.toLowerCase()) : true ) .slice(pageIndex * pageSize, (pageIndex + 1) * pageSize) ?? []; - return ( <> - setQuery(queryText ?? '')} query={query} /> + setQuery(queryText ?? '')} + query={query} + /> { = ({ connector }) = value={newDescription || ''} /> ) : ( - {connector.description} + + {connector.description} + )} @@ -107,7 +109,7 @@ export const EditDescription: React.FC = ({ connector }) = `} > mutate(newDescription)} @@ -125,7 +127,7 @@ export const EditDescription: React.FC = ({ connector }) = `} > { diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx index e8073796c7c9c..fbab2b15d3434 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_name.tsx @@ -69,7 +69,7 @@ export const EditName: React.FC = ({ connector }) => { {!isEditing ? ( <> - +

{connector.name || CONNECTOR_LABEL}

@@ -113,7 +113,7 @@ export const EditName: React.FC = ({ connector }) => { `} > = ({ connector }) => { `} > { diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx index c3e7c3f8455ba..a2df8f7c35bfe 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/edit_service_type.tsx @@ -37,7 +37,10 @@ export const EditServiceType: React.FC = ({ connector }) = connectorTypes?.connectors.map((connectorType) => ({ inputDisplay: ( - + = ({ connector }) = return ( - + {i18n.translate('xpack.serverlessSearch.connectors.serviceTypeLabel', { defaultMessage: 'Connector type', })} mutate(event)} options={options} diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors_overview.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors_overview.tsx index fd774f3dd4911..78c70321d178c 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors_overview.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors_overview.tsx @@ -38,6 +38,7 @@ export const ConnectorsOverview = () => { pageTitle={i18n.translate('xpack.serverlessSearch.connectors.title', { defaultMessage: 'Connectors', })} + data-test-subj="serverlessSearchConnectorsTitle" restrictWidth rightSideItems={[ diff --git a/x-pack/test_serverless/functional/page_objects/index.ts b/x-pack/test_serverless/functional/page_objects/index.ts index f9626a88b3103..541c7dce86cec 100644 --- a/x-pack/test_serverless/functional/page_objects/index.ts +++ b/x-pack/test_serverless/functional/page_objects/index.ts @@ -18,6 +18,7 @@ import { SvlSearchLandingPageProvider } from './svl_search_landing_page'; import { SvlSecLandingPageProvider } from './svl_sec_landing_page'; import { SvlTriggersActionsPageProvider } from './svl_triggers_actions_ui_page'; import { SvlRuleDetailsPageProvider } from './svl_rule_details_ui_page'; +import { SvlSearchConnectorsPageProvider } from './svl_search_connectors_page'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -28,6 +29,7 @@ export const pageObjects = { svlObltOnboardingPage: SvlObltOnboardingPageProvider, SvlObltOnboardingStreamLogFilePage: SvlObltOnboardingStreamLogFilePageProvider, svlObltOverviewPage: SvlObltOverviewPageProvider, + svlSearchConnectorsPage: SvlSearchConnectorsPageProvider, svlSearchLandingPage: SvlSearchLandingPageProvider, svlSecLandingPage: SvlSecLandingPageProvider, svlTriggersActionsUI: SvlTriggersActionsPageProvider, diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts new file mode 100644 index 0000000000000..f610251bf0719 --- /dev/null +++ b/x-pack/test_serverless/functional/page_objects/svl_search_connectors_page.ts @@ -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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../ftr_provider_context'; +export function SvlSearchConnectorsPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const retry = getService('retry'); + return { + connectorConfigurationPage: { + async createConnector() { + await testSubjects.click('serverlessSearchConnectorsOverviewCreateConnectorButton'); + await testSubjects.existOrFail('serverlessSearchEditConnectorButton'); + await testSubjects.exists('serverlessSearchConnectorLinkElasticsearchRunWithDockerButton'); + await testSubjects.exists('serverlessSearchConnectorLinkElasticsearchRunFromSourceButton'); + }, + async editDescription(description: string) { + await testSubjects.existOrFail('serverlessSearchEditDescriptionButton'); + await testSubjects.click('serverlessSearchEditDescriptionButton'); + await testSubjects.exists('serverlessSearchEditDescriptionFieldText'); + await testSubjects.existOrFail('serverlessSearchSaveDescriptionButton'); + await testSubjects.existOrFail('serverlessSearchCancelDescriptionButton'); + await testSubjects.setValue('serverlessSearchEditDescriptionFieldText', description); + await testSubjects.click('serverlessSearchSaveDescriptionButton'); + await testSubjects.exists('serverlessSearchConnectorDescription'); + expect(await testSubjects.getVisibleText('serverlessSearchConnectorDescription')).to.be( + description + ); + }, + async editName(name: string) { + await testSubjects.existOrFail('serverlessSearchEditNameButton'); + await testSubjects.click('serverlessSearchEditNameButton'); + await testSubjects.existOrFail('serverlessSearchEditNameFieldText'); + await testSubjects.existOrFail('serverlessSearchSaveNameButton'); + await testSubjects.existOrFail('serverlessSearchCancelNameButton'); + await testSubjects.setValue('serverlessSearchEditNameFieldText', name); + await testSubjects.click('serverlessSearchSaveNameButton'); + await testSubjects.exists('serverlessSearchConnectorName'); + expect(await testSubjects.getVisibleText('serverlessSearchConnectorName')).to.be(name); + }, + async editType(type: string) { + await testSubjects.existOrFail('serverlessSearchEditConnectorTypeLabel'); + await testSubjects.existOrFail('serverlessSearchEditConnectorTypeChoices'); + await testSubjects.click('serverlessSearchEditConnectorTypeChoices'); + await testSubjects.exists('serverlessSearchConnectorServiceType-zoom'); + await testSubjects.click(`serverlessSearchConnectorServiceType-${type}`); + await testSubjects.existOrFail('serverlessSearchConnectorServiceType-zoom'); + }, + async expectConnectorIdToMatchUrl(connectorId: string) { + expect(await browser.getCurrentUrl()).contain(`/app/connectors/${connectorId}`); + }, + async getConnectorId() { + return await testSubjects.getVisibleText('serverlessSearchConnectorConnectorId'); + }, + }, + connectorOverviewPage: { + async changeSearchBarTableSelectValue(option: string) { + await testSubjects.existOrFail('serverlessSearchConnectorsTableSelect'); + await testSubjects.setValue('serverlessSearchConnectorsTableSelect', option); + }, + async connectorNameExists(connectorName: string) { + const connectorsList = await this.getConnectorsList(); + return Boolean(connectorsList.find((name) => name === connectorName)); + }, + async confirmDeleteConnectorModalComponentsExists() { + await testSubjects.existOrFail('serverlessSearchDeleteConnectorModalFieldText'); + await testSubjects.existOrFail('confirmModalConfirmButton'); + await testSubjects.existOrFail('confirmModalCancelButton'); + }, + async confirmConnectorTableIsDisappearedAfterDelete() { + await retry.waitForWithTimeout('delete modal to disappear', 5000, () => + testSubjects + .missingOrFail('confirmModalConfirmButton') + .then(() => true) + .catch(() => false) + ); + browser.refresh(); + this.expectConnectorTableToHaveNoItems(); + }, + async expectConnectorOverviewPageComponentsToExist() { + await testSubjects.existOrFail('serverlessSearchConnectorsTitle'); + await testSubjects.existOrFail('serverlessSearchConnectorsOverviewElasticConnectorsLink'); + await testSubjects.exists('serverlessSearchEmptyConnectorsPromptCreateConnectorButton'); + await testSubjects.existOrFail('serverlessSearchConnectorsOverviewCreateConnectorButton'); + }, + async expectConnectorTableToExist() { + await testSubjects.existOrFail('serverlessSearchConnectorTable'); + }, + async expectConnectorTableToHaveNoItems(timeout?: number) { + await testSubjects.missingOrFail('serverlessSearchColumnsLink', { timeout }); + }, + async expectDeleteConnectorButtonExist() { + await testSubjects.existOrFail('serverlessSearchDeleteConnectorModalActionButton'); + }, + async expectSearchBarToExist() { + await testSubjects.existOrFail('serverlessSearchConnectorsTableSearchBar'); + }, + + async deleteConnectorWithCorrectName(connectorNameToBeDeleted: string) { + const fieldText = await testSubjects.find('serverlessSearchDeleteConnectorModalFieldText'); + await fieldText.clearValue(); + await retry.try(async () => { + expect( + await ( + await testSubjects.find('serverlessSearchDeleteConnectorModalFieldText') + ).getAttribute('value') + ).to.be(''); + }); + await retry.try(async () => { + await fieldText.type(connectorNameToBeDeleted); + }); + const isEnabled = await testSubjects.isEnabled('confirmModalConfirmButton'); + expect(isEnabled).to.be(true); + await retry.try(async () => await testSubjects.click('confirmModalConfirmButton')); + }, + async deleteConnectorIncorrectName(incorrectName: string) { + const fieldText = await testSubjects.find('serverlessSearchDeleteConnectorModalFieldText'); + await fieldText.clearValue(); + await retry.try(async () => { + await fieldText.type(incorrectName); + }); + const isEnabled = await testSubjects.isEnabled('confirmModalConfirmButton'); + expect(isEnabled).to.be(false); + }, + async getConnectorFromConnectorTable(connectorName: string) { + await testSubjects.getAttribute('serverlessSearchColumnsLink', connectorName); + }, + async getConnectorsList() { + const rows = await ( + await testSubjects.find('serverlessSearchConnectorTable') + ).findAllByCssSelector('.euiTableRow'); + return await Promise.all( + rows.map(async (connector) => { + return await ( + await connector.findByTestSubject('serverlessSearchColumnsLink') + ).getVisibleText(); + }) + ); + }, + async openDeleteConnectorModal() { + await retry.try( + async () => await testSubjects.click('serverlessSearchDeleteConnectorModalActionButton') + ); + await testSubjects.exists('confirmModalBodyText'); + expect(await testSubjects.getVisibleText('confirmModalBodyText')).to.be( + 'This action cannot be undone. Please type my-connector to confirm.\nConnector name' + ); + }, + async setSearchBarValue(value: string) { + await testSubjects.setValue('serverlessSearchConnectorsTableSearchBar', value); + await testSubjects.pressEnter('serverlessSearchConnectorsTableSearchBar'); + }, + }, + }; +} diff --git a/x-pack/test_serverless/functional/test_suites/search/connectors/connectors_overview.ts b/x-pack/test_serverless/functional/test_suites/search/connectors/connectors_overview.ts new file mode 100644 index 0000000000000..d5fedb8c54585 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/search/connectors/connectors_overview.ts @@ -0,0 +1,112 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; +const TEST_CONNECTOR_NAME = 'my-connector'; +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects([ + 'svlCommonPage', + 'svlCommonNavigation', + 'common', + 'svlSearchConnectorsPage', + ]); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + describe('connectors', function () { + before(async () => { + await pageObjects.svlCommonPage.login(); + await pageObjects.svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'serverlessConnectors', + }); + }); + + after(async () => { + await pageObjects.svlCommonPage.forceLogout(); + }); + + it('Connector app is loaded and has no connectors', async () => { + await pageObjects.svlSearchConnectorsPage.connectorOverviewPage.expectConnectorOverviewPageComponentsToExist(); + }); + describe('create and configure connector', async () => { + it('create connector and confirm connector configuration page is loaded', async () => { + await pageObjects.svlSearchConnectorsPage.connectorConfigurationPage.createConnector(); + await pageObjects.svlSearchConnectorsPage.connectorConfigurationPage.expectConnectorIdToMatchUrl( + await pageObjects.svlSearchConnectorsPage.connectorConfigurationPage.getConnectorId() + ); + }); + it('edit description', async () => { + await pageObjects.svlSearchConnectorsPage.connectorConfigurationPage.editDescription( + 'test description' + ); + }); + it('edit name', async () => { + await pageObjects.svlSearchConnectorsPage.connectorConfigurationPage.editName( + TEST_CONNECTOR_NAME + ); + }); + it('edit type', async () => { + await pageObjects.svlSearchConnectorsPage.connectorConfigurationPage.editType('zoom'); + }); + it('confirm connector is created', async () => { + await pageObjects.svlCommonNavigation.sidenav.clickLink({ + deepLinkId: 'serverlessConnectors', + }); + browser.refresh(); + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.expectConnectorTableToExist(); + }); + }); + describe('connector table', async () => { + it('confirm searchBar to exist', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.expectSearchBarToExist(); + }); + + it('searchBar and select filter connector table', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.getConnectorFromConnectorTable( + TEST_CONNECTOR_NAME + ); + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.setSearchBarValue( + TEST_CONNECTOR_NAME + ); + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.connectorNameExists( + TEST_CONNECTOR_NAME + ); + + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.changeSearchBarTableSelectValue( + 'Type' + ); + + await testSubjects.click('clearSearchButton'); + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.setSearchBarValue('confluence'); + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.expectConnectorTableToHaveNoItems(); + await testSubjects.click('clearSearchButton'); + }); + }); + describe('delete connector', async () => { + it('delete connector button exist in table', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.expectDeleteConnectorButtonExist(); + }); + it('open delete connector modal', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.openDeleteConnectorModal(); + }); + it('delete connector button open modal', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.confirmDeleteConnectorModalComponentsExists(); + }); + it('delete connector field is disabled if field name does not match connector name', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.deleteConnectorIncorrectName( + 'invalid' + ); + }); + it('delete connector button deletes connector', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.deleteConnectorWithCorrectName( + TEST_CONNECTOR_NAME + ); + }); + it('confirm connector table is disappeared after delete ', async () => { + pageObjects.svlSearchConnectorsPage.connectorOverviewPage.confirmConnectorTableIsDisappearedAfterDelete(); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/search/index.ts b/x-pack/test_serverless/functional/test_suites/search/index.ts index 6d0987d0292ce..8b753e58005fa 100644 --- a/x-pack/test_serverless/functional/test_suites/search/index.ts +++ b/x-pack/test_serverless/functional/test_suites/search/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless search UI', function () { loadTestFile(require.resolve('./landing_page')); + loadTestFile(require.resolve('./connectors/connectors_overview')); loadTestFile(require.resolve('./default_dataview')); loadTestFile(require.resolve('./navigation')); loadTestFile(require.resolve('./cases/attachment_framework')); From 64bd042a21c34f95145c6117c87760134637a657 Mon Sep 17 00:00:00 2001 From: Liam Thompson <32779855+leemthompo@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:48:40 +0100 Subject: [PATCH 02/95] [Enterprise Search] Clean up copy (#173622) Somehow this copy snuck in without being edited, looks like a copy paste from a former tooltip or something, with no punctuation. This PR cleans the text up. ## Before Screenshot 2023-12-19 at 14 05 56 --- .../new_index/select_connector/select_connector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx index 31c4f9cc7a71b..8c75fa9a4b1b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/new_index/select_connector/select_connector.tsx @@ -225,7 +225,7 @@ export const SelectConnector: React.FC = () => { 'xpack.enterpriseSearch.selectConnector.p.areAvailableDirectlyWithinLabel', { defaultMessage: - 'Are available directly within Elastic Cloud deployments No additional infrastructure is required You can also convert them as self hosted Connectors client at any moment', + 'Available directly within Elastic Cloud deployments. No additional infrastructure is required. You can also convert native connectors to self-hosted connector clients.', } )}

@@ -259,7 +259,7 @@ export const SelectConnector: React.FC = () => { 'xpack.enterpriseSearch.selectConnector.p.deployConnectorsOnYourLabel', { defaultMessage: - 'Deploy connectors on your own infrastructure You can also customize existing Connector clients or build your own using our connector framework', + 'Deploy connectors on your own infrastructure. You can also customize existing connector clients, or build your own using our connector framework.', } )}

From b9d3c8611876632c587ca854125315c6a5c303bb Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:07:25 +0100 Subject: [PATCH 03/95] [Fleet] show download rate in upgrade details tooltlip (#173614) ## Summary Closes https://github.com/elastic/kibana/issues/171943 Showing download rate in the upgrade details tooltip. Used fake data as I couldn't get an actual agent to be in downloading state with download percent and rate. image Insert test ES data with curl and go to Agent list/details to see the tooltip, replace `existing_agent_id` with an existing agent's id or insert a full agent doc. ``` curl -sk -XPOST --user elastic:changeme -H 'content-type:application/json' \ http://localhost:9200/_security/role/fleet_superuser -d ' { "indices": [ { "names": [".fleet*",".kibana*"], "privileges": ["all"], "allow_restricted_indices": true } ] }' curl -sk -XPOST --user elastic:changeme -H 'content-type:application/json' \ http://localhost:9200/_security/user/fleet_superuser -d ' { "password": "password", "roles": ["superuser", "fleet_superuser"] }' curl -sk -XPOST --user fleet_superuser:password -H 'content-type:application/json' \ -H'x-elastic-product-origin:fleet' \ http://localhost:9200/.fleet-agents/_update_by_query -d ' { "script": { "source": "ctx._source.upgrade_details.state = \"UPG_DOWNLOADING\"; ctx._source.upgrade_details.metadata.download_percent = 22; ctx._source.upgrade_details.metadata.download_rate = 1223912;", "lang": "painless" }, "query": { "term": { "agent.id":"existing_agent_id" } } }' ``` ### Checklist - [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 --- .../fleet/common/types/models/agent.ts | 1 + .../components/agent_upgrade_status.test.tsx | 32 +++++++++++++++++-- .../components/agent_upgrade_status.tsx | 30 +++++++++++++---- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/fleet/common/types/models/agent.ts b/x-pack/plugins/fleet/common/types/models/agent.ts index 120f0bc883e1d..1242a61124952 100644 --- a/x-pack/plugins/fleet/common/types/models/agent.ts +++ b/x-pack/plugins/fleet/common/types/models/agent.ts @@ -446,6 +446,7 @@ export interface AgentUpgradeDetails { metadata?: { scheduled_at?: string; download_percent?: number; + download_rate?: number; // bytes per second failed_state?: AgentUpgradeStateType; error_msg?: string; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.test.tsx index 1518a68fd6f0c..a5f3498fd0b59 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.test.tsx @@ -48,12 +48,38 @@ describe('getDownloadEstimate', () => { expect(getDownloadEstimate()).toEqual(''); }); - it('should return an empty string if the agent has a zero download percent', () => { - expect(getDownloadEstimate(0)).toEqual(''); + it('should display 0% if the agent has a zero download percent', () => { + expect(getDownloadEstimate({ download_percent: 0 })).toEqual(' (0%)'); + }); + + it('should display 0 Bps if the agent has a zero download rate', () => { + expect(getDownloadEstimate({ download_rate: 0 })).toEqual(' (at 0.0 Bps)'); }); it('should return a formatted string if the agent has a positive download percent', () => { - expect(getDownloadEstimate(16.4)).toEqual(' (16.4%)'); + expect(getDownloadEstimate({ download_percent: 16.4 })).toEqual(' (16.4%)'); + }); + + it('should return a formatted string if the agent has a kBps download rate', () => { + expect(getDownloadEstimate({ download_rate: 1024 })).toEqual(' (at 1.0 kBps)'); + }); + + it('should return a formatted string if the agent has a download rate and download percent', () => { + expect(getDownloadEstimate({ download_rate: 10, download_percent: 99 })).toEqual( + ' (99% at 10.0 Bps)' + ); + }); + + it('should return a formatted string if the agent has a MBps download rate', () => { + expect(getDownloadEstimate({ download_rate: 1200000 })).toEqual(' (at 1.1 MBps)'); + }); + + it('should return a formatted string if the agent has a GBps download rate', () => { + expect(getDownloadEstimate({ download_rate: 2400000000 })).toEqual(' (at 2.2 GBps)'); + }); + + it('should return a formatted string if the agent has a GBps download rate more than 1024', () => { + expect(getDownloadEstimate({ download_rate: 1200000000 * 1024 })).toEqual(' (at 1144.4 GBps)'); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.tsx index 203b490f9f3ef..e5cf2eb7913c6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_upgrade_status.tsx @@ -35,14 +35,34 @@ export function getUpgradeStartDelay(scheduledAt?: string): string { return ` The upgrade will start in less than ${Math.ceil(timeDiffMillis / 36e5)} hours.`; } -export function getDownloadEstimate(downloadPercent?: number): string { - if (!downloadPercent || downloadPercent === 0) { +export function getDownloadEstimate(metadata?: AgentUpgradeDetails['metadata']): string { + if ( + !metadata || + (metadata.download_percent === undefined && metadata.download_rate === undefined) + ) { return ''; } + let tooltip = ''; + if (metadata.download_percent !== undefined) { + tooltip = `${metadata.download_percent}%`; + } + if (metadata.download_rate !== undefined) { + tooltip += ` at ${formatRate(metadata.download_rate)}`; + } - return ` (${downloadPercent}%)`; + return ` (${tooltip.trim()})`; } +const formatRate = (downloadRate: number) => { + let i = 0; + const byteUnits = [' Bps', ' kBps', ' MBps', ' GBps']; + for (; i < byteUnits.length - 1; i++) { + if (downloadRate < 1024) break; + downloadRate = downloadRate / 1024; + } + return downloadRate.toFixed(1) + byteUnits[i]; +}; + function getStatusComponents(agentUpgradeDetails?: AgentUpgradeDetails) { switch (agentUpgradeDetails?.state) { case 'UPG_REQUESTED': @@ -97,9 +117,7 @@ function getStatusComponents(agentUpgradeDetails?: AgentUpgradeDetails) { id="xpack.fleet.agentUpgradeStatusTooltip.upgradeDownloading" defaultMessage="Downloading the new agent artifact version{downloadEstimate}." values={{ - downloadEstimate: getDownloadEstimate( - agentUpgradeDetails?.metadata?.download_percent - ), + downloadEstimate: getDownloadEstimate(agentUpgradeDetails?.metadata), }} /> ), From b1457d0cb10c0884220573ac84d7e79c36780aeb Mon Sep 17 00:00:00 2001 From: Panagiota Mitsopoulou Date: Tue, 19 Dec 2023 16:16:09 +0100 Subject: [PATCH 04/95] attach slo card to dashboard (#172670) Fixes https://github.com/elastic/kibana/issues/171349 https://github.com/elastic/kibana/assets/2852703/f951885a-e0f3-4f12-bfa0-fe5e354c9285 --------- Co-authored-by: shahzad31 --- x-pack/plugins/observability/kibana.jsonc | 4 +- .../public/application/index.tsx | 85 ++++++++++--------- .../components/card_view/slo_card_item.tsx | 42 +++++++-- .../card_view/slo_card_item_actions.tsx | 1 + .../slos/components/slo_item_actions.tsx | 19 +++++ .../pages/slos/components/slo_list_item.tsx | 1 + .../pages/slos/hooks/use_slo_list_actions.ts | 33 +++++++ x-pack/plugins/observability/public/plugin.ts | 3 + x-pack/plugins/observability/tsconfig.json | 1 + 9 files changed, 139 insertions(+), 50 deletions(-) diff --git a/x-pack/plugins/observability/kibana.jsonc b/x-pack/plugins/observability/kibana.jsonc index 690fd21462128..d5633ad9f36fe 100644 --- a/x-pack/plugins/observability/kibana.jsonc +++ b/x-pack/plugins/observability/kibana.jsonc @@ -21,6 +21,7 @@ "dataViewEditor", "embeddable", "uiActions", + "presentationUtil", "exploratoryView", "features", "files", @@ -55,7 +56,8 @@ "kibanaUtils", "unifiedSearch", "stackAlerts", - "spaces" + "spaces", + "embeddable" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 7081005487fc3..7b01bc1f8eeb1 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -84,51 +84,54 @@ export const renderApp = ({ const ApplicationUsageTrackingProvider = usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; const CloudProvider = plugins.cloud?.CloudContextProvider ?? React.Fragment; + const PresentationContextProvider = plugins.presentationUtil?.ContextProvider ?? React.Fragment; ReactDOM.render( - - - - - - + + + + + - - - - - - - - - - - - - - - - - - , + + + + + + + + + + + + + + + + + + + + , element ); return () => { diff --git a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx index 04a23c296a602..ccdfe39481558 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item.tsx @@ -20,6 +20,10 @@ import { ALL_VALUE, HistoricalSummaryResponse, SLOWithSummaryResponse } from '@k import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; +import { + LazySavedObjectSaveModalDashboard, + withSuspense, +} from '@kbn/presentation-util-plugin/public'; import { SloCardBadgesPortal } from './badges_portal'; import { useSloListActions } from '../../hooks/use_slo_list_actions'; import { BurnRateRuleFlyout } from '../common/burn_rate_rule_flyout'; @@ -30,7 +34,7 @@ import { SloCardItemActions } from './slo_card_item_actions'; import { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo'; import { SloDeleteConfirmationModal } from '../../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; import { SloCardItemBadges } from './slo_card_item_badges'; - +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); export interface Props { slo: SLOWithSummaryResponse; rules: Array> | undefined; @@ -64,15 +68,17 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isAddRuleFlyoutOpen, setIsAddRuleFlyoutOpen] = useState(false); const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); - + const [isDashboardAttachmentReady, setDashboardAttachmentReady] = useState(false); const historicalSliData = formatHistoricalData(historicalSummary, 'sli_value'); - const { handleCreateRule, handleDeleteCancel, handleDeleteConfirm } = useSloListActions({ - slo, - setDeleteConfirmationModalOpen, - setIsActionsPopoverOpen, - setIsAddRuleFlyoutOpen, - }); + const { handleCreateRule, handleDeleteCancel, handleDeleteConfirm, handleAttachToDashboardSave } = + useSloListActions({ + slo, + setDeleteConfirmationModalOpen, + setIsActionsPopoverOpen, + setIsAddRuleFlyoutOpen, + setDashboardAttachmentReady, + }); return ( <> @@ -104,6 +110,7 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards setIsActionsPopoverOpen={setIsActionsPopoverOpen} setIsAddRuleFlyoutOpen={setIsAddRuleFlyoutOpen} setDeleteConfirmationModalOpen={setDeleteConfirmationModalOpen} + setDashboardAttachmentReady={setDashboardAttachmentReady} /> )} @@ -130,6 +137,25 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, cards onConfirm={handleDeleteConfirm} /> ) : null} + {isDashboardAttachmentReady ? ( + { + setDashboardAttachmentReady(false); + }} + onSave={handleAttachToDashboardSave} + /> + ) : null} ); } diff --git a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_actions.tsx b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_actions.tsx index 51d1887d433fb..ff4d7f363caee 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_actions.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/card_view/slo_card_item_actions.tsx @@ -41,6 +41,7 @@ interface Props { setIsActionsPopoverOpen: (value: boolean) => void; setDeleteConfirmationModalOpen: (value: boolean) => void; setIsAddRuleFlyoutOpen: (value: boolean) => void; + setDashboardAttachmentReady: (value: boolean) => void; } export function SloCardItemActions(props: Props) { diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx index 51652abe13542..8bda3b98bd510 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_item_actions.tsx @@ -30,6 +30,7 @@ interface Props { setIsActionsPopoverOpen: (value: boolean) => void; setDeleteConfirmationModalOpen: (value: boolean) => void; setIsAddRuleFlyoutOpen: (value: boolean) => void; + setDashboardAttachmentReady?: (value: boolean) => void; btnProps?: Partial; } const CustomShadowPanel = styled(EuiPanel)<{ shadow: string }>` @@ -59,6 +60,7 @@ export function SloItemActions({ setIsActionsPopoverOpen, setIsAddRuleFlyoutOpen, setDeleteConfirmationModalOpen, + setDashboardAttachmentReady, btnProps, }: Props) { const { @@ -110,6 +112,13 @@ export function SloItemActions({ setIsAddRuleFlyoutOpen(true); }; + const handleAttachToDashboard = () => { + setIsActionsPopoverOpen(false); + if (setDashboardAttachmentReady) { + setDashboardAttachmentReady(true); + } + }; + const btn = ( , + + {i18n.translate('xpack.observability.slo.item.actions.attachToDashboard', { + defaultMessage: 'Attach to Dashboard', + })} + , ]} /> diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx index 31455cea01905..dc3865af00050 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list_item.tsx @@ -9,6 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import type { Rule } from '@kbn/triggers-actions-ui-plugin/public'; import React, { useState } from 'react'; + import { SloDeleteConfirmationModal } from '../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; import { useSloFormattedSummary } from '../hooks/use_slo_summary'; import { BurnRateRuleFlyout } from './common/burn_rate_rule_flyout'; diff --git a/x-pack/plugins/observability/public/pages/slos/hooks/use_slo_list_actions.ts b/x-pack/plugins/observability/public/pages/slos/hooks/use_slo_list_actions.ts index 169e1e54c5222..bdc6cf3a4c49d 100644 --- a/x-pack/plugins/observability/public/pages/slos/hooks/use_slo_list_actions.ts +++ b/x-pack/plugins/observability/public/pages/slos/hooks/use_slo_list_actions.ts @@ -6,19 +6,26 @@ */ import { SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public'; +import { useCallback } from 'react'; import { useDeleteSlo } from '../../../hooks/slo/use_delete_slo'; +import { SLO_EMBEDDABLE } from '../../../embeddable/slo/overview/slo_embeddable'; +import { useKibana } from '../../../utils/kibana_react'; export function useSloListActions({ slo, setIsAddRuleFlyoutOpen, setIsActionsPopoverOpen, setDeleteConfirmationModalOpen, + setDashboardAttachmentReady, }: { slo: SLOWithSummaryResponse; setIsActionsPopoverOpen: (val: boolean) => void; setIsAddRuleFlyoutOpen: (val: boolean) => void; setDeleteConfirmationModalOpen: (val: boolean) => void; + setDashboardAttachmentReady?: (val: boolean) => void; }) { + const { embeddable } = useKibana().services; const { mutate: deleteSlo } = useDeleteSlo(); const handleDeleteConfirm = () => { @@ -34,9 +41,35 @@ export function useSloListActions({ setIsAddRuleFlyoutOpen(true); }; + const handleAttachToDashboardSave: SaveModalDashboardProps['onSave'] = useCallback( + ({ dashboardId, newTitle, newDescription }) => { + const stateTransfer = embeddable!.getStateTransfer(); + const embeddableInput = { + title: newTitle, + description: newDescription, + sloId: slo.id, + sloInstanceId: slo.instanceId, + }; + + const state = { + input: embeddableInput, + type: SLO_EMBEDDABLE, + }; + + const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; + + stateTransfer.navigateToWithEmbeddablePackage('dashboards', { + state, + path, + }); + }, + [embeddable, slo.id, slo.instanceId] + ); + return { handleDeleteConfirm, handleDeleteCancel, handleCreateRule, + handleAttachToDashboardSave, }; } diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 2e716f42d0713..808573579cb99 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -67,6 +67,7 @@ import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/pu import type { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public'; import { firstValueFrom } from 'rxjs'; +import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import { observabilityAppId, observabilityFeatureId } from '../common'; import { ALERTS_PATH, @@ -123,6 +124,7 @@ export interface ObservabilityPublicPluginsSetup { uiActions: UiActionsSetup; licensing: LicensingPluginSetup; serverless?: ServerlessPluginSetup; + presentationUtil?: PresentationUtilPluginStart; } export interface ObservabilityPublicPluginsStart { actionTypeRegistry: ActionTypeRegistryContract; @@ -153,6 +155,7 @@ export interface ObservabilityPublicPluginsStart { serverless?: ServerlessPluginStart; uiSettings: IUiSettingsClient; uiActions: UiActionsStart; + presentationUtil?: PresentationUtilPluginStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index 6cc8bbfef132f..61762322f9eed 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -97,6 +97,7 @@ "@kbn/serverless", "@kbn/dashboard-plugin", "@kbn/calculate-auto", + "@kbn/presentation-util-plugin", "@kbn/task-manager-plugin", "@kbn/core-elasticsearch-client-server-mocks", "@kbn/core-saved-objects-api-server-mocks" From 86561c2ace1924abb1d36c0c7abfd16b46a135c8 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 19 Dec 2023 16:20:37 +0100 Subject: [PATCH 05/95] [UX INP] Fix label casing (#173605) ## Summary Fix label casing !! --- .../components/sections/ux/core_web_vitals/translations.ts | 2 +- x-pack/plugins/ux/e2e/journeys/inp.journey.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/public/pages/overview/components/sections/ux/core_web_vitals/translations.ts b/x-pack/plugins/observability/public/pages/overview/components/sections/ux/core_web_vitals/translations.ts index d6445320a7978..bded2fd67cca8 100644 --- a/x-pack/plugins/observability/public/pages/overview/components/sections/ux/core_web_vitals/translations.ts +++ b/x-pack/plugins/observability/public/pages/overview/components/sections/ux/core_web_vitals/translations.ts @@ -16,7 +16,7 @@ export const LCP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.lcp', }); export const INP_LABEL = i18n.translate('xpack.observability.ux.coreVitals.inp', { - defaultMessage: 'Interaction to Next Paint', + defaultMessage: 'Interaction to next paint', }); export const CLS_LABEL = i18n.translate('xpack.observability.ux.coreVitals.cls', { diff --git a/x-pack/plugins/ux/e2e/journeys/inp.journey.ts b/x-pack/plugins/ux/e2e/journeys/inp.journey.ts index 800ae3e38b3fd..8422dacf03781 100644 --- a/x-pack/plugins/ux/e2e/journeys/inp.journey.ts +++ b/x-pack/plugins/ux/e2e/journeys/inp.journey.ts @@ -59,7 +59,7 @@ journey('INP', async ({ page, params }) => { }); step('Check INP Values', async () => { - expect(await page.$('text=Interaction to Next Paint')); + expect(await page.$('text=Interaction to next paint')); await page.waitForSelector('[data-test-subj=inp-core-vital] > .euiTitle'); await page.waitForSelector('text=381 ms'); }); From d79f191b4e12a1ae7634c1a1f5e676aeec499b93 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Tue, 19 Dec 2023 08:28:17 -0700 Subject: [PATCH 06/95] [Discover] Include column width in shared links (#172405) ## Summary Resolves https://github.com/elastic/kibana/issues/170577. Includes the `grid` properties (which include specified column widths) in the shareable links generated from Discover. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Release note Discover sharing links now preserve customized column widths. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/discover/common/locator.test.ts | 9 +++++++-- src/plugins/discover/common/locator.ts | 9 +++++++++ .../main/components/top_nav/get_top_nav_links.tsx | 3 +-- .../main/services/discover_app_state_container.ts | 4 ++-- .../application/main/services/discover_state.ts | 1 + src/plugins/saved_search/common/types.ts | 13 +++++-------- src/plugins/saved_search/tsconfig.json | 1 + 7 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/plugins/discover/common/locator.test.ts b/src/plugins/discover/common/locator.test.ts index 80bc4ceebed2b..93da54ad365e9 100644 --- a/src/plugins/discover/common/locator.test.ts +++ b/src/plugins/discover/common/locator.test.ts @@ -218,17 +218,22 @@ describe('Discover url generator', () => { expect(path).toContain('__test__'); }); - test('can specify columns, interval, sort and savedQuery', async () => { + test('can specify columns, grid, interval, sort and savedQuery', async () => { const { locator } = await setup(); const { path } = await locator.getLocation({ columns: ['_source'], + grid: { + columns: { + _source: { width: 150 }, + }, + }, interval: 'auto', sort: [['timestamp, asc']] as string[][] & SerializableRecord, savedQuery: '__savedQueryId__', }); expect(path).toMatchInlineSnapshot( - `"#/?_a=(columns:!(_source),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"` + `"#/?_a=(columns:!(_source),grid:(columns:(_source:(width:150))),interval:auto,savedQuery:__savedQueryId__,sort:!(!('timestamp,%20asc')))"` ); }); diff --git a/src/plugins/discover/common/locator.ts b/src/plugins/discover/common/locator.ts index 70e60f55b5fb1..9be9947e743dd 100644 --- a/src/plugins/discover/common/locator.ts +++ b/src/plugins/discover/common/locator.ts @@ -10,6 +10,7 @@ import type { SerializableRecord } from '@kbn/utility-types'; import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query'; import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public'; import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public'; +import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; import { DataViewSpec } from '@kbn/data-views-plugin/common'; import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; import { VIEW_MODE } from './constants'; @@ -70,6 +71,11 @@ export interface DiscoverAppLocatorParams extends SerializableRecord { */ columns?: string[]; + /** + * Data Grid related state + */ + grid?: DiscoverGridSettings; + /** * Used interval of the histogram */ @@ -139,6 +145,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition Get links -> Snapshot const params: DiscoverAppLocatorParams = { - ...otherState, + ...appState, ...(savedSearch.id ? { savedSearchId: savedSearch.id } : {}), ...(dataView?.isPersisted() ? { dataViewId: dataView?.id } diff --git a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts index facf8e26fb851..e1614bf796391 100644 --- a/src/plugins/discover/public/application/main/services/discover_app_state_container.ts +++ b/src/plugins/discover/public/application/main/services/discover_app_state_container.ts @@ -24,7 +24,7 @@ import { SavedSearch, VIEW_MODE } from '@kbn/saved-search-plugin/public'; import { IKbnUrlStateStorage, ISyncStateRef, syncState } from '@kbn/kibana-utils-plugin/public'; import { isEqual } from 'lodash'; import { connectToQueryState, syncGlobalQueryStateWithUrl } from '@kbn/data-plugin/public'; -import type { UnifiedDataTableSettings } from '@kbn/unified-data-table'; +import type { DiscoverGridSettings } from '@kbn/saved-search-plugin/common'; import type { DiscoverServices } from '../../../build_services'; import { addLog } from '../../../utils/add_log'; import { cleanupUrlState } from '../utils/cleanup_url_state'; @@ -94,7 +94,7 @@ export interface DiscoverAppState { /** * Data Grid related state */ - grid?: UnifiedDataTableSettings; + grid?: DiscoverGridSettings; /** * Hide chart */ diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 1dc58643ebdc0..8994afb8a5f96 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -570,6 +570,7 @@ function createUrlGeneratorState({ : data.query.timefilter.timefilter.getTime(), searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, columns: appState.columns, + grid: appState.grid, sort: appState.sort, savedQuery: appState.savedQuery, interval: appState.interval, diff --git a/src/plugins/saved_search/common/types.ts b/src/plugins/saved_search/common/types.ts index c47548aebd8d4..acb98d26a0d14 100644 --- a/src/plugins/saved_search/common/types.ts +++ b/src/plugins/saved_search/common/types.ts @@ -9,13 +9,14 @@ import type { ISearchSource, RefreshInterval, TimeRange } from '@kbn/data-plugin/common'; import type { SavedObjectReference } from '@kbn/core-saved-objects-server'; import type { SavedObjectsResolveResponse } from '@kbn/core/server'; +import type { SerializableRecord } from '@kbn/utility-types'; import { VIEW_MODE } from '.'; -export interface DiscoverGridSettings { +export interface DiscoverGridSettings extends SerializableRecord { columns?: Record; } -export interface DiscoverGridSettingsColumn { +export interface DiscoverGridSettingsColumn extends SerializableRecord { width?: number; } @@ -25,9 +26,7 @@ export interface SavedSearchAttributes { sort: Array<[string, string]>; columns: string[]; description: string; - grid: { - columns?: Record; - }; + grid: DiscoverGridSettings; hideChart: boolean; isTextBasedQuery: boolean; usesAdHocDataView?: boolean; @@ -59,9 +58,7 @@ export interface SavedSearch { columns?: string[]; description?: string; tags?: string[] | undefined; - grid?: { - columns?: Record; - }; + grid?: DiscoverGridSettings; hideChart?: boolean; viewMode?: VIEW_MODE; hideAggregatedPreview?: boolean; diff --git a/src/plugins/saved_search/tsconfig.json b/src/plugins/saved_search/tsconfig.json index 7ed2cb4e82119..b1aa1679469ee 100644 --- a/src/plugins/saved_search/tsconfig.json +++ b/src/plugins/saved_search/tsconfig.json @@ -31,6 +31,7 @@ "@kbn/discover-utils", "@kbn/logging", "@kbn/core-plugins-server", + "@kbn/utility-types", ], "exclude": [ "target/**/*", From 99763dc61647c817384019f6603de7ad258eea01 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Tue, 19 Dec 2023 08:32:31 -0700 Subject: [PATCH 07/95] [Lens] Fix context formula functions (#172710) ## Summary Fix https://github.com/elastic/kibana/issues/170762 https://github.com/elastic/kibana/assets/315764/f5b50ffa-4a03-45ee-bc7a-2f2aca7fa3bd ### Checklist - [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 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../kbn-es-query/src/expressions/types.ts | 1 + packages/kbn-optimizer/limits.yml | 2 +- .../__snapshots__/kibana.test.ts.snap | 1 + .../common/search/expressions/kibana.test.ts | 1 + .../data/common/search/expressions/kibana.ts | 1 + .../expression_functions/specs/index.ts | 1 + .../expression_functions/specs/math_column.ts | 6 +- .../common/expression_functions/types.ts | 2 + .../formula_context/context_fns.test.ts | 91 ++++++++++++++++ .../formula_context/context_fns.ts | 97 +++++++++++++++++ .../expressions/formula_context/index.ts | 8 ++ .../plugins/lens/common/expressions/index.ts | 1 + .../formula/context_variables.test.ts | 101 ------------------ .../definitions/formula/context_variables.tsx | 77 ++++++------- .../config_panel/config_panel.test.tsx | 1 + .../workspace_panel/workspace_panel.tsx | 17 +-- .../lens/public/embeddable/embeddable.tsx | 1 + x-pack/plugins/lens/public/expressions.ts | 8 ++ .../lens/public/state_management/selectors.ts | 3 + .../lens/server/expressions/expressions.ts | 6 ++ .../translations/translations/fr-FR.json | 3 - .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 23 files changed, 264 insertions(+), 171 deletions(-) create mode 100644 x-pack/plugins/lens/common/expressions/formula_context/context_fns.test.ts create mode 100644 x-pack/plugins/lens/common/expressions/formula_context/context_fns.ts create mode 100644 x-pack/plugins/lens/common/expressions/formula_context/index.ts diff --git a/packages/kbn-es-query/src/expressions/types.ts b/packages/kbn-es-query/src/expressions/types.ts index 42dc021c9b752..ed367adc7973a 100644 --- a/packages/kbn-es-query/src/expressions/types.ts +++ b/packages/kbn-es-query/src/expressions/types.ts @@ -9,6 +9,7 @@ import { Filter, Query, TimeRange } from '../filters'; export interface ExecutionContextSearch { + now?: number; filters?: Filter[]; query?: Query | Query[]; timeRange?: TimeRange; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 1e4c82b5226df..d38e1399ed3aa 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -86,7 +86,7 @@ pageLoadAssetSize: kibanaUsageCollection: 16463 kibanaUtils: 79713 kubernetesSecurity: 77234 - lens: 39000 + lens: 41000 licenseManagement: 41817 licensing: 29004 links: 44490 diff --git a/src/plugins/data/common/search/expressions/__snapshots__/kibana.test.ts.snap b/src/plugins/data/common/search/expressions/__snapshots__/kibana.test.ts.snap index 2400f7a1f67d6..7bcf782fbbbc2 100644 --- a/src/plugins/data/common/search/expressions/__snapshots__/kibana.test.ts.snap +++ b/src/plugins/data/common/search/expressions/__snapshots__/kibana.test.ts.snap @@ -14,6 +14,7 @@ Object { }, }, ], + "now": 0, "query": Array [ Object { "language": "lucene", diff --git a/src/plugins/data/common/search/expressions/kibana.test.ts b/src/plugins/data/common/search/expressions/kibana.test.ts index c82bc0293cefe..4992a345bd0d2 100644 --- a/src/plugins/data/common/search/expressions/kibana.test.ts +++ b/src/plugins/data/common/search/expressions/kibana.test.ts @@ -20,6 +20,7 @@ describe('interpreter/functions#kibana', () => { beforeEach(() => { input = { timeRange: { from: '0', to: '1' } }; search = { + now: 0, type: 'kibana_context', query: { language: 'lucene', query: 'geo.src:US' }, filters: [ diff --git a/src/plugins/data/common/search/expressions/kibana.ts b/src/plugins/data/common/search/expressions/kibana.ts index 83d2cdc1c64b9..ad8405a51418c 100644 --- a/src/plugins/data/common/search/expressions/kibana.ts +++ b/src/plugins/data/common/search/expressions/kibana.ts @@ -41,6 +41,7 @@ export const kibana: ExpressionFunctionKibana = { // TODO: But it shouldn't be need. ...input, type: 'kibana_context', + now: getSearchContext().now ?? Date.now(), query: [...toArray(getSearchContext().query), ...toArray((input || {}).query)], filters: [...(getSearchContext().filters || []), ...((input || {}).filters || [])], timeRange: getSearchContext().timeRange || (input ? input.timeRange : undefined), diff --git a/src/plugins/expressions/common/expression_functions/specs/index.ts b/src/plugins/expressions/common/expression_functions/specs/index.ts index 0e473e4a79e5a..37af3ede5ebe0 100644 --- a/src/plugins/expressions/common/expression_functions/specs/index.ts +++ b/src/plugins/expressions/common/expression_functions/specs/index.ts @@ -17,6 +17,7 @@ export * from './overall_metric'; export * from './derivative'; export * from './moving_average'; export * from './ui_setting'; +export * from './math_column'; export type { MapColumnArguments } from './map_column'; export { mapColumn } from './map_column'; export type { MathArguments, MathInput } from './math'; diff --git a/src/plugins/expressions/common/expression_functions/specs/math_column.ts b/src/plugins/expressions/common/expression_functions/specs/math_column.ts index e056bc6b876e1..6b75af7de4ca9 100644 --- a/src/plugins/expressions/common/expression_functions/specs/math_column.ts +++ b/src/plugins/expressions/common/expression_functions/specs/math_column.ts @@ -18,12 +18,14 @@ export type MathColumnArguments = MathArguments & { copyMetaFrom?: string | null; }; -export const mathColumn: ExpressionFunctionDefinition< +export type ExpressionFunctionMathColumn = ExpressionFunctionDefinition< 'mathColumn', Datatable, MathColumnArguments, Promise -> = { +>; + +export const mathColumn: ExpressionFunctionMathColumn = { name: 'mathColumn', type: 'datatable', inputTypes: ['datatable'], diff --git a/src/plugins/expressions/common/expression_functions/types.ts b/src/plugins/expressions/common/expression_functions/types.ts index 018ee9e9fac0c..c59169ccf04ab 100644 --- a/src/plugins/expressions/common/expression_functions/types.ts +++ b/src/plugins/expressions/common/expression_functions/types.ts @@ -20,6 +20,7 @@ import { ExpressionFunctionDerivative, ExpressionFunctionMovingAverage, ExpressionFunctionOverallMetric, + ExpressionFunctionMathColumn, } from './specs'; import { ExpressionAstFunction } from '../ast'; @@ -132,4 +133,5 @@ export interface ExpressionFunctionDefinitions { overall_metric: ExpressionFunctionOverallMetric; derivative: ExpressionFunctionDerivative; moving_average: ExpressionFunctionMovingAverage; + math_column: ExpressionFunctionMathColumn; } diff --git a/x-pack/plugins/lens/common/expressions/formula_context/context_fns.test.ts b/x-pack/plugins/lens/common/expressions/formula_context/context_fns.test.ts new file mode 100644 index 0000000000000..f064238992b07 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/formula_context/context_fns.test.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExecutionContext } from '@kbn/expressions-plugin/common'; +import { Adapters } from '@kbn/inspector-plugin/common'; +import { formulaIntervalFn, formulaNowFn, formulaTimeRangeFn } from './context_fns'; + +describe('interval', () => { + it('should return 0 if no time range available', () => { + // (not sure if this case is actually possible) + const result = formulaIntervalFn.fn(undefined, { targetBars: 100 }, { + getSearchContext: () => ({ + /* no time range */ + }), + } as ExecutionContext); + expect(result).toEqual(0); + }); + + it('should return 0 if no targetBars is passed', () => { + const result = formulaIntervalFn.fn( + undefined, + { + /* no targetBars */ + }, + { + getSearchContext: () => ({ + timeRange: { + from: 'now-15m', + to: 'now', + }, + }), + } as ExecutionContext + ); + expect(result).toEqual(0); + }); + + it('should return a valid value > 0 if both timeRange and targetBars is passed', () => { + const result = formulaIntervalFn.fn(undefined, { targetBars: 100 }, { + getSearchContext: () => ({ + timeRange: { + from: 'now-15m', + to: 'now', + }, + }), + } as ExecutionContext); + expect(result).toEqual(10000); + }); +}); + +describe('time range', () => { + it('should return 0 if no time range is available', () => { + // (not sure if this case is actually possible) + const result = formulaTimeRangeFn.fn(undefined, {}, { + getSearchContext: () => ({ + /* no time range */ + }), + } as ExecutionContext); + expect(result).toEqual(0); + }); + + it('should return a valid value > 0 if time range is available', () => { + const result = formulaTimeRangeFn.fn(undefined, {}, { + getSearchContext: () => ({ + timeRange: { + from: 'now-15m', + to: 'now', + }, + now: 1000000, // important to provide this to make the result consistent + }), + } as ExecutionContext); + + expect(result).toBe(900000); + }); +}); + +describe('now', () => { + it('should return the now value when passed', () => { + const now = 123456789; + expect( + formulaNowFn.fn(undefined, {}, { + getSearchContext: () => ({ + now, + }), + } as ExecutionContext) + ).toEqual(now); + }); +}); diff --git a/x-pack/plugins/lens/common/expressions/formula_context/context_fns.ts b/x-pack/plugins/lens/common/expressions/formula_context/context_fns.ts new file mode 100644 index 0000000000000..2f77d1142f7d1 --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/formula_context/context_fns.ts @@ -0,0 +1,97 @@ +/* + * 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 { getAbsoluteTimeRange, calcAutoIntervalNear } from '@kbn/data-plugin/common'; +import type { TimeRange } from '@kbn/es-query'; +import type { ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; +import moment from 'moment'; +import { i18n } from '@kbn/i18n'; + +export type ExpressionFunctionFormulaTimeRange = ExpressionFunctionDefinition< + 'formula_time_range', + undefined, + object, + number +>; + +const getTimeRangeAsNumber = (timeRange: TimeRange | undefined, now: number | undefined) => { + if (!timeRange) return 0; + const absoluteTimeRange = getAbsoluteTimeRange( + timeRange, + now != null ? { forceNow: new Date(now) } : {} + ); + return timeRange ? moment(absoluteTimeRange.to).diff(moment(absoluteTimeRange.from)) : 0; +}; + +export const formulaTimeRangeFn: ExpressionFunctionFormulaTimeRange = { + name: 'formula_time_range', + + help: i18n.translate('xpack.lens.formula.timeRange.help', { + defaultMessage: 'The specified time range, in milliseconds (ms).', + }), + + args: {}, + + fn(_input, _args, { getSearchContext }) { + const { timeRange, now } = getSearchContext(); + return getTimeRangeAsNumber(timeRange, now); + }, +}; + +export type ExpressionFunctionFormulaInterval = ExpressionFunctionDefinition< + 'formula_interval', + undefined, + { + targetBars?: number; + }, + number +>; + +export const formulaIntervalFn: ExpressionFunctionFormulaInterval = { + name: 'formula_interval', + + help: i18n.translate('xpack.lens.formula.interval.help', { + defaultMessage: 'The specified minimum interval for the date histogram, in milliseconds (ms).', + }), + + args: { + targetBars: { + types: ['number'], + help: i18n.translate('xpack.lens.formula.interval.targetBars.help', { + defaultMessage: 'The target number of bars for the date histogram.', + }), + }, + }, + + fn(_input, args, { getSearchContext }) { + const { timeRange, now } = getSearchContext(); + return timeRange && args.targetBars + ? calcAutoIntervalNear(args.targetBars, getTimeRangeAsNumber(timeRange, now)).asMilliseconds() + : 0; + }, +}; + +export type ExpressionFunctionFormulaNow = ExpressionFunctionDefinition< + 'formula_now', + undefined, + object, + number +>; + +export const formulaNowFn: ExpressionFunctionFormulaNow = { + name: 'formula_now', + + help: i18n.translate('xpack.lens.formula.now.help', { + defaultMessage: 'The current now moment used in Kibana expressed in milliseconds (ms).', + }), + + args: {}, + + fn(_input, _args, { getSearchContext }) { + return getSearchContext().now ?? Date.now(); + }, +}; diff --git a/x-pack/plugins/lens/common/expressions/formula_context/index.ts b/x-pack/plugins/lens/common/expressions/formula_context/index.ts new file mode 100644 index 0000000000000..da8931779cfbe --- /dev/null +++ b/x-pack/plugins/lens/common/expressions/formula_context/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './context_fns'; diff --git a/x-pack/plugins/lens/common/expressions/index.ts b/x-pack/plugins/lens/common/expressions/index.ts index ccb6343334d62..c3ccaddac9fd3 100644 --- a/x-pack/plugins/lens/common/expressions/index.ts +++ b/x-pack/plugins/lens/common/expressions/index.ts @@ -11,3 +11,4 @@ export * from './format_column'; export * from './map_to_columns'; export * from './time_scale'; export * from './datatable'; +export * from './formula_context'; diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts index 407f458a11e3e..db38e18d3bd19 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.test.ts @@ -34,20 +34,6 @@ function createLayer( }; } -function createExpression(type: 'interval' | 'now' | 'time_range', value: number) { - return [ - { - type: 'function', - function: 'mathColumn', - arguments: { - id: ['col1'], - name: [`Constant: ${type}`], - expression: [String(value)], - }, - }, - ]; -} - describe('context variables', () => { describe('interval', () => { describe('getErrorMessages', () => { @@ -124,53 +110,6 @@ describe('context variables', () => { ).toBeUndefined(); }); }); - describe('toExpression', () => { - it('should return 0 if no dateRange is passed', () => { - expect( - intervalOperation.toExpression( - createLayer('interval'), - 'col1', - createMockedIndexPattern(), - { now: new Date(), targetBars: 100 } - ) - ).toEqual(expect.arrayContaining(createExpression('interval', 0))); - }); - - it('should return 0 if no targetBars is passed', () => { - expect( - intervalOperation.toExpression( - createLayer('interval'), - 'col1', - createMockedIndexPattern(), - { - dateRange: { - fromDate: new Date(2022, 0, 1).toISOString(), - toDate: new Date(2023, 0, 1).toISOString(), - }, - now: new Date(), - } - ) - ).toEqual(expect.arrayContaining(createExpression('interval', 0))); - }); - - it('should return a valid value > 0 if both dateRange and targetBars is passed', () => { - expect( - intervalOperation.toExpression( - createLayer('interval'), - 'col1', - createMockedIndexPattern(), - { - dateRange: { - fromDate: new Date(2022, 0, 1).toISOString(), - toDate: new Date(2023, 0, 1).toISOString(), - }, - now: new Date(), - targetBars: 100, - } - ) - ).toEqual(expect.arrayContaining(createExpression('interval', 86400000))); - }); - }); }); describe('time_range', () => { describe('getErrorMessages', () => { @@ -202,35 +141,6 @@ describe('context variables', () => { ).toEqual(expect.arrayContaining(['The current time range interval is not available'])); }); }); - - describe('toExpression', () => { - it('should return 0 if no dateRange is passed', () => { - expect( - timeRangeOperation.toExpression( - createLayer('time_range'), - 'col1', - createMockedIndexPattern(), - { now: new Date(), targetBars: 100 } - ) - ).toEqual(expect.arrayContaining(createExpression('time_range', 0))); - }); - - it('should return a valid value > 0 if dateRange is passed', () => { - expect( - timeRangeOperation.toExpression( - createLayer('time_range'), - 'col1', - createMockedIndexPattern(), - { - dateRange: { - fromDate: new Date(2022, 0, 1).toISOString(), - toDate: new Date(2023, 0, 1).toISOString(), - }, - } - ) - ).toEqual(expect.arrayContaining(createExpression('time_range', 31536000000))); - }); - }); }); describe('now', () => { describe('getErrorMessages', () => { @@ -240,16 +150,5 @@ describe('context variables', () => { ).toBeUndefined(); }); }); - - describe('toExpression', () => { - it('should return the now value when passed', () => { - const now = new Date(); - expect( - nowOperation.toExpression(createLayer('now'), 'col1', createMockedIndexPattern(), { - now, - }) - ).toEqual(expect.arrayContaining(createExpression('now', +now))); - }); - }); }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx index 4b7b5b94b493b..f5f28d94ad228 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/formula/context_variables.tsx @@ -6,9 +6,18 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment'; -import { calcAutoIntervalNear, UI_SETTINGS } from '@kbn/data-plugin/common'; +import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { partition } from 'lodash'; +import { + buildExpressionFunction, + buildExpression, + ExpressionFunctionDefinitions, +} from '@kbn/expressions-plugin/common'; +import { + ExpressionFunctionFormulaInterval, + ExpressionFunctionFormulaNow, + ExpressionFunctionFormulaTimeRange, +} from '../../../../../../common/expressions/formula_context/context_fns'; import type { DateHistogramIndexPatternColumn, FormBasedLayer, @@ -58,13 +67,9 @@ export interface TimeRangeIndexPatternColumn extends ReferenceBasedIndexPatternC operationType: 'time_range'; } -function getTimeRangeFromContext({ dateRange }: ContextValues) { - return dateRange ? moment(dateRange.toDate).diff(moment(dateRange.fromDate)) : 0; -} - function getTimeRangeErrorMessages( - layer: FormBasedLayer, - columnId: string, + _layer: FormBasedLayer, + _columnId: string, indexPattern: IndexPattern, dateRange?: DateRange | undefined ) { @@ -89,12 +94,11 @@ function getTimeRangeErrorMessages( export const timeRangeOperation = createContextValueBasedOperation({ type: 'time_range', label: 'Time range', - description: i18n.translate('xpack.lens.indexPattern.timeRange.documentation.markdown', { - defaultMessage: ` -The specified time range, in milliseconds (ms). - `, + description: i18n.translate('xpack.lens.formula.timeRange.help', { + defaultMessage: 'The specified time range, in milliseconds (ms).', }), - getContextValue: getTimeRangeFromContext, + getExpressionFunction: (_context: ContextValues) => + buildExpressionFunction('formula_time_range', {}), getErrorMessage: getTimeRangeErrorMessages, }); @@ -102,9 +106,6 @@ export interface NowIndexPatternColumn extends ReferenceBasedIndexPatternColumn operationType: 'now'; } -function getNowFromContext({ now }: ContextValues) { - return now == null ? Date.now() : +now; -} function getNowErrorMessage() { return undefined; } @@ -112,12 +113,11 @@ function getNowErrorMessage() { export const nowOperation = createContextValueBasedOperation({ type: 'now', label: 'Current now', - description: i18n.translate('xpack.lens.indexPattern.now.documentation.markdown', { - defaultMessage: ` - The current now moment used in Kibana expressed in milliseconds (ms). - `, + description: i18n.translate('xpack.lens.formula.now.help', { + defaultMessage: 'The current now moment used in Kibana expressed in milliseconds (ms).', }), - getContextValue: getNowFromContext, + getExpressionFunction: (_context: ContextValues) => + buildExpressionFunction('formula_now', {}), getErrorMessage: getNowErrorMessage, }); @@ -125,12 +125,6 @@ export interface IntervalIndexPatternColumn extends ReferenceBasedIndexPatternCo operationType: 'interval'; } -function getIntervalFromContext(context: ContextValues) { - return context.dateRange && context.targetBars - ? calcAutoIntervalNear(context.targetBars, getTimeRangeFromContext(context)).asMilliseconds() - : 0; -} - function getIntervalErrorMessages( layer: FormBasedLayer, columnId: string, @@ -174,12 +168,13 @@ function getIntervalErrorMessages( export const intervalOperation = createContextValueBasedOperation({ type: 'interval', label: 'Date histogram interval', - description: i18n.translate('xpack.lens.indexPattern.interval.documentation.markdown', { - defaultMessage: ` -The specified minimum interval for the date histogram, in milliseconds (ms). - `, + description: i18n.translate('xpack.lens.formula.interval.help', { + defaultMessage: 'The specified minimum interval for the date histogram, in milliseconds (ms).', }), - getContextValue: getIntervalFromContext, + getExpressionFunction: ({ targetBars }: ContextValues) => + buildExpressionFunction('formula_interval', { + targetBars, + }), getErrorMessage: getIntervalErrorMessages, }); @@ -191,14 +186,14 @@ export type ConstantsIndexPatternColumn = function createContextValueBasedOperation({ label, type, - getContextValue, + getExpressionFunction, getErrorMessage, description, }: { label: string; type: ColumnType['operationType']; description: string; - getContextValue: (context: ContextValues) => number; + getExpressionFunction: (context: ContextValues) => ReturnType; getErrorMessage: OperationDefinition['getErrorMessage']; }): OperationDefinition { return { @@ -233,15 +228,11 @@ function createContextValueBasedOperation { const column = layer.columns[columnId] as ColumnType; return [ - { - type: 'function', - function: 'mathColumn', - arguments: { - id: [columnId], - name: [column.label], - expression: [String(getContextValue(context))], - }, - }, + buildExpressionFunction('mathColumn', { + id: columnId, + name: column.label, + expression: buildExpression([getExpressionFunction(context)]), + }).toAst(), ]; }, createCopy(layers, source, target) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index b0729cb489ba7..04d69c1afc571 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -484,6 +484,7 @@ describe('ConfigPanel', () => { }, dateRange: expect.anything(), filters: [], + now: expect.anything(), query: undefined, }, groupId: 'a', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 73a4ef853390d..bc3b71c08487a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -24,7 +24,6 @@ import { } from '@elastic/eui'; import type { CoreStart } from '@kbn/core/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { ExecutionContextSearch } from '@kbn/es-query'; import type { ExpressionRendererEvent, ExpressionRenderError, @@ -66,7 +65,6 @@ import { editVisualizationAction, setSaveable, useLensSelector, - selectExecutionContext, selectIsFullscreenDatasource, selectVisualization, selectDatasourceStates, @@ -80,6 +78,7 @@ import { VisualizationState, DatasourceStates, DataViewsState, + selectExecutionContextSearch, } from '../../../state_management'; import type { LensInspector } from '../../../lens_inspector_service'; import { inferTimeField, DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS } from '../../../utils'; @@ -712,7 +711,7 @@ export const VisualizationWrapper = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const context = useLensSelector(selectExecutionContext); + const searchContext = useLensSelector(selectExecutionContextSearch); // Used for reporting const { isRenderComplete, hasDynamicError, setIsRenderComplete, setDynamicError, nodeRef } = useReportingState(errors); @@ -722,18 +721,6 @@ export const VisualizationWrapper = ({ onRender$(); }, [setIsRenderComplete, onRender$]); - const searchContext: ExecutionContextSearch = useMemo( - () => ({ - query: context.query, - timeRange: { - from: context.dateRange.fromDate, - to: context.dateRange.toDate, - }, - filters: context.filters, - disableWarningToasts: true, - }), - [context] - ); const searchSessionId = useLensSelector(selectSearchSessionId); if (errors.length) { diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 9851a10c8641c..e5d34db62ba75 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -1253,6 +1253,7 @@ export class Embeddable const input = this.getInput(); const context: ExecutionContextSearch = { + now: this.deps.data.nowProvider.get().getTime(), timeRange: input.timeslice !== undefined ? { diff --git a/x-pack/plugins/lens/public/expressions.ts b/x-pack/plugins/lens/public/expressions.ts index ef12b43bec71d..32856175db1b0 100644 --- a/x-pack/plugins/lens/public/expressions.ts +++ b/x-pack/plugins/lens/public/expressions.ts @@ -14,6 +14,11 @@ import { formatColumn } from '../common/expressions/format_column'; import { counterRate } from '../common/expressions/counter_rate'; import { getTimeScale } from '../common/expressions/time_scale/time_scale'; import { collapse } from '../common/expressions/collapse'; +import { + formulaIntervalFn, + formulaNowFn, + formulaTimeRangeFn, +} from '../common/expressions/formula_context'; type TimeScaleArguments = Parameters; @@ -25,6 +30,9 @@ export const setupExpressions = ( getForceNow: TimeScaleArguments[2] ) => { [ + formulaTimeRangeFn, + formulaNowFn, + formulaIntervalFn, collapse, counterRate, formatColumn, diff --git a/x-pack/plugins/lens/public/state_management/selectors.ts b/x-pack/plugins/lens/public/state_management/selectors.ts index 7572c31287297..44121c4d064c7 100644 --- a/x-pack/plugins/lens/public/state_management/selectors.ts +++ b/x-pack/plugins/lens/public/state_management/selectors.ts @@ -46,9 +46,11 @@ export const selectTriggerApplyChanges = (state: LensState) => { return shouldApply; }; +// TODO - is there any point to keeping this around since we have selectExecutionSearchContext? export const selectExecutionContext = createSelector( [selectQuery, selectFilters, selectResolvedDateRange], (query, filters, dateRange) => ({ + now: Date.now(), dateRange, query, filters, @@ -56,6 +58,7 @@ export const selectExecutionContext = createSelector( ); export const selectExecutionContextSearch = createSelector(selectExecutionContext, (res) => ({ + now: res.now, query: res.query, timeRange: { from: res.dateRange.fromDate, diff --git a/x-pack/plugins/lens/server/expressions/expressions.ts b/x-pack/plugins/lens/server/expressions/expressions.ts index 1e80fc5bb49a3..b5e8fc2851608 100644 --- a/x-pack/plugins/lens/server/expressions/expressions.ts +++ b/x-pack/plugins/lens/server/expressions/expressions.ts @@ -13,6 +13,9 @@ import { mapToColumns, getTimeScale, getDatatable, + formulaIntervalFn, + formulaNowFn, + formulaTimeRangeFn, } from '../../common/expressions'; import { getDatatableUtilitiesFactory, getFormatFactory, getTimeZoneFactory } from './utils'; @@ -23,6 +26,9 @@ export const setupExpressions = ( expressions: ExpressionsServerSetup ) => { [ + formulaNowFn, + formulaIntervalFn, + formulaTimeRangeFn, counterRate, formatColumn, mapToColumns, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index b6816376aa061..6acdf51501610 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -21980,11 +21980,9 @@ "xpack.lens.indexPattern.counterRate.documentation.markdown": "\nCalcule le taux d'un compteur toujours croissant. Cette fonction renvoie uniquement des résultats utiles inhérents aux champs d'indicateurs de compteur qui contiennent une mesure quelconque à croissance régulière.\nSi la valeur diminue, elle est interprétée comme une mesure de réinitialisation de compteur. Pour obtenir des résultats plus précis, \"counter_rate\" doit être calculé d’après la valeur \"max\" du champ.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\nIl utilise l'intervalle en cours utilisé dans la formule.\n\nExemple : visualiser le taux d'octets reçus au fil du temps par un serveur Memcached :\n`counter_rate(max(memcached.stats.read.bytes))`\n ", "xpack.lens.indexPattern.cumulativeSum.documentation.markdown": "\nCalcule la somme cumulée d'un indicateur au fil du temps, en ajoutant toutes les valeurs précédentes d'une série à chaque valeur. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\n\nExemple : visualiser les octets reçus cumulés au fil du temps :\n`cumulative_sum(sum(bytes))`\n ", "xpack.lens.indexPattern.differences.documentation.markdown": "\nCalcule la différence par rapport à la dernière valeur d'un indicateur au fil du temps. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates.\nLes données doivent être séquentielles pour les différences. Si vos données sont vides lorsque vous utilisez des différences, essayez d'augmenter l'intervalle de l'histogramme de dates.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\n\nExemple : visualiser la modification des octets reçus au fil du temps :\n`differences(sum(bytes))`\n ", - "xpack.lens.indexPattern.interval.documentation.markdown": "\nL’intervalle minimum spécifié pour l’histogramme de date, en millisecondes (ms).\n ", "xpack.lens.indexPattern.lastValue.documentation.markdown": "\nRenvoie la valeur d'un champ du dernier document, triée par le champ d'heure par défaut de la vue de données.\n\nCette fonction permet de récupérer le dernier état d'une entité.\n\nExemple : obtenir le statut actuel du serveur A :\n`last_value(server.status, kql='server.name=\"A\"')`\n ", "xpack.lens.indexPattern.metric.documentation.markdown": "\nRenvoie l'indicateur {metric} d'un champ. Cette fonction fonctionne uniquement pour les champs numériques.\n\nExemple : obtenir l'indicateur {metric} d'un prix :\n\"{metric}(price)\"\n\nExemple : obtenir l'indicateur {metric} d'un prix pour des commandes du Royaume-Uni :\n\"{metric}(price, kql='location:UK')\"\n ", "xpack.lens.indexPattern.movingAverage.documentation.markdown": "\nCalcule la moyenne mobile d'un indicateur au fil du temps, en prenant la moyenne des n dernières valeurs pour calculer la valeur actuelle. Pour utiliser cette fonction, vous devez également configurer une dimension de l'histogramme de dates.\nLa valeur de fenêtre par défaut est {defaultValue}.\n\nCe calcul est réalisé séparément pour des séries distinctes définies par des filtres ou des dimensions de valeurs supérieures.\n\nPrend un paramètre nommé \"window\" qui spécifie le nombre de dernières valeurs à inclure dans le calcul de la moyenne de la valeur actuelle.\n\nExemple : lisser une ligne de mesures :\n`moving_average(sum(bytes), window=5)`\n ", - "xpack.lens.indexPattern.now.documentation.markdown": "\n La durée actuelle passée dans Kibana exprimée en millisecondes (ms).\n ", "xpack.lens.indexPattern.overall_average.documentation.markdown": "\nCalcule la moyenne d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle.\nD'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes.\n\nSi le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, \"overall_average\" calcule la moyenne pour toutes les dimensions, quelle que soit la fonction utilisée.\n\nExemple : écart par rapport à la moyenne :\n\"sum(bytes) - overall_average(sum(bytes))\"\n ", "xpack.lens.indexPattern.overall_max.documentation.markdown": "\nCalcule la valeur maximale d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle.\nD'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes.\n\nSi le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, \"overall_max\" calcule la valeur maximale pour toutes les dimensions, quelle que soit la fonction utilisée.\n\nExemple : pourcentage de plage\n\"(sum(bytes) - overall_min(sum(bytes))) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))\"\n ", "xpack.lens.indexPattern.overall_min.documentation.markdown": "\nCalcule la valeur minimale d'un indicateur pour tous les points de données d'une série dans le graphique actuel. Une série est définie par une dimension à l'aide d'un histogramme de dates ou d'une fonction d'intervalle.\nD'autres dimensions permettant de répartir les données telles que les valeurs supérieures ou les filtres sont traitées en tant que séries distinctes.\n\nSi le graphique actuel n'utilise aucun histogramme de dates ou aucune fonction d'intervalle, \"overall_min\" calcule la valeur minimale pour toutes les dimensions, quelle que soit la fonction utilisée.\n\nExemple : pourcentage de plage\n\"(sum(bytes) - overall_min(sum(bytes)) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))\"\n ", @@ -21993,7 +21991,6 @@ "xpack.lens.indexPattern.percentileRanks.documentation.markdown": "\nRetourne le pourcentage de valeurs qui sont en dessous d'une certaine valeur. Par exemple, si une valeur est supérieure à 95 % des valeurs observées, elle est placée au 95e rang centile.\n\nExemple : Obtenir le pourcentage de valeurs qui sont en dessous de 100 :\n\"percentile_rank(bytes, value=100)\"\n ", "xpack.lens.indexPattern.standardDeviation.documentation.markdown": "\nRetourne la taille de la variation ou de la dispersion du champ. Cette fonction ne s’applique qu’aux champs numériques.\n\n#### Exemples\n\nPour obtenir l'écart type d'un prix, utilisez standard_deviation(price).\n\nPour obtenir la variance du prix des commandes passées au Royaume-Uni, utilisez `square(standard_deviation(price, kql='location:UK'))`.\n ", "xpack.lens.indexPattern.time_scale.documentation.markdown": "\n\nCette fonction avancée est utile pour normaliser les comptes et les sommes sur un intervalle de temps spécifique. Elle permet l'intégration avec les indicateurs qui sont stockés déjà normalisés sur un intervalle de temps spécifique.\n\nVous pouvez faire appel à cette fonction uniquement si une fonction d'histogramme des dates est utilisée dans le graphique actuel.\n\nExemple : Un rapport comparant un indicateur déjà normalisé à un autre indicateur devant être normalisé.\n\"normalize_by_unit(counter_rate(max(system.diskio.write.bytes)), unit='s') / last_value(apache.status.bytes_per_second)\"\n ", - "xpack.lens.indexPattern.timeRange.documentation.markdown": "\nL'intervalle de temps spécifié, en millisecondes (ms).\n ", "xpack.lens.AggBasedLabel": "visualisation basée sur l'agrégation", "xpack.lens.app.addToLibrary": "Enregistrer dans la bibliothèque", "xpack.lens.app.cancel": "Annuler", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9ae21a4439e8a..0ed5b8922a49f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21995,11 +21995,9 @@ "xpack.lens.indexPattern.counterRate.documentation.markdown": "\n増加し続けるカウンターのレートを計算します。この関数は、経時的に単調に増加する種類の測定を含むカウンターメトリックフィールドでのみ結果を生成します。\n値が小さくなる場合は、カウンターリセットであると解釈されます。最も正確な結果を得るには、フィールドの「max`」で「counter_rate」を計算してください。\n\nこの計算はフィルターで定義された別の系列または上位値のディメンションに対して個別に実行されます。\n式で使用されるときには、現在の間隔を使用します。\n\n例:Memcachedサーバーで経時的に受信されたバイトの比率を可視化します。\n`counter_rate(max(memcached.stats.read.bytes))`\n ", "xpack.lens.indexPattern.cumulativeSum.documentation.markdown": "\n経時的なメトリックの累計値を計算し、系列のすべての前の値を各値に追加します。この関数を使用するには、日付ヒストグラムディメンションも構成する必要があります。\n\nこの計算はフィルターで定義された別の系列または上位値のディメンションに対して個別に実行されます。\n\n例:経時的に累積された受信バイト数を可視化します。\n`cumulative_sum(sum(bytes))`\n ", "xpack.lens.indexPattern.differences.documentation.markdown": "\n経時的にメトリックの最後の値に対する差異を計算します。この関数を使用するには、日付ヒストグラムディメンションも構成する必要があります。\n差異ではデータが連続する必要があります。差異を使用するときにデータが空の場合は、データヒストグラム間隔を大きくしてみてください。\n\nこの計算はフィルターで定義された別の系列または上位値のディメンションに対して個別に実行されます。\n\n例:経時的に受信したバイト数の変化を可視化します。\n`differences(sum(bytes))`\n ", - "xpack.lens.indexPattern.interval.documentation.markdown": "\nミリ秒(ms)で指定した、日付ヒストグラムの最小間隔。\n ", "xpack.lens.indexPattern.lastValue.documentation.markdown": "\n最後のドキュメントからフィールドの値を返し、データビューのデフォルト時刻フィールドで並べ替えます。\n\nこの関数はエンティティの最新の状態を取得する際に役立ちます。\n\n例:サーバーAの現在のステータスを取得:\n`last_value(server.status, kql='server.name=\"A\"')`\n ", "xpack.lens.indexPattern.metric.documentation.markdown": "\nフィールドの{metric}を返します。この関数は数値フィールドでのみ動作します。\n\n例:価格の{metric}を取得:\n`{metric}(price)`\n\n例:英国からの注文の価格の{metric}を取得:\n`{metric}(price, kql='location:UK')`\n ", "xpack.lens.indexPattern.movingAverage.documentation.markdown": "\n経時的なメトリックの移動平均を計算します。最後のn番目の値を平均化し、現在の値を計算します。この関数を使用するには、日付ヒストグラムディメンションも構成する必要があります。\nデフォルトウィンドウ値は{defaultValue}です\n\nこの計算はフィルターで定義された別の系列または上位値のディメンションに対して個別に実行されます。\n\n指名パラメーター「window」を取ります。これは現在値の平均計算に含める最後の値の数を指定します。\n\n例:測定の線を平滑化:\n`moving_average(sum(bytes), window=5)`\n ", - "xpack.lens.indexPattern.now.documentation.markdown": "\n ミリ秒(ms)で表された、Kibanaで使用される現在の日時。\n ", "xpack.lens.indexPattern.overall_average.documentation.markdown": "\n現在のグラフの系列のすべてのデータポイントのメトリックの平均を計算します。系列は日付ヒストグラムまたは間隔関数を使用してディメンションによって定義されます。\n上位の値やフィルターなどのデータを分解する他のディメンションは別の系列として処理されます。\n\n日付ヒストグラムまたは間隔関数が現在のグラフで使用されている場合、使用されている関数に関係なく、「overall_average」はすべてのディメンションで平均値を計算します。\n\n例:平均からの収束:\n`sum(bytes) - overall_average(sum(bytes))`\n ", "xpack.lens.indexPattern.overall_max.documentation.markdown": "\n現在のグラフの系列のすべてのデータポイントのメトリックの最大値を計算します。系列は日付ヒストグラムまたは間隔関数を使用してディメンションによって定義されます。\n上位の値やフィルターなどのデータを分解する他のディメンションは別の系列として処理されます。\n\n日付ヒストグラムまたは間隔関数が現在のグラフで使用されている場合、使用されている関数に関係なく、「overall_max」はすべてのディメンションで最大値を計算します。\n\n例:範囲の割合\n`(sum(bytes) - overall_min(sum(bytes))) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`\n ", "xpack.lens.indexPattern.overall_min.documentation.markdown": "\n現在のグラフの系列のすべてのデータポイントのメトリックの最小値を計算します。系列は日付ヒストグラムまたは間隔関数を使用してディメンションによって定義されます。\n上位の値やフィルターなどのデータを分解する他のディメンションは別の系列として処理されます。\n\n日付ヒストグラムまたは間隔関数が現在のグラフで使用されている場合、使用されている関数に関係なく、「overall_min」はすべてのディメンションで最小値を計算します。\n\n例:範囲の割合\n`(sum(bytes) - overall_min(sum(bytes)) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`\n ", @@ -22008,7 +22006,6 @@ "xpack.lens.indexPattern.percentileRanks.documentation.markdown": "\n特定の値未満の値の割合が返されます。たとえば、値が観察された値の95%以上の場合、95パーセンタイルランクであるとされます。\n\n例:100未満の値のパーセンタイルを取得します。\n`percentile_rank(bytes, value=100)`\n ", "xpack.lens.indexPattern.standardDeviation.documentation.markdown": "\nフィールドの分散または散布度が返されます。この関数は数値フィールドでのみ動作します。\n\n#### 例\n\n価格の標準偏差を取得するには、standard_deviation(price)を使用します。\n\n英国からの注文書の価格の分散を取得するには、square(standard_deviation(price, kql='location:UK'))を使用します。\n ", "xpack.lens.indexPattern.time_scale.documentation.markdown": "\n\nこの高度な機能は、特定の期間に対してカウントと合計を正規化する際に役立ちます。すでに特定の期間に対して正規化され、保存されたメトリックとの統合が可能です。\n\nこの機能は、現在のグラフで日付ヒストグラム関数が使用されている場合にのみ使用できます。\n\n例:すでに正規化されているメトリックを、正規化が必要な別のメトリックと比較した比率。\n`normalize_by_unit(counter_rate(max(system.diskio.write.bytes)), unit='s') / last_value(apache.status.bytes_per_second)`\n ", - "xpack.lens.indexPattern.timeRange.documentation.markdown": "\nミリ秒(ms)で指定された時間範囲。\n ", "xpack.lens.AggBasedLabel": "集約に基づく可視化", "xpack.lens.app.addToLibrary": "ライブラリに保存", "xpack.lens.app.cancel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fcfce0f121c37..2def05af32b75 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21994,11 +21994,9 @@ "xpack.lens.indexPattern.counterRate.documentation.markdown": "\n计算不断增大的计数器的速率。此函数将仅基于计数器指标字段生成有帮助的结果,包括随着时间的推移度量某种单调递增。\n如果值确实变小,则其将此解析为计数器重置。要获取很精确的结果,应基于字段的 `max`计算 `counter_rate`。\n\n筛选或排名最前值维度定义的不同序列将分别执行此计算。\n用于公式中时,其使用当前时间间隔。\n\n例如:可视化随着时间的推移 Memcached 服务器接收的字节速率:\n`counter_rate(max(memcached.stats.read.bytes))`\n ", "xpack.lens.indexPattern.cumulativeSum.documentation.markdown": "\n计算随着时间的推移指标的累计和,即序列的所有以前值相加得出每个值。要使用此函数,您还需要配置 Date Histogram 维度。\n\n筛选或排名最前值维度定义的不同序列将分别执行此计算。\n\n例如:可视化随着时间的推移累计接收的字节:\n`cumulative_sum(sum(bytes))`\n ", "xpack.lens.indexPattern.differences.documentation.markdown": "\n计算随着时间的推移与指标最后一个值的差异。要使用此函数,您还需要配置 Date Histogram 维度。\n差异需要数据是顺序的。如果使用差异时数据为空,请尝试增加 Date Histogram 时间间隔。\n\n筛选或排名最前值维度定义的不同序列将分别执行此计算。\n\n例如:可视化随着时间的推移接收的字节的变化:\n`differences(sum(bytes))`\n ", - "xpack.lens.indexPattern.interval.documentation.markdown": "\nDate Histogram 的指定最小时间间隔,单位为毫秒 (ms)。\n ", "xpack.lens.indexPattern.lastValue.documentation.markdown": "\n返回最后一个文档的字段值,按数据视图的默认时间字段排序。\n\n此函数用于检索实体的最新状态。\n\n例如:获取服务器 A 的当前状态:\n`last_value(server.status, kql='server.name=\"A\"')`\n ", "xpack.lens.indexPattern.metric.documentation.markdown": "\n返回字段的 {metric}。此函数仅适用于数字字段。\n\n例如:获取价格的 {metric}:\n`{metric}(price)`\n\n例如:获取英国订单价格的 {metric}:\n`{metric}(price, kql='location:UK')`\n ", "xpack.lens.indexPattern.movingAverage.documentation.markdown": "\n计算随着时间的推移指标的移动平均值,即计算最后 n 个值的平均值以得出当前值。要使用此函数,您还需要配置 Date Histogram 维度。\n默认窗口值为 {defaultValue}。\n\n筛选或排名最前值维度定义的不同序列将分别执行此计算。\n\n取已命名参数 `window`,其指定当前值的平均计算中要包括过去多少个值。\n\n例如:平滑度量线:\n`moving_average(sum(bytes), window=5)`\n ", - "xpack.lens.indexPattern.now.documentation.markdown": "\n Kibana 中使用的当前时刻,用毫秒 (ms) 表示。\n ", "xpack.lens.indexPattern.overall_average.documentation.markdown": "\n为当前图表中序列的所有数据点计算指标的平均值。序列由维度使用 Date Histogram 或时间间隔函数定义。\n分解数据的其他维度,如排名最前值或筛选,将被视为不同的序列。\n\n如果当前图表未使用 Date Histogram 或时间间隔函数,则无论使用什么函数,`overall_average` 都将计算所有维度的平均值\n\n例如:与平均值的偏离:\n`sum(bytes) - overall_average(sum(bytes))`\n ", "xpack.lens.indexPattern.overall_max.documentation.markdown": "\n为当前图表中序列的所有数据点计算指标的最大值。序列由维度使用 Date Histogram 或时间间隔函数定义。\n分解数据的其他维度,如排名最前值或筛选,将被视为不同的序列。\n\n如果当前图表未使用 Date Histogram 或内部函数,则无论使用什么函数,`overall_max` 都将计算所有维度的最大值\n\n例如:范围的百分比\n`(sum(bytes) - overall_min(sum(bytes))) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`\n ", "xpack.lens.indexPattern.overall_min.documentation.markdown": "\n为当前图表中序列的所有数据点计算指标的最小值。序列由维度使用 Date Histogram 或时间间隔函数定义。\n分解数据的其他维度,如排名最前值或筛选,将被视为不同的序列。\n\n如果当前图表未使用 Date Histogram 或时间间隔函数,则无论使用什么函数,`overall_min` 都将计算所有维度的最小值\n\n例如:范围的百分比\n`(sum(bytes) - overall_min(sum(bytes)) / (overall_max(sum(bytes)) - overall_min(sum(bytes)))`\n ", @@ -22007,7 +22005,6 @@ "xpack.lens.indexPattern.percentileRanks.documentation.markdown": "\n返回小于某个值的值的百分比。例如,如果某个值大于或等于 95% 的观察值,则称它处于第 95 个百分位等级\n\n例如:获取小于 100 的值的百分比:\n`percentile_rank(bytes, value=100)`\n ", "xpack.lens.indexPattern.standardDeviation.documentation.markdown": "\n返回字段的变量或差量数量。此函数仅适用于数字字段。\n\n#### 示例\n\n要获取价格的标准偏差,请使用 `standard_deviation(price)`。\n\n要获取来自英国的订单的价格方差,请使用 `square(standard_deviation(price, kql='location:UK'))`。\n ", "xpack.lens.indexPattern.time_scale.documentation.markdown": "\n\n此高级函数用于将计数和总和标准化为特定时间间隔。它允许集成所存储的已标准化为特定时间间隔的指标。\n\n此函数只能在当前图表中使用了日期直方图函数时使用。\n\n例如:将已标准化指标与其他需要标准化的指标进行比较的比率。\n`normalize_by_unit(counter_rate(max(system.diskio.write.bytes)), unit='s') / last_value(apache.status.bytes_per_second)`\n ", - "xpack.lens.indexPattern.timeRange.documentation.markdown": "\n指定的时间范围,单位为毫秒 (ms)。\n ", "xpack.lens.AggBasedLabel": "基于聚合的可视化", "xpack.lens.app.addToLibrary": "保存到库", "xpack.lens.app.cancel": "取消", From 4fc4dfbbda8771e5893c18160ee4238cd7b5f8db Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Tue, 19 Dec 2023 08:45:52 -0700 Subject: [PATCH 08/95] [ML] Trained models: Adds workflow for creating ingest pipeline for a trained model (#170902) ## Summary Related issue: https://github.com/elastic/kibana/issues/168988 This PR adds the ability to create an ingest pipeline using a trained model for inference. From within 'Test model' flyout - adds a `Create pipeline` button that opens another 'Create pipeline' flyout (similar to the DFA models deploy model flyout). This flyout utilizes the configuration utilized for testing the model. image image image image image image ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../add_inference_pipeline_flyout.tsx | 11 +- .../components/pipeline_details.tsx | 145 +------------ .../components/reindex_with_pipeline.tsx | 13 +- .../components/ml_inference/get_steps.ts | 11 +- .../components/ml_inference/validation.ts | 44 +++- .../add_inference_pipeline_footer.tsx | 14 +- ...dd_inference_pipeline_horizontal_steps.tsx | 111 ++++++---- .../application/components/shared/index.ts | 13 ++ .../on_failure_configuration.tsx | 12 +- .../shared/pipeline_details_title.tsx | 73 +++++++ .../shared/pipeline_name_and_description.tsx | 115 +++++++++++ .../review_and_create_pipeline.tsx | 48 +++-- .../create_pipeline_for_model_flyout.tsx | 193 ++++++++++++++++++ ...ference_properties_from_pipeline_config.ts | 118 +++++++++++ .../get_pipeline_config.ts | 37 ++++ .../pipeline_details.tsx | 183 +++++++++++++++++ .../create_pipeline_for_model/state.ts | 42 ++++ .../test_trained_model.tsx | 79 +++++++ .../model_management/models_list.tsx | 12 +- .../model_management/test_models/index.ts | 2 +- .../test_models/models/index_input.tsx | 82 ++++++-- .../test_models/models/inference_base.ts | 18 +- .../inference_input_form/index_input.tsx | 37 ++-- .../input_form_controls.tsx | 63 ++++++ .../inference_input_form/text_input.tsx | 21 +- .../test_models/selected_model.tsx | 157 ++++++++++++-- .../test_models/test_flyout.tsx | 136 +++--------- ...est_model_and_pipeline_creation_flyout.tsx | 37 ++++ .../test_trained_model_content.tsx | 115 +++++++++++ .../test_trained_models_context.tsx | 32 +++ .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 35 files changed, 1592 insertions(+), 387 deletions(-) rename x-pack/plugins/ml/public/application/components/{ml_inference/components => shared}/add_inference_pipeline_footer.tsx (88%) rename x-pack/plugins/ml/public/application/components/{ml_inference/components => shared}/add_inference_pipeline_horizontal_steps.tsx (58%) create mode 100644 x-pack/plugins/ml/public/application/components/shared/index.ts rename x-pack/plugins/ml/public/application/components/{ml_inference/components => shared}/on_failure_configuration.tsx (96%) create mode 100644 x-pack/plugins/ml/public/application/components/shared/pipeline_details_title.tsx create mode 100644 x-pack/plugins/ml/public/application/components/shared/pipeline_name_and_description.tsx rename x-pack/plugins/ml/public/application/components/{ml_inference/components => shared}/review_and_create_pipeline.tsx (86%) create mode 100644 x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/create_pipeline_for_model_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_inference_properties_from_pipeline_config.ts create mode 100644 x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_pipeline_config.ts create mode 100644 x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/pipeline_details.tsx create mode 100644 x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/state.ts create mode 100644 x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/test_trained_model.tsx create mode 100644 x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/input_form_controls.tsx create mode 100644 x-pack/plugins/ml/public/application/model_management/test_models/test_model_and_pipeline_creation_flyout.tsx create mode 100644 x-pack/plugins/ml/public/application/model_management/test_models/test_trained_model_content.tsx create mode 100644 x-pack/plugins/ml/public/application/model_management/test_models/test_trained_models_context.tsx diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 57ae1cc9f6e4a..a259d76c6affb 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -671,6 +671,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D cronExpressions: `${ELASTICSEARCH_DOCS}cron-expressions.html`, executeWatchActionModes: `${ELASTICSEARCH_DOCS}watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`, indexExists: `${ELASTICSEARCH_DOCS}indices-exists.html`, + inferTrainedModel: `${ELASTICSEARCH_DOCS}infer-trained-model.html`, multiSearch: `${ELASTICSEARCH_DOCS}search-multi-search.html`, openIndex: `${ELASTICSEARCH_DOCS}indices-open-close.html`, putComponentTemplate: `${ELASTICSEARCH_DOCS}indices-component-template.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 3b6d22a190244..910c0c218dcc5 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -391,6 +391,7 @@ export interface DocLinks { cronExpressions: string; executeWatchActionModes: string; indexExists: string; + inferTrainedModel: string; multiSearch: string; openIndex: string; putComponentTemplate: string; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx index c1e612e3b08d1..e1b4cc2710257 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx @@ -22,14 +22,14 @@ import { extractErrorProperties } from '@kbn/ml-error-utils'; import { ModelItem } from '../../model_management/models_list'; import type { AddInferencePipelineSteps } from './types'; import { ADD_INFERENCE_PIPELINE_STEPS } from './constants'; -import { AddInferencePipelineFooter } from './components/add_inference_pipeline_footer'; -import { AddInferencePipelineHorizontalSteps } from './components/add_inference_pipeline_horizontal_steps'; +import { AddInferencePipelineFooter } from '../shared'; +import { AddInferencePipelineHorizontalSteps } from '../shared'; import { getInitialState, getModelType } from './state'; import { PipelineDetails } from './components/pipeline_details'; import { ProcessorConfiguration } from './components/processor_configuration'; -import { OnFailureConfiguration } from './components/on_failure_configuration'; +import { OnFailureConfiguration } from '../shared'; import { TestPipeline } from './components/test_pipeline'; -import { ReviewAndCreatePipeline } from './components/review_and_create_pipeline'; +import { ReviewAndCreatePipeline } from '../shared'; import { useMlApiContext } from '../../contexts/kibana'; import { getPipelineConfig } from './get_pipeline_config'; import { validateInferencePipelineConfigurationStep } from './validation'; @@ -122,6 +122,8 @@ export const AddInferencePipelineFlyout: FC = ( setStep={setStep} isDetailsStepValid={pipelineNameError === undefined && targetFieldError === undefined} isConfigureProcessorStepValid={hasUnsavedChanges === false} + hasProcessorStep + pipelineCreated={formState.pipelineCreated} /> {step === ADD_INFERENCE_PIPELINE_STEPS.DETAILS && ( @@ -184,6 +186,7 @@ export const AddInferencePipelineFlyout: FC = ( isConfigureProcessorStepValid={hasUnsavedChanges === false} pipelineCreated={formState.pipelineCreated} creatingPipeline={formState.creatingPipeline} + hasProcessorStep /> diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/pipeline_details.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/pipeline_details.tsx index bd36ee4bf6c49..034c6ed5468e6 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/pipeline_details.tsx +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/pipeline_details.tsx @@ -14,18 +14,14 @@ import { EuiFlexItem, EuiForm, EuiFormRow, - EuiLink, - EuiSpacer, - EuiTitle, - EuiText, - EuiTextArea, EuiPanel, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useMlKibana } from '../../../contexts/kibana'; import type { MlInferenceState } from '../types'; +import { PipelineDetailsTitle } from '../../shared'; +import { PipelineNameAndDescription } from '../../shared'; interface Props { handlePipelineConfigUpdate: (configUpdate: Partial) => void; @@ -47,12 +43,6 @@ export const PipelineDetails: FC = memo( targetField, targetFieldError, }) => { - const { - services: { - docLinks: { links }, - }, - } = useMlKibana(); - const handleConfigChange = (value: string, type: string) => { handlePipelineConfigUpdate({ [type]: value }); }; @@ -60,133 +50,18 @@ export const PipelineDetails: FC = memo( return ( - -

- {i18n.translate( - 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.title', - { defaultMessage: 'Create a pipeline' } - )} -

-
- - -

- {modelId}, - pipeline: ( - - pipeline - - ), - }} - /> -

-

- - _reindex API - - ), - pipelineSimulateLink: ( - - pipeline/_simulate - - ), - }} - /> -

-
+
- {/* NAME */} - - {i18n.translate( - 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText', - { - defaultMessage: - 'Pipeline names are unique within a deployment and can only contain letters, numbers, underscores, and hyphens.', - } - )} - - ) - } - error={pipelineNameError} - isInvalid={pipelineNameError !== undefined} - > - ) => - handleConfigChange(e.target.value, 'pipelineName') - } - /> - - {/* DESCRIPTION */} - - {i18n.translate( - 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.description.helpText', - { - defaultMessage: 'A description of what this pipeline does.', - } - )} - - } - > - ) => - handleConfigChange(e.target.value, 'pipelineDescription') - } - /> - + {/* NAME and DESCRIPTION */} + {/* TARGET FIELD */} = ({ pipelineName, sourceIndex }) => { - const [selectedIndex, setSelectedIndex] = useState([ - { label: sourceIndex }, - ]); + const [selectedIndex, setSelectedIndex] = useState( + sourceIndex ? [{ label: sourceIndex }] : [] + ); const [options, setOptions] = useState([]); const [destinationIndex, setDestinationIndex] = useState(''); const [destinationIndexExists, setDestinationIndexExists] = useState(false); @@ -205,7 +205,7 @@ export const ReindexWithPipeline: FC = ({ pipelineName, sourceIndex }) => setCanReindexError(errorMessage); } } - if (hasPrivileges !== undefined) { + if (hasPrivileges !== undefined && selectedIndex.length) { checkPrivileges(); } }, @@ -264,6 +264,7 @@ export const ReindexWithPipeline: FC = ({ pipelineName, sourceIndex }) => 0) || !canReindex || destinationIndexExists @@ -395,7 +396,7 @@ export const ReindexWithPipeline: FC = ({ pipelineName, sourceIndex }) => 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.reindexStartedMessage', { defaultMessage: 'Reindexing of {sourceIndex} to {destinationIndex} has started.', - values: { sourceIndex, destinationIndex }, + values: { sourceIndex: selectedIndex[0].label, destinationIndex }, } )} color="success" diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/get_steps.ts b/x-pack/plugins/ml/public/application/components/ml_inference/get_steps.ts index a7d3ea17de099..3f7c9ff2255fe 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/get_steps.ts +++ b/x-pack/plugins/ml/public/application/components/ml_inference/get_steps.ts @@ -11,7 +11,8 @@ import { ADD_INFERENCE_PIPELINE_STEPS } from './constants'; export function getSteps( step: AddInferencePipelineSteps, isConfigureStepValid: boolean, - isPipelineDataValid: boolean + isPipelineDataValid: boolean, + hasProcessorStep: boolean ) { let nextStep: AddInferencePipelineSteps | undefined; let previousStep: AddInferencePipelineSteps | undefined; @@ -19,7 +20,9 @@ export function getSteps( switch (step) { case ADD_INFERENCE_PIPELINE_STEPS.DETAILS: - nextStep = ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR; + nextStep = hasProcessorStep + ? ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR + : ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE; isContinueButtonEnabled = isConfigureStepValid; break; case ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR: @@ -29,7 +32,9 @@ export function getSteps( break; case ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE: nextStep = ADD_INFERENCE_PIPELINE_STEPS.TEST; - previousStep = ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR; + previousStep = hasProcessorStep + ? ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR + : ADD_INFERENCE_PIPELINE_STEPS.DETAILS; isContinueButtonEnabled = isPipelineDataValid; break; case ADD_INFERENCE_PIPELINE_STEPS.TEST: diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts b/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts index f6326669cf55b..8c2e567b1c4f1 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts +++ b/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts @@ -5,8 +5,10 @@ * 2.0. */ +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { i18n } from '@kbn/i18n'; import { IngestInferenceProcessor } from '@elastic/elasticsearch/lib/api/types'; +import type { SupportedPytorchTasksType } from '@kbn/ml-trained-models-utils'; import { InferenceModelTypes } from './types'; import type { AddInferencePipelineFormErrors } from './types'; @@ -46,6 +48,18 @@ const INFERENCE_CONFIG_MODEL_TYPE_ERROR = i18n.translate( defaultMessage: 'Inference configuration inference type must match model type.', } ); +const PROCESSOR_REQUIRED = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.processorRequiredError', + { + defaultMessage: 'At least one processor is required to create the pipeline.', + } +); +const INFERENCE_PROCESSOR_REQUIRED = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.inferenceProcessorRequiredError', + { + defaultMessage: "An inference processor specifying 'model_id' is required.", + } +); const VALID_PIPELINE_NAME_REGEX = /^[\w\-]+$/; export const isValidPipelineName = (input: string): boolean => { @@ -75,7 +89,7 @@ export const validateInferencePipelineConfigurationStep = ( export const validateInferenceConfig = ( inferenceConfig: IngestInferenceProcessor['inference_config'], - modelType?: InferenceModelTypes + modelType?: InferenceModelTypes | SupportedPytorchTasksType ) => { const inferenceConfigKeys = Object.keys(inferenceConfig ?? {}); let error; @@ -116,3 +130,31 @@ export const validateFieldMap = ( return error; }; + +export const validatePipelineProcessors = ( + pipelineProcessors: estypes.IngestPipeline, + taskType?: SupportedPytorchTasksType +) => { + const { processors } = pipelineProcessors; + let error; + // Must have at least one processor + if (!Array.isArray(processors) || (Array.isArray(processors) && processors.length < 1)) { + error = PROCESSOR_REQUIRED; + } + + const inferenceProcessor = processors?.find( + (processor) => processor.inference && processor.inference.model_id + ); + + if (inferenceProcessor === undefined) { + error = INFERENCE_PROCESSOR_REQUIRED; + } else { + // If populated, inference config must have the correct model type + const inferenceConfig = inferenceProcessor.inference?.inference_config; + if (taskType && inferenceConfig) { + error = validateInferenceConfig(inferenceConfig, taskType); + } + } + + return error; +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx b/x-pack/plugins/ml/public/application/components/shared/add_inference_pipeline_footer.tsx similarity index 88% rename from x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx rename to x-pack/plugins/ml/public/application/components/shared/add_inference_pipeline_footer.tsx index 04ea2ea217375..e6d000c4e9531 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx +++ b/x-pack/plugins/ml/public/application/components/shared/add_inference_pipeline_footer.tsx @@ -9,14 +9,14 @@ import React, { FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { AddInferencePipelineSteps } from '../types'; +import type { AddInferencePipelineSteps } from '../ml_inference/types'; import { BACK_BUTTON_LABEL, CANCEL_BUTTON_LABEL, CLOSE_BUTTON_LABEL, CONTINUE_BUTTON_LABEL, -} from '../constants'; -import { getSteps } from '../get_steps'; +} from '../ml_inference/constants'; +import { getSteps } from '../ml_inference/get_steps'; interface Props { isDetailsStepValid: boolean; @@ -26,7 +26,8 @@ interface Props { step: AddInferencePipelineSteps; onClose: () => void; onCreate: () => void; - setStep: React.Dispatch>; + setStep: (step: AddInferencePipelineSteps) => void; + hasProcessorStep: boolean; } export const AddInferencePipelineFooter: FC = ({ @@ -38,10 +39,11 @@ export const AddInferencePipelineFooter: FC = ({ onCreate, step, setStep, + hasProcessorStep, }) => { const { nextStep, previousStep, isContinueButtonEnabled } = useMemo( - () => getSteps(step, isDetailsStepValid, isConfigureProcessorStepValid), - [isDetailsStepValid, isConfigureProcessorStepValid, step] + () => getSteps(step, isDetailsStepValid, isConfigureProcessorStepValid, hasProcessorStep), + [isDetailsStepValid, isConfigureProcessorStepValid, step, hasProcessorStep] ); return ( diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_horizontal_steps.tsx b/x-pack/plugins/ml/public/application/components/shared/add_inference_pipeline_horizontal_steps.tsx similarity index 58% rename from x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_horizontal_steps.tsx rename to x-pack/plugins/ml/public/application/components/shared/add_inference_pipeline_horizontal_steps.tsx index 2a34f6483c24d..c8cdf387c84c2 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_horizontal_steps.tsx +++ b/x-pack/plugins/ml/public/application/components/shared/add_inference_pipeline_horizontal_steps.tsx @@ -8,58 +8,58 @@ import React, { FC, memo } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiStepsHorizontal, EuiStepsHorizontalProps } from '@elastic/eui'; -import type { AddInferencePipelineSteps } from '../types'; -import { ADD_INFERENCE_PIPELINE_STEPS } from '../constants'; +import { EuiStepsHorizontal, type EuiStepsHorizontalProps } from '@elastic/eui'; +import type { AddInferencePipelineSteps } from '../ml_inference/types'; +import { ADD_INFERENCE_PIPELINE_STEPS } from '../ml_inference/constants'; const steps = Object.values(ADD_INFERENCE_PIPELINE_STEPS); interface Props { step: AddInferencePipelineSteps; - setStep: React.Dispatch>; + setStep: (step: AddInferencePipelineSteps) => void; isDetailsStepValid: boolean; - isConfigureProcessorStepValid: boolean; + isConfigureProcessorStepValid?: boolean; + hasProcessorStep: boolean; + pipelineCreated: boolean; } +const DISABLED = 'disabled'; +const COMPLETE = 'complete'; +const INCOMPLETE = 'incomplete'; + export const AddInferencePipelineHorizontalSteps: FC = memo( - ({ step, setStep, isDetailsStepValid, isConfigureProcessorStepValid }) => { + ({ + step, + setStep, + isDetailsStepValid, + isConfigureProcessorStepValid, + hasProcessorStep, + pipelineCreated, + }) => { const currentStepIndex = steps.findIndex((s) => s === step); + const navSteps: EuiStepsHorizontalProps['steps'] = [ { // Details - onClick: () => setStep(ADD_INFERENCE_PIPELINE_STEPS.DETAILS), - status: isDetailsStepValid ? 'complete' : 'incomplete', - title: i18n.translate( - 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.details.title', - { - defaultMessage: 'Details', - } - ), - }, - { - // Processor configuration onClick: () => { - if (!isDetailsStepValid) return; - setStep(ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR); + if (pipelineCreated) return; + setStep(ADD_INFERENCE_PIPELINE_STEPS.DETAILS); }, - status: - isDetailsStepValid && isConfigureProcessorStepValid && currentStepIndex > 1 - ? 'complete' - : 'incomplete', + status: isDetailsStepValid ? COMPLETE : INCOMPLETE, title: i18n.translate( - 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.configureProcessor.title', + 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.details.title', { - defaultMessage: 'Configure processor', + defaultMessage: 'Details', } ), }, { - // handle failures + // Handle failures onClick: () => { - if (!isDetailsStepValid) return; + if (!isDetailsStepValid || pipelineCreated) return; setStep(ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE); }, - status: currentStepIndex > 2 ? 'complete' : 'incomplete', + status: currentStepIndex > 2 ? COMPLETE : INCOMPLETE, title: i18n.translate( 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.handleFailures.title', { @@ -70,10 +70,10 @@ export const AddInferencePipelineHorizontalSteps: FC = memo( { // Test onClick: () => { - if (!isConfigureProcessorStepValid || !isDetailsStepValid) return; + if (!isConfigureProcessorStepValid || !isDetailsStepValid || pipelineCreated) return; setStep(ADD_INFERENCE_PIPELINE_STEPS.TEST); }, - status: currentStepIndex > 3 ? 'complete' : 'incomplete', + status: currentStepIndex > 3 ? COMPLETE : INCOMPLETE, title: i18n.translate( 'xpack.ml.trainedModels.content.indices.transforms.addInferencePipelineModal.steps.test.title', { @@ -84,10 +84,10 @@ export const AddInferencePipelineHorizontalSteps: FC = memo( { // Review and Create onClick: () => { - if (!isConfigureProcessorStepValid) return; + if (!isConfigureProcessorStepValid || pipelineCreated) return; setStep(ADD_INFERENCE_PIPELINE_STEPS.CREATE); }, - status: isDetailsStepValid && isConfigureProcessorStepValid ? 'incomplete' : 'disabled', + status: isDetailsStepValid && isConfigureProcessorStepValid ? INCOMPLETE : DISABLED, title: i18n.translate( 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.create.title', { @@ -96,23 +96,60 @@ export const AddInferencePipelineHorizontalSteps: FC = memo( ), }, ]; + + if (hasProcessorStep === true) { + navSteps.splice(1, 0, { + // Processor configuration + onClick: () => { + if (!isDetailsStepValid || pipelineCreated) return; + setStep(ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR); + }, + status: + isDetailsStepValid && isConfigureProcessorStepValid && currentStepIndex > 1 + ? COMPLETE + : INCOMPLETE, + title: i18n.translate( + 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.configureProcessor.title', + { + defaultMessage: 'Configure processor', + } + ), + }); + } + let DETAILS_INDEX: number; + let CONFIGURE_INDEX: number | undefined; + let ON_FAILURE_INDEX: number; + let TEST_INDEX: number; + let CREATE_INDEX: number; + + if (hasProcessorStep) { + [DETAILS_INDEX, CONFIGURE_INDEX, ON_FAILURE_INDEX, TEST_INDEX, CREATE_INDEX] = [ + 0, 1, 2, 3, 4, 5, + ]; + } else { + [DETAILS_INDEX, ON_FAILURE_INDEX, TEST_INDEX, CREATE_INDEX] = [0, 1, 2, 3, 4]; + } + switch (step) { case ADD_INFERENCE_PIPELINE_STEPS.DETAILS: - navSteps[0].status = 'current'; + navSteps[DETAILS_INDEX].status = 'current'; break; case ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR: - navSteps[1].status = 'current'; + if (CONFIGURE_INDEX !== undefined) { + navSteps[CONFIGURE_INDEX].status = 'current'; + } break; case ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE: - navSteps[2].status = 'current'; + navSteps[ON_FAILURE_INDEX].status = 'current'; break; case ADD_INFERENCE_PIPELINE_STEPS.TEST: - navSteps[3].status = 'current'; + navSteps[TEST_INDEX].status = 'current'; break; case ADD_INFERENCE_PIPELINE_STEPS.CREATE: - navSteps[4].status = 'current'; + navSteps[CREATE_INDEX].status = 'current'; break; } + return ; } ); diff --git a/x-pack/plugins/ml/public/application/components/shared/index.ts b/x-pack/plugins/ml/public/application/components/shared/index.ts new file mode 100644 index 0000000000000..573c13257a465 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/shared/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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { AddInferencePipelineHorizontalSteps } from './add_inference_pipeline_horizontal_steps'; +export { AddInferencePipelineFooter } from './add_inference_pipeline_footer'; +export { ReviewAndCreatePipeline } from './review_and_create_pipeline'; +export { OnFailureConfiguration } from './on_failure_configuration'; +export { PipelineDetailsTitle } from './pipeline_details_title'; +export { PipelineNameAndDescription } from './pipeline_name_and_description'; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx b/x-pack/plugins/ml/public/application/components/shared/on_failure_configuration.tsx similarity index 96% rename from x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx rename to x-pack/plugins/ml/public/application/components/shared/on_failure_configuration.tsx index f53e80b122f53..10ab5a46a6327 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx +++ b/x-pack/plugins/ml/public/application/components/shared/on_failure_configuration.tsx @@ -25,12 +25,12 @@ import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { SaveChangesButton } from './save_changes_button'; -import type { MlInferenceState } from '../types'; -import { getDefaultOnFailureConfiguration } from '../state'; -import { CANCEL_EDIT_MESSAGE, EDIT_MESSAGE } from '../constants'; -import { useMlKibana } from '../../../contexts/kibana'; -import { isValidJson } from '../../../../../common/util/validation_utils'; +import { SaveChangesButton } from '../ml_inference/components/save_changes_button'; +import type { MlInferenceState } from '../ml_inference/types'; +import { getDefaultOnFailureConfiguration } from '../ml_inference/state'; +import { CANCEL_EDIT_MESSAGE, EDIT_MESSAGE } from '../ml_inference/constants'; +import { useMlKibana } from '../../contexts/kibana'; +import { isValidJson } from '../../../../common/util/validation_utils'; interface Props { handleAdvancedConfigUpdate: (configUpdate: Partial) => void; diff --git a/x-pack/plugins/ml/public/application/components/shared/pipeline_details_title.tsx b/x-pack/plugins/ml/public/application/components/shared/pipeline_details_title.tsx new file mode 100644 index 0000000000000..ce7ef231256c5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/shared/pipeline_details_title.tsx @@ -0,0 +1,73 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCode, EuiLink, EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; + +import { useMlKibana } from '../../contexts/kibana'; + +interface Props { + modelId: string; +} + +export const PipelineDetailsTitle: FC = ({ modelId }) => { + const { + services: { + docLinks: { links }, + }, + } = useMlKibana(); + + return ( + <> + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.title', + { defaultMessage: 'Create a pipeline' } + )} +

+
+ + +

+ {modelId}, + pipeline: ( + + pipeline + + ), + }} + /> +

+

+ + _reindex API + + ), + pipelineSimulateLink: ( + + pipeline/_simulate + + ), + }} + /> +

+
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/shared/pipeline_name_and_description.tsx b/x-pack/plugins/ml/public/application/components/shared/pipeline_name_and_description.tsx new file mode 100644 index 0000000000000..c64fbca939034 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/shared/pipeline_name_and_description.tsx @@ -0,0 +1,115 @@ +/* + * 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. + */ + +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFieldText, EuiFormRow, EuiText, EuiTextArea } from '@elastic/eui'; + +interface Props { + handlePipelineConfigUpdate: (configUpdate: Partial) => void; + pipelineNameError: string | undefined; + pipelineDescription: string; + pipelineName: string; +} + +export const PipelineNameAndDescription: FC = ({ + pipelineName, + pipelineNameError, + pipelineDescription, + handlePipelineConfigUpdate, +}) => { + const handleConfigChange = (value: string, type: string) => { + handlePipelineConfigUpdate({ [type]: value }); + }; + + return ( + <> + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText', + { + defaultMessage: + 'Pipeline names are unique within a deployment and can only contain letters, numbers, underscores, and hyphens.', + } + )} + + ) + } + error={pipelineNameError} + isInvalid={pipelineNameError !== undefined} + > + ) => + handleConfigChange(e.target.value, 'pipelineName') + } + /> + + {/* DESCRIPTION */} + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.description.helpText', + { + defaultMessage: 'A description of the pipeline.', + } + )} + + } + > + ) => + handleConfigChange(e.target.value, 'pipelineDescription') + } + /> + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx b/x-pack/plugins/ml/public/application/components/shared/review_and_create_pipeline.tsx similarity index 86% rename from x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx rename to x-pack/plugins/ml/public/application/components/shared/review_and_create_pipeline.tsx index d80e678bafb06..7cb3066436af9 100644 --- a/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx +++ b/x-pack/plugins/ml/public/application/components/shared/review_and_create_pipeline.tsx @@ -7,6 +7,7 @@ import React, { FC, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiAccordion, @@ -18,17 +19,28 @@ import { EuiSpacer, EuiTitle, EuiText, + EuiTextColor, htmlIdGenerator, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types'; -import { useMlKibana } from '../../../contexts/kibana'; -import { ReindexWithPipeline } from './reindex_with_pipeline'; +import { useMlKibana } from '../../contexts/kibana'; +import { ReindexWithPipeline } from '../ml_inference/components/reindex_with_pipeline'; const MANAGEMENT_APP_ID = 'management'; +function getFieldFromPipelineConfig(config: estypes.IngestPipeline) { + const { processors } = config; + let field = ''; + if (processors?.length) { + field = Object.keys(processors[0].inference?.field_map ?? {})[0]; + } + return field; +} + interface Props { + highlightTargetField?: boolean; inferencePipeline: IngestPipeline; modelType?: string; pipelineName: string; @@ -38,6 +50,7 @@ interface Props { } export const ReviewAndCreatePipeline: FC = ({ + highlightTargetField = false, inferencePipeline, modelType, pipelineName, @@ -62,6 +75,10 @@ export const ReviewAndCreatePipeline: FC = ({ : links.ingest.inferenceClassification; const accordionId = useMemo(() => htmlIdGenerator()(), []); + const targetedField = useMemo( + () => getFieldFromPipelineConfig(inferencePipeline), + [inferencePipeline] + ); const configCodeBlock = useMemo( () => ( @@ -84,7 +101,7 @@ export const ReviewAndCreatePipeline: FC = ({ gutterSize="s" data-test-subj="mlTrainedModelsInferenceReviewAndCreateStep" > - + {pipelineCreated === false ? (

@@ -189,20 +206,19 @@ export const ReviewAndCreatePipeline: FC = ({ ) : null} - - -

- {!pipelineCreated ? ( - - ) : null} -

-
-
+ {highlightTargetField ? ( + + + {targetedField} }} + /> + + + ) : null} - {pipelineCreated && sourceIndex ? ( + {pipelineCreated ? ( <> void; + model: ModelItem; +} + +export const CreatePipelineForModelFlyout: FC = ({ + onClose, + model, +}) => { + const { + currentContext: { pipelineConfig }, + } = useTestTrainedModelsContext(); + + const initialState = useMemo( + () => getInitialState(model, pipelineConfig), + // eslint-disable-next-line react-hooks/exhaustive-deps + [model.model_id, pipelineConfig] + ); + const [formState, setFormState] = useState(initialState); + const [step, setStep] = useState(ADD_INFERENCE_PIPELINE_STEPS.DETAILS); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + const taskType = useMemo( + () => Object.keys(model.inference_config ?? {})[0], + // eslint-disable-next-line react-hooks/exhaustive-deps + [model.model_id] + ) as SupportedPytorchTasksType; + + const { + trainedModels: { createInferencePipeline }, + } = useMlApiContext(); + + const createPipeline = async () => { + setFormState({ ...formState, creatingPipeline: true }); + try { + const config = getPipelineConfig(formState); + await createInferencePipeline(formState.pipelineName, config); + setFormState({ + ...formState, + pipelineCreated: true, + creatingPipeline: false, + pipelineError: undefined, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + const errorProperties = extractErrorProperties(e); + setFormState({ + ...formState, + creatingPipeline: false, + pipelineError: errorProperties.message ?? e.message, + }); + } + }; + + const pipelineNames = useFetchPipelines(); + + const handleConfigUpdate = (configUpdate: Partial) => { + const updatedState = { ...formState, ...configUpdate }; + setFormState(updatedState); + }; + + const handleSetStep = (currentStep: AddInferencePipelineSteps) => { + setStep(currentStep); + }; + + const { pipelineName: pipelineNameError } = useMemo(() => { + const errors = validateInferencePipelineConfigurationStep( + formState.pipelineName, + pipelineNames + ); + return errors; + }, [pipelineNames, formState.pipelineName]); + + return ( + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.createInferencePipeline.title', + { + defaultMessage: 'Create inference pipeline', + } + )} +

+
+
+ + + + {step === ADD_INFERENCE_PIPELINE_STEPS.DETAILS && ( + + )} + {step === ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE && ( + + )} + {step === ADD_INFERENCE_PIPELINE_STEPS.TEST && ( + + )} + {step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && ( + + )} + + + + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_inference_properties_from_pipeline_config.ts b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_inference_properties_from_pipeline_config.ts new file mode 100644 index 0000000000000..68b7dcda61a73 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_inference_properties_from_pipeline_config.ts @@ -0,0 +1,118 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { + IngestInferenceProcessor, + IngestInferenceConfig, +} from '@elastic/elasticsearch/lib/api/types'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; +import { DEFAULT_INPUT_FIELD } from '../test_models/models/inference_base'; + +const INPUT_FIELD = 'inputField'; +const ZERO_SHOT_CLASSIFICATION_PROPERTIES = ['labels', 'multi_label'] as const; +const QUESTION_ANSWERING_PROPERTIES = ['question'] as const; + +const MODEL_INFERENCE_CONFIG_PROPERTIES = { + [SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING]: QUESTION_ANSWERING_PROPERTIES, + [SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION]: ZERO_SHOT_CLASSIFICATION_PROPERTIES, +} as const; + +type SupportedModelInferenceConfigPropertiesType = keyof typeof MODEL_INFERENCE_CONFIG_PROPERTIES; + +interface MLIngestInferenceProcessor extends IngestInferenceProcessor { + inference_config: MLInferencePipelineInferenceConfig; +} + +// Currently, estypes doesn't include pipeline processor types with the trained model processors +type MLInferencePipelineInferenceConfig = IngestInferenceConfig & { + zero_shot_classification?: estypes.MlZeroShotClassificationInferenceOptions; + question_answering?: estypes.MlQuestionAnsweringInferenceUpdateOptions; +}; + +interface GetInferencePropertiesFromPipelineConfigReturnType { + inputField: string; + inferenceConfig?: MLInferencePipelineInferenceConfig; + inferenceObj?: IngestInferenceProcessor | MLIngestInferenceProcessor; + fieldMap?: IngestInferenceProcessor['field_map']; + labels?: string[]; + multi_label?: boolean; + question?: string; +} + +function isSupportedInferenceConfigPropertyType( + arg: unknown +): arg is SupportedModelInferenceConfigPropertiesType { + return typeof arg === 'string' && Object.keys(MODEL_INFERENCE_CONFIG_PROPERTIES).includes(arg); +} + +export function isMlInferencePipelineInferenceConfig( + arg: unknown +): arg is MLInferencePipelineInferenceConfig { + return ( + isPopulatedObject(arg, [SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING]) || + isPopulatedObject(arg, [SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION]) + ); +} + +export function isMlIngestInferenceProcessor(arg: unknown): arg is MLIngestInferenceProcessor { + return ( + isPopulatedObject(arg) && + arg.hasOwnProperty('inference_config') && + (isPopulatedObject(arg.inference_config, [SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING]) || + isPopulatedObject(arg.inference_config, [SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION])) + ); +} + +export function getInferencePropertiesFromPipelineConfig( + type: string, + pipelineConfig: estypes.IngestPipeline +): GetInferencePropertiesFromPipelineConfigReturnType { + const propertiesToReturn: GetInferencePropertiesFromPipelineConfigReturnType = { + [INPUT_FIELD]: '', + }; + + pipelineConfig.processors?.forEach((processor) => { + const { inference } = processor; + if (inference) { + propertiesToReturn.inferenceObj = inference; + // Get the input field + if (inference.field_map) { + propertiesToReturn.fieldMap = inference.field_map; + + for (const [key, value] of Object.entries(inference.field_map)) { + if (value === DEFAULT_INPUT_FIELD) { + propertiesToReturn[INPUT_FIELD] = key; + } + } + if (propertiesToReturn[INPUT_FIELD] === '') { + // If not found, set to the first field in the field map + propertiesToReturn[INPUT_FIELD] = Object.keys(inference.field_map)[0]; + } + } + propertiesToReturn.inferenceConfig = inference.inference_config; + // Get the properties associated with the type of model/task + if ( + isMlInferencePipelineInferenceConfig(propertiesToReturn.inferenceConfig) && + isSupportedInferenceConfigPropertyType(type) + ) { + MODEL_INFERENCE_CONFIG_PROPERTIES[type]?.forEach((property) => { + const configSettings = + propertiesToReturn.inferenceConfig && propertiesToReturn.inferenceConfig[type]; + propertiesToReturn[property] = + configSettings && configSettings.hasOwnProperty(property) + ? // @ts-ignore + configSettings[property] + : undefined; + }); + } + } + }); + + return propertiesToReturn; +} diff --git a/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_pipeline_config.ts b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_pipeline_config.ts new file mode 100644 index 0000000000000..10c405090f057 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/get_pipeline_config.ts @@ -0,0 +1,37 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import type { InferecePipelineCreationState } from './state'; + +export function getPipelineConfig(state: InferecePipelineCreationState): estypes.IngestPipeline { + const { ignoreFailure, modelId, onFailure, pipelineDescription, initialPipelineConfig } = state; + const processor = + initialPipelineConfig?.processors && initialPipelineConfig.processors?.length + ? initialPipelineConfig?.processors[0] + : {}; + + return { + description: pipelineDescription, + processors: [ + { + inference: { + ...(processor?.inference + ? { + ...processor.inference, + ignore_failure: ignoreFailure, + ...(onFailure && Object.keys(onFailure).length > 0 + ? { on_failure: onFailure } + : { on_failure: undefined }), + } + : {}), + model_id: modelId, + }, + }, + ], + }; +} diff --git a/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/pipeline_details.tsx b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/pipeline_details.tsx new file mode 100644 index 0000000000000..ef474ce3b4345 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/pipeline_details.tsx @@ -0,0 +1,183 @@ +/* + * 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, { FC, memo, useState } from 'react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; +import type { SupportedPytorchTasksType } from '@kbn/ml-trained-models-utils'; +import { type InferecePipelineCreationState } from './state'; +import { EDIT_MESSAGE, CANCEL_EDIT_MESSAGE } from '../../components/ml_inference/constants'; +import { isValidJson } from '../../../../common/util/validation_utils'; +import { useTestTrainedModelsContext } from '../test_models/test_trained_models_context'; +import { SaveChangesButton } from '../../components/ml_inference/components/save_changes_button'; +import { validatePipelineProcessors } from '../../components/ml_inference/validation'; +import { PipelineDetailsTitle, PipelineNameAndDescription } from '../../components/shared'; + +interface Props { + handlePipelineConfigUpdate: (configUpdate: Partial) => void; + modelId: string; + pipelineNameError: string | undefined; + pipelineName: string; + pipelineDescription: string; + initialPipelineConfig?: InferecePipelineCreationState['initialPipelineConfig']; + setHasUnsavedChanges: React.Dispatch>; + taskType?: SupportedPytorchTasksType; +} + +export const PipelineDetails: FC = memo( + ({ + handlePipelineConfigUpdate, + modelId, + pipelineName, + pipelineNameError, + pipelineDescription, + initialPipelineConfig, + setHasUnsavedChanges, + taskType, + }) => { + const [isProcessorConfigValid, setIsProcessorConfigValid] = useState(true); + const [processorConfigError, setProcessorConfigError] = useState(); + + const { + currentContext: { pipelineConfig }, + } = useTestTrainedModelsContext(); + const [processorConfigString, setProcessorConfigString] = useState( + JSON.stringify(initialPipelineConfig ?? {}, null, 2) + ); + const [editProcessorConfig, setEditProcessorConfig] = useState(false); + + const updateProcessorConfig = () => { + const invalidProcessorConfigMessage = validatePipelineProcessors( + JSON.parse(processorConfigString), + taskType + ); + if (invalidProcessorConfigMessage === undefined) { + handlePipelineConfigUpdate({ initialPipelineConfig: JSON.parse(processorConfigString) }); + setHasUnsavedChanges(false); + setEditProcessorConfig(false); + setProcessorConfigError(undefined); + } else { + setHasUnsavedChanges(true); + setIsProcessorConfigValid(false); + setProcessorConfigError(invalidProcessorConfigMessage); + } + }; + + const handleProcessorConfigChange = (json: string) => { + setProcessorConfigString(json); + const valid = isValidJson(json); + setIsProcessorConfigValid(valid); + }; + + const resetProcessorConfig = () => { + setProcessorConfigString(JSON.stringify(pipelineConfig, null, 2)); + setIsProcessorConfigValid(true); + setProcessorConfigError(undefined); + }; + + return ( + + + + + + + {/* NAME */} + + {/* NAME and DESCRIPTION */} + + {/* PROCESSOR CONFIGURATION */} + + + { + const editingState = !editProcessorConfig; + if (editingState === false) { + setProcessorConfigError(undefined); + setIsProcessorConfigValid(true); + setHasUnsavedChanges(false); + } + setEditProcessorConfig(editingState); + }} + > + {editProcessorConfig ? CANCEL_EDIT_MESSAGE : EDIT_MESSAGE} + + + + {editProcessorConfig ? ( + + ) : null} + + + {editProcessorConfig ? ( + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.resetInferenceConfigButton', + { defaultMessage: 'Reset' } + )} + + ) : null} + + + } + error={processorConfigError} + isInvalid={processorConfigError !== undefined} + data-test-subj="mlTrainedModelsInferencePipelineInferenceConfigEditor" + > + {editProcessorConfig ? ( + + ) : ( + + {processorConfigString} + + )} + + + +
+ + ); + } +); diff --git a/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/state.ts b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/state.ts new file mode 100644 index 0000000000000..9edcb19e61e38 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/state.ts @@ -0,0 +1,42 @@ +/* + * 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 * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IngestInferenceProcessor } from '@elastic/elasticsearch/lib/api/types'; +import { getDefaultOnFailureConfiguration } from '../../components/ml_inference/state'; +import type { ModelItem } from '../models_list'; + +export interface InferecePipelineCreationState { + creatingPipeline: boolean; + error: boolean; + ignoreFailure: boolean; + modelId: string; + onFailure?: IngestInferenceProcessor['on_failure']; + pipelineName: string; + pipelineNameError?: string; + pipelineDescription: string; + pipelineCreated: boolean; + pipelineError?: string; + initialPipelineConfig?: estypes.IngestPipeline; + takeActionOnFailure: boolean; +} + +export const getInitialState = ( + model: ModelItem, + initialPipelineConfig: estypes.IngestPipeline | undefined +): InferecePipelineCreationState => ({ + creatingPipeline: false, + error: false, + ignoreFailure: false, + modelId: model.model_id, + onFailure: getDefaultOnFailureConfiguration(), + pipelineDescription: `Uses the pre-trained model ${model.model_id} to infer against the data that is being ingested in the pipeline`, + pipelineName: `ml-inference-${model.model_id}`, + pipelineCreated: false, + initialPipelineConfig, + takeActionOnFailure: true, +}); diff --git a/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/test_trained_model.tsx b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/test_trained_model.tsx new file mode 100644 index 0000000000000..5f793fa84801e --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/create_pipeline_for_model/test_trained_model.tsx @@ -0,0 +1,79 @@ +/* + * 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, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { ModelItem } from '../models_list'; +import { TestTrainedModelContent } from '../test_models/test_trained_model_content'; +import { useMlKibana } from '../../contexts/kibana'; +import { type InferecePipelineCreationState } from './state'; + +interface ContentProps { + model: ModelItem; + handlePipelineConfigUpdate: (configUpdate: Partial) => void; + externalPipelineConfig?: estypes.IngestPipeline; +} + +export const TestTrainedModel: FC = ({ + model, + handlePipelineConfigUpdate, + externalPipelineConfig, +}) => { + const { + services: { + docLinks: { links }, + }, + } = useMlKibana(); + + return ( + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.testTrainedModelTitle', + { defaultMessage: 'Try it out' } + )} +

+
+ + +

+ +

+

+ + Learn more. + + ), + }} + /> +

+
+
+ + + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx index 0aebca5c1673c..dd766d10c36d1 100644 --- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx @@ -69,7 +69,7 @@ import { useToastNotificationService } from '../services/toast_notification_serv import { useFieldFormatter } from '../contexts/kibana/use_field_formatter'; import { useRefresh } from '../routing/use_refresh'; import { SavedObjectsWarning } from '../components/saved_objects_warning'; -import { TestTrainedModelFlyout } from './test_models'; +import { TestModelAndPipelineCreationFlyout } from './test_models'; import { TestDfaModelsFlyout } from './test_dfa_models_flyout'; import { AddInferencePipelineFlyout } from '../components/ml_inference'; import { useEnabledFeatures } from '../contexts/ml'; @@ -819,7 +819,15 @@ export const ModelsList: FC = ({ /> )} {modelToTest === null ? null : ( - + { + setModelToTest(null); + if (refreshList) { + fetchModelsData(); + } + }} + /> )} {dfaModelToTest === null ? null : ( diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/index.ts b/x-pack/plugins/ml/public/application/model_management/test_models/index.ts index 25078a40d4206..4b238f477092e 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/index.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { TestTrainedModelFlyout } from './test_flyout'; +export { TestModelAndPipelineCreationFlyout } from './test_model_and_pipeline_creation_flyout'; export { isTestable, isDfaTrainedModel } from './utils'; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/index_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/index_input.tsx index 03466cd87a16b..3ae11845646ad 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/index_input.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/index_input.tsx @@ -10,7 +10,15 @@ import React, { FC, useState, useMemo, useEffect, useCallback } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { firstValueFrom } from 'rxjs'; import { DataView } from '@kbn/data-views-plugin/common'; -import { EuiSpacer, EuiSelect, EuiFormRow, EuiAccordion, EuiCodeBlock } from '@elastic/eui'; +import { + EuiAccordion, + EuiCode, + EuiCodeBlock, + EuiFormRow, + EuiSpacer, + EuiSelect, + EuiText, +} from '@elastic/eui'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { i18n } from '@kbn/i18n'; @@ -21,9 +29,14 @@ import type { InferrerType } from '.'; interface Props { inferrer: InferrerType; data: ReturnType; + disableIndexSelection: boolean; } -export const InferenceInputFormIndexControls: FC = ({ inferrer, data }) => { +export const InferenceInputFormIndexControls: FC = ({ + inferrer, + data, + disableIndexSelection, +}) => { const { dataViewListItems, fieldNames, @@ -40,14 +53,25 @@ export const InferenceInputFormIndexControls: FC = ({ inferrer, data }) = return ( <> - setSelectedDataViewId(e.target.value)} - hasNoInitialSelection={true} - disabled={runningState === RUNNING_STATE.RUNNING} - fullWidth - /> + {disableIndexSelection ? ( + + + {dataViewListItems.find((item) => item.value === selectedDataViewId)?.text} + + + ) : ( + { + inferrer.setSelectedDataViewId(e.target.value); + setSelectedDataViewId(e.target.value); + }} + hasNoInitialSelection={true} + disabled={runningState === RUNNING_STATE.RUNNING} + fullWidth + /> + )} = ({ inferrer, data }) = setSelectedField(e.target.value)} + onChange={(e) => { + setSelectedField(e.target.value); + }} hasNoInitialSelection={true} disabled={runningState === RUNNING_STATE.RUNNING} fullWidth @@ -79,7 +105,14 @@ export const InferenceInputFormIndexControls: FC = ({ inferrer, data }) = } )} > - + {JSON.stringify(pipeline, null, 2)} @@ -87,7 +120,13 @@ export const InferenceInputFormIndexControls: FC = ({ inferrer, data }) = ); }; -export function useIndexInput({ inferrer }: { inferrer: InferrerType }) { +export function useIndexInput({ + inferrer, + defaultSelectedDataViewId, +}: { + inferrer: InferrerType; + defaultSelectedDataViewId?: string; +}) { const { services: { data: { @@ -100,7 +139,9 @@ export function useIndexInput({ inferrer }: { inferrer: InferrerType }) { const [dataViewListItems, setDataViewListItems] = useState< Array<{ value: string; text: string }> >([]); - const [selectedDataViewId, setSelectedDataViewId] = useState(undefined); + const [selectedDataViewId, setSelectedDataViewId] = useState( + defaultSelectedDataViewId + ); const [selectedDataView, setSelectedDataView] = useState(null); const [fieldNames, setFieldNames] = useState>([]); const selectedField = useObservable(inferrer.getInputField$(), inferrer.getInputField()); @@ -197,11 +238,20 @@ export function useIndexInput({ inferrer }: { inferrer: InferrerType }) { })); setFieldNames(tempFieldNames); - const fieldName = tempFieldNames.length === 1 ? tempFieldNames[0].value : undefined; + const defaultSelectedField = inferrer.getInputField(); + + const fieldName = + defaultSelectedField && + tempFieldNames.find((field) => field.value === defaultSelectedField) + ? defaultSelectedField + : tempFieldNames[0].value; + // Only set a field if it's the default field + // if (inferrer.getInputField() === DEFAULT_INPUT_FIELD) { inferrer.setInputField(fieldName); + // } } }, - [selectedDataView, inferrer] + [selectedDataView, inferrer] // defaultSelectedField ); useEffect( diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts index 8dc0bf6b88815..bdd082cf8ca04 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts @@ -31,7 +31,7 @@ export type InferenceOptions = | estypes.MlTextEmbeddingInferenceOptions | estypes.MlQuestionAnsweringInferenceUpdateOptions; -const DEFAULT_INPUT_FIELD = 'text_field'; +export const DEFAULT_INPUT_FIELD = 'text_field'; export const DEFAULT_INFERENCE_TIME_OUT = '30s'; export type FormattedNerResponse = Array<{ @@ -72,8 +72,10 @@ export abstract class InferenceBase { private isValid$ = new BehaviorSubject(false); private pipeline$ = new BehaviorSubject({}); private supportedFieldTypes: ES_FIELD_TYPES[] = [ES_FIELD_TYPES.TEXT]; + private selectedDataViewId: string | undefined; protected readonly info: string[] = []; + public switchToCreationMode?: () => void; private subscriptions$: Subscription = new Subscription(); @@ -87,8 +89,13 @@ export abstract class InferenceBase { this.inputField$.next(this.modelInputField); } + public setSwitchtoCreationMode(callback: () => void) { + this.switchToCreationMode = callback; + } + public destroy() { this.subscriptions$.unsubscribe(); + this.pipeline$.unsubscribe(); } protected initialize( @@ -162,6 +169,15 @@ export abstract class InferenceBase { this.runningState$.next(RUNNING_STATE.STOPPED); } + public setSelectedDataViewId(dataViewId: string) { + // Data view selected for testing + this.selectedDataViewId = dataViewId; + } + + public getSelectedDataViewId() { + return this.selectedDataViewId; + } + public setInputField(field: string | undefined) { // if the field is not set, change to be the same as the model input field this.inputField$.next(field === undefined ? this.modelInputField : field); diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx index 4dbe900283657..4b87ddccfb4b2 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx @@ -11,7 +11,6 @@ import useObservable from 'react-use/lib/useObservable'; import { EuiSpacer, - EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, @@ -28,13 +27,22 @@ import { ErrorMessage } from '../../inference_error'; import type { InferrerType } from '..'; import { useIndexInput, InferenceInputFormIndexControls } from '../index_input'; import { RUNNING_STATE } from '../inference_base'; +import { InputFormControls } from './input_form_controls'; +import { useTestTrainedModelsContext } from '../../test_trained_models_context'; interface Props { inferrer: InferrerType; } export const IndexInputForm: FC = ({ inferrer }) => { - const data = useIndexInput({ inferrer }); + const { + currentContext: { defaultSelectedDataViewId, createPipelineFlyoutOpen }, + } = useTestTrainedModelsContext(); + + const data = useIndexInput({ + inferrer, + defaultSelectedDataViewId, + }); const { reloadExamples, selectedField } = data; const [errorText, setErrorText] = useState(null); @@ -60,23 +68,26 @@ export const IndexInputForm: FC = ({ inferrer }) => { return ( <>{infoComponent} - + - - + - + {runningState === RUNNING_STATE.RUNNING ? : null} diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/input_form_controls.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/input_form_controls.tsx new file mode 100644 index 0000000000000..18cacbb745504 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/input_form_controls.tsx @@ -0,0 +1,63 @@ +/* + * 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, { FC } from 'react'; + +import { EuiButton, EuiButtonEmpty, EuiFlexItem } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import type { InferrerType } from '..'; + +interface Props { + testButtonDisabled: boolean; + createPipelineButtonDisabled: boolean; + inferrer: InferrerType; + showCreatePipelineButton?: boolean; +} + +export const InputFormControls: FC = ({ + testButtonDisabled, + createPipelineButtonDisabled, + inferrer, + showCreatePipelineButton, +}) => { + return ( + <> + + + + + + {showCreatePipelineButton ? ( + + { + if (inferrer.switchToCreationMode) { + inferrer.switchToCreationMode(); + } + }} + > + + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx index fc162a305c32b..ae5a4c72cac60 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useMemo, useCallback, FormEventHandler } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { EuiSpacer, EuiButton, EuiTabs, EuiTab, EuiForm } from '@elastic/eui'; +import { EuiFlexGroup, EuiSpacer, EuiTabs, EuiTab, EuiForm } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { extractErrorMessage } from '@kbn/ml-error-utils'; @@ -18,6 +18,7 @@ import type { InferrerType } from '..'; import { OutputLoadingContent } from '../../output_loading'; import { RUNNING_STATE } from '../inference_base'; import { RawOutput } from '../raw_output'; +import { InputFormControls } from './input_form_controls'; interface Props { inferrer: InferrerType; @@ -57,17 +58,15 @@ export const TextInputForm: FC = ({ inferrer }) => { <>{inputComponent}
- - + - +
{runningState !== RUNNING_STATE.STOPPED ? ( <> diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx index 4747d9c149186..80ee311d533a8 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/selected_model.tsx @@ -7,6 +7,7 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import React, { FC, useMemo, useEffect } from 'react'; +import { cloneDeep } from 'lodash'; import { TRAINED_MODEL_TYPE, SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; import { NerInference } from './models/ner'; @@ -22,52 +23,182 @@ import { import { TextEmbeddingInference } from './models/text_embedding'; import { useMlApiContext } from '../../contexts/kibana'; +import { type TestTrainedModelsContextType } from './test_trained_models_context'; import { InferenceInputForm } from './models/inference_input_form'; import { InferrerType } from './models'; import { INPUT_TYPE } from './models/inference_base'; import { TextExpansionInference } from './models/text_expansion'; +import { type InferecePipelineCreationState } from '../create_pipeline_for_model/state'; +import { + getInferencePropertiesFromPipelineConfig, + isMlIngestInferenceProcessor, + isMlInferencePipelineInferenceConfig, +} from '../create_pipeline_for_model/get_inference_properties_from_pipeline_config'; interface Props { model: estypes.MlTrainedModelConfig; inputType: INPUT_TYPE; deploymentId: string; + handlePipelineConfigUpdate?: (configUpdate: Partial) => void; + externalPipelineConfig?: estypes.IngestPipeline; + setCurrentContext?: React.Dispatch; } -export const SelectedModel: FC = ({ model, inputType, deploymentId }) => { +export const SelectedModel: FC = ({ + model, + inputType, + deploymentId, + handlePipelineConfigUpdate, + externalPipelineConfig, + setCurrentContext, +}) => { const { trainedModels } = useMlApiContext(); const inferrer = useMemo(() => { - if (model.model_type === TRAINED_MODEL_TYPE.PYTORCH) { - const taskType = Object.keys(model.inference_config ?? {})[0]; + const taskType = Object.keys(model.inference_config ?? {})[0]; + let tempInferrer: InferrerType | undefined; + const pipelineConfigValues = externalPipelineConfig + ? getInferencePropertiesFromPipelineConfig(taskType, externalPipelineConfig) + : null; + if (model.model_type === TRAINED_MODEL_TYPE.PYTORCH) { switch (taskType) { case SUPPORTED_PYTORCH_TASKS.NER: - return new NerInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new NerInference(trainedModels, model, inputType, deploymentId); + break; case SUPPORTED_PYTORCH_TASKS.TEXT_CLASSIFICATION: - return new TextClassificationInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new TextClassificationInference( + trainedModels, + model, + inputType, + deploymentId + ); + break; case SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION: - return new ZeroShotClassificationInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new ZeroShotClassificationInference( + trainedModels, + model, + inputType, + deploymentId + ); + if (pipelineConfigValues) { + const { labels, multi_label: multiLabel } = pipelineConfigValues; + if (labels && multiLabel !== undefined) { + tempInferrer.setLabelsText(Array.isArray(labels) ? labels.join(',') : labels); + tempInferrer.setMultiLabel(Boolean(multiLabel)); + } + } + break; case SUPPORTED_PYTORCH_TASKS.TEXT_EMBEDDING: - return new TextEmbeddingInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new TextEmbeddingInference(trainedModels, model, inputType, deploymentId); + break; case SUPPORTED_PYTORCH_TASKS.FILL_MASK: - return new FillMaskInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new FillMaskInference(trainedModels, model, inputType, deploymentId); + break; case SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING: - return new QuestionAnsweringInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new QuestionAnsweringInference( + trainedModels, + model, + inputType, + deploymentId + ); + if (pipelineConfigValues?.question) { + tempInferrer.setQuestionText(pipelineConfigValues.question); + } + break; case SUPPORTED_PYTORCH_TASKS.TEXT_EXPANSION: - return new TextExpansionInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new TextExpansionInference(trainedModels, model, inputType, deploymentId); + break; default: break; } } else if (model.model_type === TRAINED_MODEL_TYPE.LANG_IDENT) { - return new LangIdentInference(trainedModels, model, inputType, deploymentId); + tempInferrer = new LangIdentInference(trainedModels, model, inputType, deploymentId); + } + if (tempInferrer) { + if (pipelineConfigValues) { + tempInferrer.setInputField(pipelineConfigValues.inputField); + } + if (externalPipelineConfig === undefined) { + tempInferrer.setSwitchtoCreationMode(() => { + if (tempInferrer && setCurrentContext) { + setCurrentContext({ + pipelineConfig: tempInferrer.getPipeline(), + defaultSelectedDataViewId: tempInferrer.getSelectedDataViewId(), + createPipelineFlyoutOpen: true, + }); + } + }); + } else { + tempInferrer?.getPipeline$().subscribe((testPipeline) => { + if (handlePipelineConfigUpdate && testPipeline && externalPipelineConfig) { + const { + fieldMap: testFieldMap, + inferenceConfig: testInferenceConfig, + labels, + multi_label: multiLabel, + question, + } = getInferencePropertiesFromPipelineConfig(taskType, testPipeline); + + const updatedPipeline = cloneDeep(externalPipelineConfig); + const { inferenceObj: externalInference, inferenceConfig: externalInferenceConfig } = + getInferencePropertiesFromPipelineConfig(taskType, updatedPipeline); + + if (externalInference) { + // Always update target field change + externalInference.field_map = testFieldMap; + + if (externalInferenceConfig === undefined) { + externalInference.inference_config = testInferenceConfig; + } else if ( + isMlIngestInferenceProcessor(externalInference) && + isMlInferencePipelineInferenceConfig(externalInference.inference_config) + ) { + // Only update the properties that change in the test step to avoid overwriting user edits + if ( + taskType === SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION && + labels && + multiLabel !== undefined + ) { + const external = + externalInference.inference_config[ + SUPPORTED_PYTORCH_TASKS.ZERO_SHOT_CLASSIFICATION + ]; + + if (external) { + external.multi_label = multiLabel; + external.labels = labels; + } + } else if ( + taskType === SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING && + question !== undefined + ) { + const external = + externalInference.inference_config[SUPPORTED_PYTORCH_TASKS.QUESTION_ANSWERING]; + + if (external) { + external.question = question; + } + } + } + } + + handlePipelineConfigUpdate({ + initialPipelineConfig: updatedPipeline, + }); + } + }); + } } - }, [inputType, model, trainedModels, deploymentId]); + return tempInferrer; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [inputType, model, trainedModels, deploymentId, setCurrentContext]); useEffect(() => { return () => { inferrer?.destroy(); }; - }, [inferrer]); + }, [inferrer, model.model_id]); if (inferrer !== undefined) { return ; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx index 434621d11773b..f595fd1de35d7 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/test_flyout.tsx @@ -5,126 +5,36 @@ * 2.0. */ -import React, { FC, useState, useMemo } from 'react'; +import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiFormRow, - EuiSelect, - EuiSpacer, - EuiTab, - EuiTabs, - EuiTitle, - useEuiPaddingSize, -} from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; -import { SelectedModel } from './selected_model'; -import { INPUT_TYPE } from './models/inference_base'; import { type ModelItem } from '../models_list'; +import { TestTrainedModelContent } from './test_trained_model_content'; interface Props { model: ModelItem; onClose: () => void; } -export const TestTrainedModelFlyout: FC = ({ model, onClose }) => { - const [deploymentId, setDeploymentId] = useState(model.deployment_ids[0]); - const mediumPadding = useEuiPaddingSize('m'); - - const [inputType, setInputType] = useState(INPUT_TYPE.TEXT); - - const onlyShowTab: INPUT_TYPE | undefined = useMemo(() => { - return (model.type ?? []).includes(SUPPORTED_PYTORCH_TASKS.TEXT_EXPANSION) - ? INPUT_TYPE.INDEX - : undefined; - }, [model]); - - return ( - <> - - - -

- -

-
- - -

{model.model_id}

-
-
- - {model.deployment_ids.length > 1 ? ( - <> - - } - > - { - return { text: v, value: v }; - })} - value={deploymentId} - onChange={(e) => { - setDeploymentId(e.target.value); - }} - /> - - - - ) : null} - - {onlyShowTab === undefined ? ( - <> - - setInputType(INPUT_TYPE.TEXT)} - > - - - setInputType(INPUT_TYPE.INDEX)} - > - - - - - - - ) : null} - - = ({ model, onClose }) => ( + + + +

+ - - - - ); -}; +

+
+ + +

{model.model_id}

+
+
+ + + +
+); diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/test_model_and_pipeline_creation_flyout.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/test_model_and_pipeline_creation_flyout.tsx new file mode 100644 index 0000000000000..33c4e69df59b6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/test_model_and_pipeline_creation_flyout.tsx @@ -0,0 +1,37 @@ +/* + * 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, { FC, useState } from 'react'; + +import { + type TestTrainedModelsContextType, + TestTrainedModelsContext, +} from './test_trained_models_context'; +import type { ModelItem } from '../models_list'; +import { TestTrainedModelFlyout } from './test_flyout'; +import { CreatePipelineForModelFlyout } from '../create_pipeline_for_model/create_pipeline_for_model_flyout'; + +interface Props { + model: ModelItem; + onClose: (refreshList?: boolean) => void; +} +export const TestModelAndPipelineCreationFlyout: FC = ({ model, onClose }) => { + const [currentContext, setCurrentContext] = useState({ + pipelineConfig: undefined, + createPipelineFlyoutOpen: false, + }); + + return ( + + {currentContext.createPipelineFlyoutOpen === false ? ( + + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/test_trained_model_content.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/test_trained_model_content.tsx new file mode 100644 index 0000000000000..52665343cb351 --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/test_trained_model_content.tsx @@ -0,0 +1,115 @@ +/* + * 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, { FC, useState, useMemo } from 'react'; + +import { SUPPORTED_PYTORCH_TASKS } from '@kbn/ml-trained-models-utils'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFormRow, EuiSelect, EuiSpacer, EuiTab, EuiTabs, useEuiPaddingSize } from '@elastic/eui'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { SelectedModel } from './selected_model'; +import { type ModelItem } from '../models_list'; +import { INPUT_TYPE } from './models/inference_base'; +import { useTestTrainedModelsContext } from './test_trained_models_context'; +import { type InferecePipelineCreationState } from '../create_pipeline_for_model/state'; + +interface ContentProps { + model: ModelItem; + handlePipelineConfigUpdate?: (configUpdate: Partial) => void; + externalPipelineConfig?: estypes.IngestPipeline; +} + +export const TestTrainedModelContent: FC = ({ + model, + handlePipelineConfigUpdate, + externalPipelineConfig, +}) => { + const [deploymentId, setDeploymentId] = useState(model.deployment_ids[0]); + const mediumPadding = useEuiPaddingSize('m'); + + const [inputType, setInputType] = useState(INPUT_TYPE.TEXT); + const { + currentContext: { createPipelineFlyoutOpen }, + setCurrentContext, + } = useTestTrainedModelsContext(); + + const onlyShowTab: INPUT_TYPE | undefined = useMemo(() => { + return (model.type ?? []).includes(SUPPORTED_PYTORCH_TASKS.TEXT_EXPANSION) || + createPipelineFlyoutOpen + ? INPUT_TYPE.INDEX + : undefined; + }, [model, createPipelineFlyoutOpen]); + return ( + <> + {' '} + {model.deployment_ids.length > 1 ? ( + <> + + } + > + { + return { text: v, value: v }; + })} + value={deploymentId} + onChange={(e) => { + setDeploymentId(e.target.value); + }} + /> + + + + ) : null} + {onlyShowTab === undefined ? ( + <> + + setInputType(INPUT_TYPE.TEXT)} + > + + + setInputType(INPUT_TYPE.INDEX)} + > + + + + + + + ) : null} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/test_trained_models_context.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/test_trained_models_context.tsx new file mode 100644 index 0000000000000..5db45a334b16a --- /dev/null +++ b/x-pack/plugins/ml/public/application/model_management/test_models/test_trained_models_context.tsx @@ -0,0 +1,32 @@ +/* + * 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 { createContext, Dispatch, useContext } from 'react'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +export interface TestTrainedModelsContextType { + pipelineConfig?: estypes.IngestPipeline; + createPipelineFlyoutOpen: boolean; + defaultSelectedDataViewId?: string; +} +export const TestTrainedModelsContext = createContext< + | { + currentContext: TestTrainedModelsContextType; + setCurrentContext: Dispatch; + } + | undefined +>(undefined); + +export function useTestTrainedModelsContext() { + const testTrainedModelsContext = useContext(TestTrainedModelsContext); + + if (testTrainedModelsContext === undefined) { + throw new Error('TestTrainedModelsContext has not been initialized.'); + } + + return testTrainedModelsContext; +} diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 6acdf51501610..779921068028e 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26820,7 +26820,6 @@ "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.failureMessage": "Impossible de créer le pipeline \"{pipelineName}\".", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.successMessage": "Le pipeline \"{pipelineName}\" a été créé avec succès.", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.createDataViewLabel": "Créer une vue de données", - "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.description": "Ce pipeline est créé en respectant la configuration ci-dessous.", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destinationIndexLabel": "Nom de l'index de destination", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexEmpty": "Entrer un nom d'index de destination valide", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexExists": "Un index portant ce nom existe déjà.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0ed5b8922a49f..27125a6261b28 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26820,7 +26820,6 @@ "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.failureMessage": "'{pipelineName}'を作成できません。", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.successMessage": "'{pipelineName}'は正常に作成されました。", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.createDataViewLabel": "データビューを作成", - "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.description": "このパイプラインは以下の構成で作成されます。", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destinationIndexLabel": "デスティネーションインデックス名", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexEmpty": "有効なデスティネーションインデックス名を入力", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexExists": "この名前のインデックスがすでに存在します。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2def05af32b75..9f4ab71dc071c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -26818,7 +26818,6 @@ "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.failureMessage": "无法创建“{pipelineName}”。", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.create.successMessage": "已成功创建“{pipelineName}”。", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.createDataViewLabel": "创建数据视图", - "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.description": "将使用以下配置创建此管道。", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destinationIndexLabel": "目标索引名称", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexEmpty": "输入有效的目标索引", "xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.destIndexExists": "已存在具有此名称的索引。", From c62790773374a8eeb47c0398c285148004442d60 Mon Sep 17 00:00:00 2001 From: Mykola Harmash Date: Tue, 19 Dec 2023 16:48:35 +0100 Subject: [PATCH 09/95] [ObsUX] Add UI Setting for controling Profiling visibility in Infra (#173294) Closes https://github.com/elastic/kibana/issues/173154 Adds a UI setting to control Infra+Profiling integration from Kibana's Advanced Settings as well as from the Infra Settings screen. Note that the plugin config feature flag is still there because I realized we need it to disable Profiling integration in serverless. https://github.com/elastic/kibana/assets/793851/2a5ace9d-9e18-49a4-be95-c722f24072a7 ### How to test * Make sure profiling is enabled in `kibana.dev.yml` ``` xpack.profiling.enabled: true ``` * Start kibana in traditional mode, go to Infra Settings * Make sure there is the new toggle for Profiling integration and it's on * Go to one of your host's details and make sure you see the profiling tab * Toggle the Profiling integration setting off and check that the tap in host details is not visible * Start kibana in serverless mode * Make sure there is no new setting neither in Infra Settings nor in Advanced Settings * Make sure Profiling tab is not visible in host details --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/advanced-options.asciidoc | 3 ++ .../server/collectors/management/schema.ts | 4 +++ .../server/collectors/management/types.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 6 ++++ .../asset_details/hooks/use_page_header.tsx | 19 +++++++----- .../overview/kpis/cpu_profiling_prompt.tsx | 13 ++++++--- .../use_profiling_integration_setting.ts | 21 ++++++++++++++ .../settings/features_configuration_panel.tsx | 18 +++++++++++- .../source_configuration_settings.tsx | 10 +++++-- x-pack/plugins/observability/common/index.ts | 1 + .../observability/common/ui_settings_keys.ts | 2 ++ .../observability/server/ui_settings.ts | 25 ++++++++++++++-- .../functional/apps/infra/node_details.ts | 29 +++++++++++++++++++ .../functional/page_objects/asset_details.ts | 16 ++++++++++ 14 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 x-pack/plugins/infra/public/hooks/use_profiling_integration_setting.ts diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 03a85d319cf55..c0307253a7208 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -451,6 +451,9 @@ preview:[] When enabled, allows users to create Service Groups from the APM Serv [[observability-apm-trace-explorer-tab]]`observability:apmTraceExplorerTab`:: preview:[] Enable the APM Trace Explorer feature, that allows you to search and inspect traces with KQL or EQL. +[[observability-infrastructure-profiling-integration]]`observability:enableInfrastructureProfilingIntegration`:: +preview:[] Enables the Profiling view in Host details within Infrastructure. + [float] [[kibana-reporting-settings]] ==== Reporting diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 537b7739bcc0d..1fa2407cdd287 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -581,6 +581,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInfrastructureProfilingIntegration': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'securitySolution:enableGroupedNav': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 273864af2bb4a..33518b5389f57 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -46,6 +46,7 @@ export interface UsageStats { 'observability:apmAWSLambdaPriceFactor': string; 'observability:apmAWSLambdaRequestCostPerMillion': number; 'observability:enableInfrastructureHostsView': boolean; + 'observability:enableInfrastructureProfilingIntegration': boolean; 'observability:apmAgentExplorerView': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 8c2a7b13d52ca..ec17c9b9d1a3b 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10061,6 +10061,12 @@ "description": "Non-default value of setting." } }, + "observability:enableInfrastructureProfilingIntegration": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "securitySolution:enableGroupedNav": { "type": "boolean", "_meta": { diff --git a/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx index 3fae1eca66a40..905def3ab0bc0 100644 --- a/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -5,22 +5,23 @@ * 2.0. */ import { - EuiIcon, - type EuiPageHeaderProps, - type EuiBreadcrumbsProps, EuiFlexGroup, EuiFlexItem, + EuiIcon, + type EuiBreadcrumbsProps, + type EuiPageHeaderProps, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import React, { useCallback, useMemo } from 'react'; import { capitalize } from 'lodash'; +import React, { useCallback, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; -import { FormattedMessage } from '@kbn/i18n-react'; import { usePluginConfig } from '../../../containers/plugin_config_context'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { useProfilingIntegrationSetting } from '../../../hooks/use_profiling_integration_setting'; import { APM_HOST_FILTER_FIELD } from '../constants'; import { LinkToAlertsRule, LinkToApmServices, LinkToNodeDetails } from '../links'; -import { ContentTabIds, type RouteState, type LinkOptions, type Tab, type TabIds } from '../types'; +import { ContentTabIds, type LinkOptions, type RouteState, type Tab, type TabIds } from '../types'; import { useAssetDetailsRenderPropsContext } from './use_asset_details_render_props'; import { useTabSwitcherContext } from './use_tab_switcher'; @@ -110,12 +111,14 @@ const useRightSideItems = (links?: LinkOptions[]) => { const useFeatureFlagTabs = () => { const { featureFlags } = usePluginConfig(); + const isProfilingEnabled = useProfilingIntegrationSetting(); + const featureFlagControlledTabs: Partial> = useMemo( () => ({ [ContentTabIds.OSQUERY]: featureFlags.osqueryEnabled, - [ContentTabIds.PROFILING]: featureFlags.profilingEnabled, + [ContentTabIds.PROFILING]: isProfilingEnabled, }), - [featureFlags.osqueryEnabled, featureFlags.profilingEnabled] + [featureFlags.osqueryEnabled, isProfilingEnabled] ); const isTabEnabled = useCallback( diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/cpu_profiling_prompt.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/cpu_profiling_prompt.tsx index afe39963e966d..291b255e7ce33 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/cpu_profiling_prompt.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpis/cpu_profiling_prompt.tsx @@ -10,19 +10,24 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty } from '@elastic/eui'; import { EuiBadge } from '@elastic/eui'; import { EuiFlexGroup } from '@elastic/eui'; -import { usePluginConfig } from '../../../../../containers/plugin_config_context'; +import { useProfilingIntegrationSetting } from '../../../../../hooks/use_profiling_integration_setting'; import { useTabSwitcherContext } from '../../../hooks/use_tab_switcher'; export function CpuProfilingPrompt() { const { showTab } = useTabSwitcherContext(); - const { featureFlags } = usePluginConfig(); + const isProfilingEnabled = useProfilingIntegrationSetting(); - if (!featureFlags.profilingEnabled) { + if (!isProfilingEnabled) { return null; } return ( - + {i18n.translate('xpack.infra.cpuProfilingPrompt.newBadgeLabel', { defaultMessage: 'NEW', diff --git a/x-pack/plugins/infra/public/hooks/use_profiling_integration_setting.ts b/x-pack/plugins/infra/public/hooks/use_profiling_integration_setting.ts new file mode 100644 index 0000000000000..ee9101af55fc2 --- /dev/null +++ b/x-pack/plugins/infra/public/hooks/use_profiling_integration_setting.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 { useUiSetting } from '@kbn/kibana-react-plugin/public'; +import { enableInfrastructureProfilingIntegration } from '@kbn/observability-plugin/common'; +import { usePluginConfig } from '../containers/plugin_config_context'; + +export function useProfilingIntegrationSetting(): boolean { + const { + featureFlags: { profilingEnabled }, + } = usePluginConfig(); + const isProfilingUiSettingEnabled = useUiSetting( + enableInfrastructureProfilingIntegration + ); + + return profilingEnabled && isProfilingUiSettingEnabled; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/settings/features_configuration_panel.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/features_configuration_panel.tsx index 19d7392fb7ca1..aa69ef543c68f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings/features_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/features_configuration_panel.tsx @@ -10,9 +10,13 @@ import { EuiSpacer } from '@elastic/eui'; import { EuiForm } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React from 'react'; -import { enableInfrastructureHostsView } from '@kbn/observability-plugin/common'; +import { + enableInfrastructureHostsView, + enableInfrastructureProfilingIntegration, +} from '@kbn/observability-plugin/common'; import { useEditableSettings } from '@kbn/observability-shared-plugin/public'; import { LazyField } from '@kbn/advanced-settings-plugin/public'; +import { usePluginConfig } from '../../../containers/plugin_config_context'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; type Props = Pick< @@ -31,6 +35,7 @@ export function FeaturesConfigurationPanel({ const { services: { docLinks, notifications }, } = useKibanaContextForPlugin(); + const { featureFlags } = usePluginConfig(); return ( @@ -52,6 +57,17 @@ export function FeaturesConfigurationPanel({ toasts={notifications.toasts} unsavedChanges={unsavedChanges[enableInfrastructureHostsView]} /> + {featureFlags.profilingEnabled && ( + + )} ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx index 5769f861234c4..a064aaf0e151f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/settings/source_configuration_settings.tsx @@ -17,7 +17,10 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback } from 'react'; import { Prompt, useEditableSettings } from '@kbn/observability-shared-plugin/public'; -import { enableInfrastructureHostsView } from '@kbn/observability-plugin/common'; +import { + enableInfrastructureHostsView, + enableInfrastructureProfilingIntegration, +} from '@kbn/observability-plugin/common'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useSourceContext } from '../../../containers/metrics_source'; import { useInfraMLCapabilitiesContext } from '../../../containers/ml/infra_ml_capabilities'; @@ -61,7 +64,10 @@ export const SourceConfigurationSettings = ({ formState, formStateChanges, } = useSourceConfigurationFormState(source && source.configuration); - const infraUiSettings = useEditableSettings('infra_metrics', [enableInfrastructureHostsView]); + const infraUiSettings = useEditableSettings('infra_metrics', [ + enableInfrastructureHostsView, + enableInfrastructureProfilingIntegration, + ]); const resetAllUnsavedChanges = useCallback(() => { resetForm(); diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 3a86e264095bd..143bf77a1b0cc 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -32,6 +32,7 @@ export { apmTraceExplorerTab, apmLabsButton, enableInfrastructureHostsView, + enableInfrastructureProfilingIntegration, enableAwsLambdaMetrics, enableAgentExplorerView, apmAWSLambdaPriceFactor, diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 5745882055cab..bcc2f0451caac 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -17,6 +17,8 @@ export const apmServiceGroupMaxNumberOfServices = export const apmTraceExplorerTab = 'observability:apmTraceExplorerTab'; export const apmLabsButton = 'observability:apmLabsButton'; export const enableInfrastructureHostsView = 'observability:enableInfrastructureHostsView'; +export const enableInfrastructureProfilingIntegration = + 'observability:enableInfrastructureProfilingIntegration'; export const enableAwsLambdaMetrics = 'observability:enableAwsLambdaMetrics'; export const enableAgentExplorerView = 'observability:apmAgentExplorerView'; export const apmAWSLambdaPriceFactor = 'observability:apmAWSLambdaPriceFactor'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 98260fff5f4c7..8029b412ebb6a 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -37,6 +37,7 @@ import { profilingPervCPUWattArm64, profilingAWSCostDiscountRate, profilingCostPervCPUPerHour, + enableInfrastructureProfilingIntegration, } from '../common/ui_settings_keys'; const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', { @@ -236,6 +237,24 @@ export const uiSettings: Record = { }), schema: schema.boolean(), }, + [enableInfrastructureProfilingIntegration]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.enableInfrastructureProfilingIntegration', { + defaultMessage: 'Universal Profiling integration in Infrastructure', + }), + value: true, + description: i18n.translate( + 'xpack.observability.enableInfrastructureProfilingIntegrationDescription', + { + defaultMessage: + '{betaLabel} Enable Universal Profiling integration in the Infrastructure app.', + values: { + betaLabel: `[${betaLabel}]`, + }, + } + ), + schema: schema.boolean(), + }, [enableAwsLambdaMetrics]: { category: [observabilityFeatureId], name: i18n.translate('xpack.observability.enableAwsLambdaMetrics', { @@ -415,9 +434,9 @@ export const uiSettings: Record = { }), value: 1.7, description: i18n.translate('xpack.observability.profilingDatacenterPUEUiSettingDescription', { - defaultMessage: `Data center power usage effectiveness (PUE) measures how efficiently a data center uses energy. Defaults to 1.7, the average on-premise data center PUE according to the {uptimeLink} survey + defaultMessage: `Data center power usage effectiveness (PUE) measures how efficiently a data center uses energy. Defaults to 1.7, the average on-premise data center PUE according to the {uptimeLink} survey

- You can also use the PUE that corresponds with your cloud provider: + You can also use the PUE that corresponds with your cloud provider: