From 52b30b517ddb47d406c9532a00eaf6ce10b83374 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Feb 2025 23:35:48 +0000 Subject: [PATCH] Add functional buttons in form headers; fix query parse bug (#649) * Add test flow button in form headers Signed-off-by: Tyler Ohlsen * Refactor flyout into standalone component Signed-off-by: Tyler Ohlsen * More refactoring and layout updates Signed-off-by: Tyler Ohlsen * Move datasourceversion fetching into custom reusable hook Signed-off-by: Tyler Ohlsen * Remove unnecessary loading state param Signed-off-by: Tyler Ohlsen * More refactoring; set up multi-resource flyout Signed-off-by: Tyler Ohlsen * Get multi resource flyout working and entrypoints in main flow Signed-off-by: Tyler Ohlsen * Fix regex replacement Signed-off-by: Tyler Ohlsen * Add examples for ingest and search Signed-off-by: Tyler Ohlsen * Propagate runtime props to flyout content Signed-off-by: Tyler Ohlsen * Propagate in existing resources tab alsog Signed-off-by: Tyler Ohlsen --------- Signed-off-by: Tyler Ohlsen (cherry picked from commit 4bfd70cc47c41e7791d035c829501cc013d165db) Signed-off-by: github-actions[bot] --- .../workflow_detail/tools/query/query.tsx | 14 +- .../tools/resources/resource_flyout.tsx | 51 +++++ .../resources/resource_flyout_content.tsx | 160 +++++++++++++++ .../resources/resource_list_with_flyout.tsx | 86 +++----- .../tools/resources/resources_flyout.tsx | 192 ++++++++++++++++++ .../workflow_inputs/workflow_inputs.tsx | 86 +++++--- public/pages/workflows/workflows.tsx | 14 +- .../utils/config_to_template_utils.test.tsx | 157 ++++++++------ public/utils/config_to_template_utils.ts | 13 +- public/utils/utils.tsx | 26 ++- 10 files changed, 620 insertions(+), 179 deletions(-) create mode 100644 public/pages/workflow_detail/tools/resources/resource_flyout.tsx create mode 100644 public/pages/workflow_detail/tools/resources/resource_flyout_content.tsx create mode 100644 public/pages/workflow_detail/tools/resources/resources_flyout.tsx diff --git a/public/pages/workflow_detail/tools/query/query.tsx b/public/pages/workflow_detail/tools/query/query.tsx index 514d9e6a..ff1cd77d 100644 --- a/public/pages/workflow_detail/tools/query/query.tsx +++ b/public/pages/workflow_detail/tools/query/query.tsx @@ -36,10 +36,10 @@ import { containsEmptyValues, containsSameValues, getDataSourceId, - getEffectiveVersion, getPlaceholdersFromQuery, getSearchPipelineErrors, injectParameters, + useDataSourceVersion, } from '../../../../utils'; import { QueryParamsList, Results } from '../../../../general_components'; @@ -65,17 +65,7 @@ const SEARCH_OPTIONS = [ export function Query(props: QueryProps) { const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); - const [dataSourceVersion, setDataSourceVersion] = useState< - string | undefined - >(undefined); - useEffect(() => { - async function getVersion() { - if (dataSourceId !== undefined) { - setDataSourceVersion(await getEffectiveVersion(dataSourceId)); - } - } - getVersion(); - }, [dataSourceId]); + const dataSourceVersion = useDataSourceVersion(dataSourceId); const { loading } = useSelector((state: AppState) => state.opensearch); diff --git a/public/pages/workflow_detail/tools/resources/resource_flyout.tsx b/public/pages/workflow_detail/tools/resources/resource_flyout.tsx new file mode 100644 index 00000000..e54a4631 --- /dev/null +++ b/public/pages/workflow_detail/tools/resources/resource_flyout.tsx @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, +} from '@elastic/eui'; +import { WorkflowResource } from '../../../../../common'; +import { ResourceFlyoutContent } from './resource_flyout_content'; + +interface ResourceFlyoutProps { + resource: WorkflowResource; + resourceDetails: string; + onClose: () => void; + errorMessage?: string; + indexName?: string; + searchPipelineName?: string; + ingestPipelineName?: string; + searchQuery?: string; +} + +/** + * A simple flyout to display details for a particular workflow resource. + */ +export function ResourceFlyout(props: ResourceFlyoutProps) { + return ( + + + +

Resource details

+
+
+ + + +
+ ); +} diff --git a/public/pages/workflow_detail/tools/resources/resource_flyout_content.tsx b/public/pages/workflow_detail/tools/resources/resource_flyout_content.tsx new file mode 100644 index 00000000..50d767ae --- /dev/null +++ b/public/pages/workflow_detail/tools/resources/resource_flyout_content.tsx @@ -0,0 +1,160 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiEmptyPrompt, + EuiHealth, + EuiSpacer, + EuiLink, +} from '@elastic/eui'; +import { + BULK_API_DOCS_LINK, + SEARCH_PIPELINE_DOCS_LINK, + WORKFLOW_STEP_TYPE, + WorkflowResource, +} from '../../../../../common'; + +interface ResourceFlyoutContentProps { + resource: WorkflowResource; + resourceDetails: string; + errorMessage?: string; + indexName?: string; + searchPipelineName?: string; + ingestPipelineName?: string; + searchQuery?: string; +} + +/** + * The static flyout content for a particular workflow resource. + */ +export function ResourceFlyoutContent(props: ResourceFlyoutContentProps) { + return ( + + + +

Name

+
+
+ + {props.resource?.id || ''} + + + + +

Status

+
+
+ + Active + + + + +

+ {props.resource?.stepType === + WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE + ? 'Configuration' + : 'Pipeline configuration'} +

+
+
+ + {!props.errorMessage ? ( + + {props.resourceDetails} + + ) : ( + Error loading resource details} + body={

{props.errorMessage}

} + /> + )} +
+ + + +

+ {props.resource?.stepType === + WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE + ? 'Ingest additional data using the bulk API' + : props.resource?.stepType === + WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE + ? 'Ingest additional data using the bulk API' + : 'Apply a search pipeline to your applications'} +

+
+
+ {props.resource?.stepType === + WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE ? ( + + +

+ You can invoke the search pipeline API in your applications.{' '} + + Learn more + +

+
+
+ ) : ( + + +

+ You can ingest a larger amount of data using the Bulk API.{' '} + + Learn more + +

+
+
+ )} + {props.resource?.stepType === + WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE ? ( + + + {`GET /${props.indexName || 'my_index'}/_search?search_pipeline=${ + props.searchPipelineName || 'my_pipeline' + }` + + `${ + props.searchQuery + ? `\n${props.searchQuery}` + : ` +{ + "query": { + "term": { + "item_text": { + "value": "{{query_text}}" + } + } + } +}` + }`} + + + ) : ( + + + {`POST _bulk +{ "index": { "_index": "${props.indexName || 'my_index'}", "_id": "abc123" } } +{ "my_field_1": "my_field_value_1", "my_field_2": "my_field_value_2" }`} + + + )} +
+ ); +} diff --git a/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx b/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx index fecbead9..b5cde6e0 100644 --- a/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx +++ b/public/pages/workflow_detail/tools/resources/resource_list_with_flyout.tsx @@ -5,23 +5,17 @@ import React, { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; +import { getIn, useFormikContext } from 'formik'; import { Direction, - EuiCodeBlock, EuiFlexGroup, EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, EuiInMemoryTable, - EuiTitle, EuiIcon, - EuiText, - EuiEmptyPrompt, - EuiLoadingSpinner, } from '@elastic/eui'; import { Workflow, + WorkflowFormValues, WorkflowResource, customStringify, } from '../../../../../common'; @@ -38,22 +32,28 @@ import { getErrorMessageForStepType, } from '../../../../utils'; import { columns } from './columns'; +import { ResourceFlyout } from './resource_flyout'; interface ResourceListFlyoutProps { workflow?: Workflow; } /** - * The searchable list of resources for a particular workflow. + * The searchable list of resources for a particular workflow. Each resource has an "inspect" + * action to view more details within a flyout. */ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { - const [allResources, setAllResources] = useState([]); const dispatch = useAppDispatch(); + const { values } = useFormikContext(); + const [allResources, setAllResources] = useState([]); const dataSourceId = getDataSourceId(); - const [resourceDetails, setResourceDetails] = useState(null); - const [rowErrorMessage, setRowErrorMessage] = useState(null); + const [resourceDetails, setResourceDetails] = useState( + undefined + ); + const [rowErrorMessage, setRowErrorMessage] = useState( + undefined + ); const { - loading, getIndexErrorMessage, getIngestPipelineErrorMessage, getSearchPipelineErrorMessage, @@ -112,7 +112,7 @@ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { }, }; - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); const [selectedRowData, setSelectedRowData] = useState< WorkflowResource | undefined >(undefined); @@ -140,7 +140,7 @@ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { const closeFlyout = () => { setIsFlyoutVisible(false); setSelectedRowData(undefined); - setResourceDetails(null); + setResourceDetails(undefined); }; return ( @@ -172,48 +172,20 @@ export function ResourceListWithFlyout(props: ResourceListFlyoutProps) { /> - {isFlyoutVisible && ( - - - -

{selectedRowData?.id}

-
-
- - - - -

Resource details

-
-
- - {!rowErrorMessage && !loading ? ( - - {resourceDetails} - - ) : loading ? ( - } - title={

Loading

} - /> - ) : ( - Error loading resource details} - body={

{rowErrorMessage}

} - /> - )} -
-
-
-
- )} + {isFlyoutVisible && + selectedRowData !== undefined && + resourceDetails !== undefined && ( + + )} ); } diff --git a/public/pages/workflow_detail/tools/resources/resources_flyout.tsx b/public/pages/workflow_detail/tools/resources/resources_flyout.tsx new file mode 100644 index 00000000..398735e3 --- /dev/null +++ b/public/pages/workflow_detail/tools/resources/resources_flyout.tsx @@ -0,0 +1,192 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { get } from 'lodash'; +import { + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTab, + EuiTabs, + EuiTitle, +} from '@elastic/eui'; +import { + CONFIG_STEP, + customStringify, + WORKFLOW_RESOURCE_TYPE, + WORKFLOW_STEP_TYPE, + WorkflowResource, +} from '../../../../../common'; +import { ResourceFlyoutContent } from './resource_flyout_content'; +import { extractIdsByStepType, getDataSourceId } from '../../../../utils'; +import { + AppState, + getIndex, + getIngestPipeline, + getSearchPipeline, + useAppDispatch, +} from '../../../../store'; + +interface ResourcesFlyoutProps { + resources: WorkflowResource[]; + selectedStep: CONFIG_STEP; + onClose: () => void; + indexName?: string; + searchPipelineName?: string; + ingestPipelineName?: string; + searchQuery?: string; +} + +/** + * Flyout to display details for multiple workflow resources, depending on the context (ingest or search). + * Dynamically render data nested under tabs if there are multiple - e.g., an index and ingest pipeline under the ingest context. + */ +export function ResourcesFlyout(props: ResourcesFlyoutProps) { + const dispatch = useAppDispatch(); + const dataSourceId = getDataSourceId(); + + const { + getIndexErrorMessage, + getIngestPipelineErrorMessage, + getSearchPipelineErrorMessage, + indexDetails, + ingestPipelineDetails, + searchPipelineDetails, + } = useSelector((state: AppState) => state.opensearch); + + // Fetch the filtered resource(s) based on ingest or search context + const [allResources, setAllResources] = useState([]); + useEffect(() => { + if (props.resources) { + const resourcesMap = {} as { [id: string]: WorkflowResource }; + props.resources.forEach((resource) => { + if ( + (props.selectedStep === CONFIG_STEP.INGEST && + (resource.stepType === WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE || + resource.stepType === + WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE)) || + (props.selectedStep === CONFIG_STEP.SEARCH && + resource.stepType === + WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE) + ) { + resourcesMap[resource.id] = resource; + } + }); + setAllResources(Object.values(resourcesMap || {})); + } + }, [props.resources]); + + // fetch details for each resource type + useEffect(() => { + const { + indexIds, + ingestPipelineIds, + searchPipelineIds, + } = extractIdsByStepType(allResources); + if (indexIds) { + try { + dispatch(getIndex({ index: indexIds, dataSourceId })); + } catch {} + } + if (ingestPipelineIds) { + try { + dispatch( + getIngestPipeline({ pipelineId: ingestPipelineIds, dataSourceId }) + ); + } catch {} + } + if (searchPipelineIds) { + try { + dispatch( + getSearchPipeline({ pipelineId: searchPipelineIds, dataSourceId }) + ); + } catch {} + } + }, [allResources]); + + // keep state for the resource index, and the selected tab ID (if applicable) + const [selectedResourceIdx, setSelectedResourceIdx] = useState(0); + const [selectedTabId, setSelectedTabId] = useState(''); + useEffect(() => { + if (allResources) { + setSelectedTabId(get(allResources, `0.id`)); + } + }, [allResources]); + + // get the resource details, and any error message, based on the selected resource index + const selectedResource = get(allResources, selectedResourceIdx, undefined) as + | WorkflowResource + | undefined; + const selectedResourceDetailsObj = + indexDetails[selectedResource?.id || ''] ?? + ingestPipelineDetails[selectedResource?.id || ''] ?? + searchPipelineDetails[selectedResource?.id || ''] ?? + ''; + const selectedResourceDetails = customStringify({ + [selectedResource?.id || '']: selectedResourceDetailsObj, + }); + const selectedResourceErrorMessage = + selectedResource?.stepType === WORKFLOW_STEP_TYPE.CREATE_INDEX_STEP_TYPE + ? getIndexErrorMessage + : selectedResource?.stepType === + WORKFLOW_STEP_TYPE.CREATE_INGEST_PIPELINE_STEP_TYPE + ? getIngestPipelineErrorMessage + : selectedResource?.stepType === + WORKFLOW_STEP_TYPE.CREATE_SEARCH_PIPELINE_STEP_TYPE + ? getSearchPipelineErrorMessage + : (undefined as string | undefined); + + return ( + + + +

Resource details

+
+
+ + {allResources.length > 1 && ( + + + {allResources?.map((tab, idx) => { + return ( + { + setSelectedTabId(tab.id); + setSelectedResourceIdx(idx); + }} + isSelected={tab.id === selectedTabId} + disabled={false} + key={idx} + > + {tab?.type === WORKFLOW_RESOURCE_TYPE.INDEX_NAME + ? 'Index' + : 'Pipeline'} + + ); + })} + + + + )} + {selectedResource !== undefined && + selectedResourceDetails !== undefined && ( + + )} + +
+ ); +} diff --git a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx index 7a60e3fd..35252a21 100644 --- a/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx +++ b/public/pages/workflow_detail/workflow_inputs/workflow_inputs.tsx @@ -6,7 +6,6 @@ import React, { useEffect, useState } from 'react'; import { getIn, useFormikContext } from 'formik'; import { isEmpty, isEqual } from 'lodash'; -import semver from 'semver'; import { EuiSmallButton, EuiSmallButtonEmpty, @@ -20,11 +19,11 @@ import { EuiBottomBar, EuiIconTip, EuiSmallButtonIcon, + EuiButtonEmpty, } from '@elastic/eui'; import { CONFIG_STEP, CachedFormikState, - MINIMUM_FULL_SUPPORTED_VERSION, SimulateIngestPipelineResponseVerbose, TemplateNode, WORKFLOW_STEP_TYPE, @@ -58,11 +57,13 @@ import { getDataSourceId, prepareDocsForSimulate, getIngestPipelineErrors, - getEffectiveVersion, sleep, + useDataSourceVersion, + getIsPreV219, } from '../../../utils'; import { BooleanField } from './input_fields'; import '../workspace/workspace-styles.scss'; +import { ResourcesFlyout } from '../tools/resources/resources_flyout'; interface WorkflowInputsProps { workflow: Workflow | undefined; @@ -101,21 +102,8 @@ export function WorkflowInputs(props: WorkflowInputsProps) { } = useFormikContext(); const dispatch = useAppDispatch(); const dataSourceId = getDataSourceId(); - const [dataSourceVersion, setDataSourceVersion] = useState< - string | undefined - >(undefined); - useEffect(() => { - async function getVersion() { - if (dataSourceId !== undefined) { - setDataSourceVersion(await getEffectiveVersion(dataSourceId)); - } - } - getVersion(); - }, [dataSourceId]); - const isPreV219 = - dataSourceVersion !== undefined - ? semver.lt(dataSourceVersion, MINIMUM_FULL_SUPPORTED_VERSION) - : false; + const dataSourceVersion = useDataSourceVersion(dataSourceId); + const isPreV219 = getIsPreV219(dataSourceVersion); // transient running states const [isUpdatingSearchPipeline, setIsUpdatingSearchPipeline] = useState< @@ -131,6 +119,11 @@ export function WorkflowInputs(props: WorkflowInputsProps) { undefined ); + // resource details state + const [resourcesFlyoutOpen, setResourcesFlyoutOpen] = useState( + false + ); + // maintain global states const onIngest = props.selectedStep === CONFIG_STEP.INGEST; const onSearch = props.selectedStep === CONFIG_STEP.SEARCH; @@ -697,6 +690,17 @@ export function WorkflowInputs(props: WorkflowInputsProps) { className="workspace-panel" borderRadius="l" > + {resourcesFlyoutOpen && ( + setResourcesFlyoutOpen(false)} + indexName={getIn(values, 'ingest.index.name')} + ingestPipelineName={getIn(values, 'ingest.pipelineName')} + searchPipelineName={getIn(values, 'search.pipelineName')} + searchQuery={getIn(values, 'search.request')} + /> + )} {props.uiConfig === undefined ? ( ) : ( @@ -716,15 +720,43 @@ export function WorkflowInputs(props: WorkflowInputsProps) {

{onIngest ? 'Ingest flow' : 'Search flow'}

- {onIngestAndUnprovisioned && ( - - - - )} + + + {onIngestAndUnprovisioned && ( + + + + )} + {(ingestProvisioned || searchProvisioned) && ( + + props.displaySearchPanel()} + > + Test flow + + + )} + {((onIngest && ingestProvisioned) || + (onSearch && searchProvisioned)) && + props.workflow?.resourcesCreated !== undefined && ( + + setResourcesFlyoutOpen(true)} + > + Details + + + )} + + ( queryParams.dataSourceId ); - const [dataSourceVersion, setDataSourceVersion] = useState< - string | undefined - >(undefined); - useEffect(() => { - async function getVersion() { - if (dataSourceId !== undefined) { - setDataSourceVersion(await getEffectiveVersion(dataSourceId)); - } - } - getVersion(); - }, [dataSourceId]); + const dataSourceVersion = useDataSourceVersion(dataSourceId); const { workflows, loading } = useSelector( (state: AppState) => state.workflows ); diff --git a/public/utils/config_to_template_utils.test.tsx b/public/utils/config_to_template_utils.test.tsx index e594bd24..77ec69a4 100644 --- a/public/utils/config_to_template_utils.test.tsx +++ b/public/utils/config_to_template_utils.test.tsx @@ -10,110 +10,141 @@ describe('config_to_template_utils', () => { beforeEach(() => {}); describe('updatePathForExpandedQuery', () => { test('term query', () => { - expect( - updatePathForExpandedQuery('query.term.a') === 'query.term.a.value' + expect(updatePathForExpandedQuery('query.term.a')).toEqual( + 'query.term.a.value' ); - expect( - updatePathForExpandedQuery('query.term.a.value') === - 'query.term.a.value' + expect(updatePathForExpandedQuery('query.term.a.value')).toEqual( + 'query.term.a.value' ); - expect( - updatePathForExpandedQuery('$.query.term.a') === '$.query.term.a.value' + expect(updatePathForExpandedQuery('query.term.abc')).toEqual( + 'query.term.abc.value' ); - expect( - updatePathForExpandedQuery('a.b.c.d.query.term.a') === - 'a.b.c.d.query.term.a.value' + expect(updatePathForExpandedQuery('query.term.abc.value')).toEqual( + 'query.term.abc.value' ); - expect( - updatePathForExpandedQuery('query.bool.must[0].term.a') === - 'query.bool.must[0].term.a.value' + expect(updatePathForExpandedQuery('$.query.term.abc.value')).toEqual( + '$.query.term.abc.value' ); + expect(updatePathForExpandedQuery('query.bool.must[0].term.abc')).toEqual( + 'query.bool.must[0].term.abc.value' + ); + expect( + updatePathForExpandedQuery('query.bool.must[0].term.ab_c') + ).toEqual('query.bool.must[0].term.ab_c.value'); }); test('prefix query', () => { - expect( - updatePathForExpandedQuery('query.prefix.a') === 'query.prefix.a.value' + expect(updatePathForExpandedQuery('query.prefix.a')).toEqual( + 'query.prefix.a.value' ); - expect( - updatePathForExpandedQuery('query.prefix.a.value') === - 'query.prefix.a.value' + expect(updatePathForExpandedQuery('query.prefix.a.value')).toEqual( + 'query.prefix.a.value' + ); + expect(updatePathForExpandedQuery('query.prefix.abc')).toEqual( + 'query.prefix.abc.value' + ); + expect(updatePathForExpandedQuery('query.prefix.abc.value')).toEqual( + 'query.prefix.abc.value' ); }); test('fuzzy query', () => { - expect( - updatePathForExpandedQuery('query.fuzzy.a') === 'query.fuzzy.a.value' + expect(updatePathForExpandedQuery('query.fuzzy.a')).toEqual( + 'query.fuzzy.a.value' ); - expect( - updatePathForExpandedQuery('query.fuzzy.a.value') === - 'query.fuzzy.a.value' + expect(updatePathForExpandedQuery('query.fuzzy.a.value')).toEqual( + 'query.fuzzy.a.value' + ); + expect(updatePathForExpandedQuery('query.fuzzy.abc')).toEqual( + 'query.fuzzy.abc.value' + ); + expect(updatePathForExpandedQuery('query.fuzzy.abc.value')).toEqual( + 'query.fuzzy.abc.value' ); }); test('wildcard query', () => { - expect( - updatePathForExpandedQuery('query.wildcard.a') === - 'query.wildcard.a.wildcard' + expect(updatePathForExpandedQuery('query.wildcard.a')).toEqual( + 'query.wildcard.a.wildcard' ); - expect( - updatePathForExpandedQuery('query.wildcard.a.wildcard') === - 'query.wildcard.a.wildcard' + expect(updatePathForExpandedQuery('query.wildcard.a.wildcard')).toEqual( + 'query.wildcard.a.wildcard' + ); + expect(updatePathForExpandedQuery('query.wildcard.abc')).toEqual( + 'query.wildcard.abc.wildcard' + ); + expect(updatePathForExpandedQuery('query.wildcard.abc.wildcard')).toEqual( + 'query.wildcard.abc.wildcard' ); }); test('regexp query', () => { - expect( - updatePathForExpandedQuery('query.regexp.a') === 'query.regexp.a.value' + expect(updatePathForExpandedQuery('query.regexp.a')).toEqual( + 'query.regexp.a.value' ); - expect( - updatePathForExpandedQuery('query.regexp.a.value') === - 'query.regexp.a.value' + expect(updatePathForExpandedQuery('query.regexp.a.value')).toEqual( + 'query.regexp.a.value' + ); + expect(updatePathForExpandedQuery('query.regexp.abc')).toEqual( + 'query.regexp.abc.value' + ); + expect(updatePathForExpandedQuery('query.regexp.abc.value')).toEqual( + 'query.regexp.abc.value' ); }); test('match query', () => { - expect( - updatePathForExpandedQuery('query.match.a') === 'query.match.a.query' + expect(updatePathForExpandedQuery('query.match.a')).toEqual( + 'query.match.a.query' ); - expect( - updatePathForExpandedQuery('query.match.a.query') === - 'query.match.a.query' + expect(updatePathForExpandedQuery('query.match.a.query')).toEqual( + 'query.match.a.query' + ); + expect(updatePathForExpandedQuery('query.match.item_text')).toEqual( + 'query.match.item_text.query' + ); + expect(updatePathForExpandedQuery('query.match.item_text.query')).toEqual( + 'query.match.item_text.query' ); }); test('match bool prefix query', () => { expect( - updatePathForExpandedQuery('query.match_bool_prefix.a') === - 'query.match_bool_prefix.a.query' - ); + updatePathForExpandedQuery('query.match_bool_prefix.a.query') + ).toEqual('query.match_bool_prefix.a.query'); expect( - updatePathForExpandedQuery('query.match_bool_prefix.a.query') === - 'query.match_bool_prefix.a.query' - ); + updatePathForExpandedQuery('query.match_bool_prefix.item_text') + ).toEqual('query.match_bool_prefix.item_text.query'); + expect( + updatePathForExpandedQuery('query.match_bool_prefix.item_text.query') + ).toEqual('query.match_bool_prefix.item_text.query'); }); test('match phrase query', () => { - expect( - updatePathForExpandedQuery('query.match_phrase.a') === - 'query.match_phrase.a.query' + expect(updatePathForExpandedQuery('query.match_phrase.a.query')).toEqual( + 'query.match_phrase.a.query' ); expect( - updatePathForExpandedQuery('query.match_phrase.a.query') === - 'query.match_phrase.a.query' - ); + updatePathForExpandedQuery('query.match_phrase.item_text') + ).toEqual('query.match_phrase.item_text.query'); + expect( + updatePathForExpandedQuery('query.match_phrase.item_text.query') + ).toEqual('query.match_phrase.item_text.query'); }); test('match phrase prefix query', () => { expect( - updatePathForExpandedQuery('query.match_phrase_prefix.a') === - 'query.match_phrase_prefix.a.query' - ); + updatePathForExpandedQuery('query.match_phrase_prefix.a.query') + ).toEqual('query.match_phrase_prefix.a.query'); expect( - updatePathForExpandedQuery('query.match_phrase_prefix.a.query') === - 'query.match_phrase_prefix.a.query' - ); + updatePathForExpandedQuery('query.match_phrase_prefix.item_text') + ).toEqual('query.match_phrase_prefix.item_text.query'); + expect( + updatePathForExpandedQuery('query.match_phrase_prefix.item_text.query') + ).toEqual('query.match_phrase_prefix.item_text.query'); }); test('aggs query', () => { - expect( - updatePathForExpandedQuery('aggs.avg_a.avg.field') === - 'aggregations.avg_a.avg.field' + expect(updatePathForExpandedQuery('aggs.avg_a.avg.field')).toEqual( + 'aggregations.avg_a.avg.field' ); expect( - updatePathForExpandedQuery('aggs.b.c.d.aggs.avg_a.avg.field') === - 'aggregations.b.c.d.aggregations.avg_a.avg.field' - ); + updatePathForExpandedQuery('aggs.b.c.d.aggs.avg_a.avg.field') + ).toEqual('aggregations.b.c.d.aggregations.avg_a.avg.field'); + expect( + updatePathForExpandedQuery('aggs.b.c.d_e_f.aggs.avg_a.avg.field_abc') + ).toEqual('aggregations.b.c.d_e_f.aggregations.avg_a.avg.field_abc'); }); }); }); diff --git a/public/utils/config_to_template_utils.ts b/public/utils/config_to_template_utils.ts index fb305eb3..a1f7123b 100644 --- a/public/utils/config_to_template_utils.ts +++ b/public/utils/config_to_template_utils.ts @@ -705,17 +705,16 @@ export function updatePathForExpandedQuery(path: string): string { // If the path already has the suffix present, do nothing. function addSuffixToPath(path: string, prefix: string, suffix: string): string { function generateRegex(prefix: string, suffix: string): RegExp { - // match the specified prefix, followed by some value in dot or bracket notation - const notationPattern = `\\b${prefix}\\b(\\.\\w+|\\[\\w+\\])`; - // ensure the suffix (in dot or bracket notation) is not present - const suffixPattern = `(?!(\\.${suffix}|\\[${suffix}\\]))`; - return new RegExp(notationPattern + suffixPattern, 'g'); + // ensure the suffix (in dot or bracket notation) is not present, and match + // on the prefix, followed by some value in dot notation + const finalPattern = `(?!.*(\\.\\b${suffix}\\b|\\[\\b${suffix}\\b\\])).*\\b${prefix}\\b\\.(.+)`; + return new RegExp(finalPattern, 'g'); } // if the pattern matches, append the appropriate suffix via dot notation const regexPattern = generateRegex(prefix, suffix); - return path.replace(regexPattern, (_, subStr) => { - return `${prefix}${subStr}.${suffix}`; + return path.replace(regexPattern, (pattern) => { + return `${pattern}.${suffix}`; }); } diff --git a/public/utils/utils.tsx b/public/utils/utils.tsx index f59d22fb..30613db8 100644 --- a/public/utils/utils.tsx +++ b/public/utils/utils.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useEffect, useState } from 'react'; import yaml from 'js-yaml'; import jsonpath from 'jsonpath'; import { capitalize, escape, findKey, get, isEmpty, set, unset } from 'lodash'; @@ -43,6 +43,7 @@ import { MODEL_ID_PATTERN, WORKFLOW_TYPE, MIN_SUPPORTED_VERSION, + MINIMUM_FULL_SUPPORTED_VERSION, } from '../../common'; import { getCore, @@ -551,6 +552,29 @@ export const getDataSourceId = () => { return mdsQueryParams.dataSourceId; }; +export function useDataSourceVersion( + dataSourceId: string | undefined +): string | undefined { + const [dataSourceVersion, setDataSourceVersion] = useState< + string | undefined + >(undefined); + useEffect(() => { + async function getVersion() { + if (dataSourceId !== undefined) { + setDataSourceVersion(await getEffectiveVersion(dataSourceId)); + } + } + getVersion(); + }, [dataSourceId]); + return dataSourceVersion; +} + +export function getIsPreV219(dataSourceVersion: string | undefined): boolean { + return dataSourceVersion !== undefined + ? semver.lt(dataSourceVersion, MINIMUM_FULL_SUPPORTED_VERSION) + : false; +} + export const isDataSourceReady = (dataSourceId?: string) => { const dataSourceEnabled = getDataSourceEnabled().enabled; return !dataSourceEnabled || dataSourceId !== undefined;