From 66cfc55efc13dbdb6f1dc0319fe131ca2969aaab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Mar 2024 17:15:14 +0000 Subject: [PATCH] Add conditional installation for S3 integrations (#1518) * Add workflows to integration format Signed-off-by: Simeon Widdis * Render integration workflows on frontend Signed-off-by: Simeon Widdis * Add ability to toggle workflows to frontend Signed-off-by: Simeon Widdis * Add workflows to integration build options Signed-off-by: Simeon Widdis * Add asset workflow filtering to builder Signed-off-by: Simeon Widdis * Add enabled workflows to setup request Signed-off-by: Simeon Widdis * Don't allow integration setup if no workflows enabled Signed-off-by: Simeon Widdis * Add workflows to other integrations Signed-off-by: Simeon Widdis * Improve header for workflows section Signed-off-by: Simeon Widdis * Update snapshots Signed-off-by: Simeon Widdis --------- Signed-off-by: Simeon Widdis (cherry picked from commit 8874c8c6ff778221e633fc8c52356a78f878c22c) Signed-off-by: github-actions[bot] --- .../setup_integration.test.tsx.snap | 1247 +++++++++++++++++ .../__tests__/setup_integration.test.tsx | 15 + .../components/create_integration_helpers.ts | 5 +- .../components/setup_integration.tsx | 120 +- .../repository/aws_elb/aws_elb-1.0.0.json | 20 +- .../aws_vpc_flow/aws_vpc_flow-1.0.0.json | 20 +- .../repository/nginx/nginx-1.0.0.json | 20 +- .../integrations/__test__/builder.test.ts | 71 + .../integrations/integrations_adaptor.ts | 3 +- .../integrations/integrations_builder.ts | 59 +- .../integrations/integrations_manager.ts | 4 +- .../repository/integration_reader.ts | 2 + server/adaptors/integrations/types.ts | 13 +- server/adaptors/integrations/validators.ts | 19 + .../integrations/integrations_router.ts | 4 +- test/constants.ts | 8 + 16 files changed, 1579 insertions(+), 51 deletions(-) diff --git a/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap index d23aea4e7d..c77eb11c4b 100644 --- a/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap +++ b/public/components/integrations/components/__tests__/__snapshots__/setup_integration.test.tsx.snap @@ -35,6 +35,7 @@ exports[`Integration Setup Page Renders integration setup page as expected 1`] = "connectionTableName": "sample", "connectionType": "index", "displayName": "sample Integration", + "enabledWorkflows": Array [], } } integration={ @@ -719,6 +720,7 @@ exports[`Integration Setup Page Renders integration setup page as expected 1`] = "connectionTableName": "sample", "connectionType": "index", "displayName": "sample Integration", + "enabledWorkflows": Array [], } } integration={ @@ -1086,6 +1088,1243 @@ exports[`Integration Setup Page Renders the S3 connector form as expected 1`] = "name": "sample", "type": "logs", "version": "2.0.0", + "workflows": Array [ + Object { + "description": "This is a test workflow.", + "enabled_by_default": true, + "label": "Workflow 1", + "name": "workflow1", + }, + ], + } + } + setupCallout={ + Object { + "show": false, + } + } + updateConfig={[Function]} +> + +
+ +

+ Set Up Integration +

+
+ +
+ + +
+ + +
+

+ Integration Details +

+
+
+ +
+ + +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ +
+ + +
+

+ Integration Connection +

+
+
+ +
+ + +
+
+ + + +
+
+ + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+ +
+ Select the type of connection to use for queries. +
+
+
+
+
+ +
+
+ + + +
+
+ +
+ + +
+
+
+ + + ss4o_logs-nginx-test + + + +
+ +
+
+ +
+ +
+ + + + + + + + + + + +
+
+
+
+ + +
+ + +
+ Select a data source to pull the data from. +
+
+
+
+ + +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ Must be at least 1 character. +
+
+ +
+ Select a table name to associate with your data. +
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ +
+
+ + + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ The Checkpoint location must be a unique directory and not the same as the Bucket location. It will be used for caching intermediary results. +
+
+
+
+
+ +
+ + +
+

+ Installation Flows +

+
+
+ +
+ + +
+
+ + + +
+
+ + + <_EuiSplitPanelOuter + className="euiCheckableCard euiCheckableCard-isChecked" + direction="row" + hasBorder={true} + responsive={false} + > + +
+ <_EuiSplitPanelInner + color="primary" + grow={false} + onClick={[Function]} + > + +
+ +
+ +
+
+ +
+ + + <_EuiSplitPanelInner> + +
+ +
+ This is a test workflow. +
+
+
+ +
+
+ + + + +
+ Select from the available asset types based on your use case. Choose at least one. +
+
+
+
+ +
+ + +`; + +exports[`Integration Setup Page Renders the S3 connector form without workflows 1`] = ` + { expect(wrapper).toMatchSnapshot(); }); }); + + it('Renders the S3 connector form without workflows', async () => { + const wrapper = mount( + {}} + integration={{ ...TEST_INTEGRATION_CONFIG, workflows: undefined }} + setupCallout={{ show: false }} + /> + ); + + await waitFor(() => { + expect(wrapper).toMatchSnapshot(); + }); + }); }); diff --git a/public/components/integrations/components/create_integration_helpers.ts b/public/components/integrations/components/create_integration_helpers.ts index b113c6df89..527b8fdb10 100644 --- a/public/components/integrations/components/create_integration_helpers.ts +++ b/public/components/integrations/components/create_integration_helpers.ts @@ -282,7 +282,8 @@ export async function addIntegrationRequest( integration: IntegrationConfig, setToast: (title: string, color?: Color, text?: string | undefined) => void, name?: string, - dataSource?: string + dataSource?: string, + workflows?: string[] ): Promise { const http = coreRefs.http!; if (addSample) { @@ -298,7 +299,7 @@ export async function addIntegrationRequest( let response: boolean = await http .post(`${INTEGRATIONS_BASE}/store/${templateName}`, { - body: JSON.stringify({ name, dataSource }), + body: JSON.stringify({ name, dataSource, workflows }), }) .then((res) => { setToast(`${name} integration successfully added!`, 'success'); diff --git a/public/components/integrations/components/setup_integration.tsx b/public/components/integrations/components/setup_integration.tsx index 9871e1670a..01d6b52a0c 100644 --- a/public/components/integrations/components/setup_integration.tsx +++ b/public/components/integrations/components/setup_integration.tsx @@ -8,6 +8,7 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, + EuiCheckableCard, EuiComboBox, EuiEmptyPrompt, EuiFieldText, @@ -42,6 +43,7 @@ export interface IntegrationSetupInputs { connectionLocation: string; checkpointLocation: string; connectionTableName: string; + enabledWorkflows: string[]; } type SetupCallout = { show: true; title: string; color?: Color; text?: string } | { show: false }; @@ -182,6 +184,38 @@ const runQuery = async ( } }; +export function SetupWorkflowSelector({ + integration, + useWorkflows, + toggleWorkflow, +}: { + integration: IntegrationConfig; + useWorkflows: Map; + toggleWorkflow: (name: string) => void; +}) { + if (!integration.workflows) { + return null; + } + + const cards = integration.workflows.map((workflow) => { + return ( + toggleWorkflow(workflow.name)} + > + {workflow.description} + + ); + }); + + return cards; +} + export function SetupIntegrationForm({ config, updateConfig, @@ -197,6 +231,25 @@ export function SetupIntegrationForm({ const [isBucketBlurred, setIsBucketBlurred] = useState(false); const [isCheckpointBlurred, setIsCheckpointBlurred] = useState(false); + const [useWorkflows, setUseWorkflows] = useState(new Map()); + const toggleWorkflow = (name: string) => { + setUseWorkflows(new Map(useWorkflows.set(name, !useWorkflows.get(name)))); + }; + + useEffect(() => { + if (integration.workflows) { + setUseWorkflows(new Map(integration.workflows.map((w) => [w.name, w.enabled_by_default]))); + } + }, [integration.workflows]); + + useEffect(() => { + updateConfig({ + enabledWorkflows: [...useWorkflows.entries()].filter((w) => w[1]).map((w) => w[0]), + }); + // If we add the updateConfig dep here, rendering crashes with "Maximum update depth exceeded" + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [useWorkflows]); + useEffect(() => { const updateDataSources = async () => { const data = await suggestDataSources(config.connectionType); @@ -339,12 +392,47 @@ export function SetupIntegrationForm({ }} />
+ {integration.workflows ? ( + <> + + +

Installation Flows

+
+ + + + + + ) : null} ) : null} ); } +const prepareQuery = (query: string, config: IntegrationSetupInputs): string => { + let queryStr = query.replaceAll( + '{table_name}', + `${config.connectionDataSource}.default.${config.connectionTableName}` + ); + queryStr = queryStr.replaceAll('{s3_bucket_location}', config.connectionLocation); + queryStr = queryStr.replaceAll('{s3_checkpoint_location}', config.checkpointLocation); + queryStr = queryStr.replaceAll('{object_name}', config.connectionTableName); + queryStr = queryStr.replaceAll(/\s+/g, ' '); + return queryStr; +}; + const addIntegration = async ({ config, integration, @@ -375,22 +463,20 @@ const addIntegration = async ({ } else if (config.connectionType === 's3') { const http = coreRefs.http!; - const assets = await http.get(`${INTEGRATIONS_BASE}/repository/${integration.name}/assets`); + const assets: { data: ParsedIntegrationAsset[] } = await http.get( + `${INTEGRATIONS_BASE}/repository/${integration.name}/assets` + ); - // Queries must exist because we disable s3 if they're not present for (const query of assets.data.filter( - (a: ParsedIntegrationAsset): a is { type: 'query'; query: string; language: string } => + (a: ParsedIntegrationAsset): a is ParsedIntegrationAsset & { type: 'query' } => a.type === 'query' )) { - let queryStr = (query.query as string).replaceAll( - '{table_name}', - `${config.connectionDataSource}.default.${config.connectionTableName}` - ); + // Skip any queries that have conditional workflows but aren't enabled + if (query.workflows && !query.workflows.some((w) => config.enabledWorkflows.includes(w))) { + continue; + } - queryStr = queryStr.replaceAll('{s3_bucket_location}', config.connectionLocation); - queryStr = queryStr.replaceAll('{s3_checkpoint_location}', config.checkpointLocation); - queryStr = queryStr.replaceAll('{object_name}', config.connectionTableName); - queryStr = queryStr.replaceAll(/\s+/g, ' '); + const queryStr = prepareQuery(query.query, config); const result = await runQuery(queryStr, config.connectionDataSource, sessionId); if (!result.ok) { setLoading(false); @@ -400,7 +486,6 @@ const addIntegration = async ({ sessionId = result.value.sessionId ?? sessionId; } // Once everything is ready, add the integration to the new datasource as usual - // TODO determine actual values here after more about queries is known const res = await addIntegrationRequest( false, integration.name, @@ -408,7 +493,8 @@ const addIntegration = async ({ integration, setCalloutLikeToast, config.displayName, - `flint_${config.connectionDataSource}_default_${config.connectionTableName}_mview` + `flint_${config.connectionDataSource}_default_${config.connectionTableName}_mview`, + config.enabledWorkflows ); if (!res) { setLoading(false); @@ -418,11 +504,14 @@ const addIntegration = async ({ } }; -const isConfigValid = (config: IntegrationSetupInputs): boolean => { +const isConfigValid = (config: IntegrationSetupInputs, integration: IntegrationConfig): boolean => { if (config.displayName.length < 1 || config.connectionDataSource.length < 1) { return false; } if (config.connectionType === 's3') { + if (integration.workflows && config.enabledWorkflows.length < 1) { + return false; + } return ( config.connectionLocation.startsWith('s3://') && config.checkpointLocation.startsWith('s3://') ); @@ -477,7 +566,7 @@ export function SetupBottomBar({ iconType="arrowRight" iconSide="right" isLoading={loading} - disabled={!isConfigValid(config)} + disabled={!isConfigValid(config, integration)} onClick={async () => addIntegration({ integration, config, setLoading, setCalloutLikeToast }) } @@ -511,6 +600,7 @@ export function SetupIntegrationPage({ integration }: { integration: string }) { connectionLocation: '', checkpointLocation: '', connectionTableName: integration, + enabledWorkflows: [], }); const [template, setTemplate] = useState({ diff --git a/server/adaptors/integrations/__data__/repository/aws_elb/aws_elb-1.0.0.json b/server/adaptors/integrations/__data__/repository/aws_elb/aws_elb-1.0.0.json index bff1d30de2..f116e45d63 100644 --- a/server/adaptors/integrations/__data__/repository/aws_elb/aws_elb-1.0.0.json +++ b/server/adaptors/integrations/__data__/repository/aws_elb/aws_elb-1.0.0.json @@ -8,6 +8,20 @@ "labels": ["Observability", "Logs", "AWS", "Flint S3", "Cloud"], "author": "OpenSearch", "sourceUrl": "https://github.com/opensearch-project/dashboards-observability/tree/main/server/adaptors/integrations/__data__/repository/aws_elb/info", + "workflows": [ + { + "name": "queries", + "label": "Queries (recommended)", + "description": "Tables and pre-written queries for quickly getting insights on your data.", + "enabled_by_default": true + }, + { + "name": "dashboards", + "label": "Dashboards & Visualizations", + "description": "Dashboards and indices that enable you to easily visualize important metrics.", + "enabled_by_default": false + } + ], "statics": { "logo": { "annotation": "ELB Logo", @@ -51,7 +65,8 @@ "name": "aws_elb", "version": "1.0.0", "extension": "ndjson", - "type": "savedObjectBundle" + "type": "savedObjectBundle", + "workflows": ["dashboards"] }, { "name": "create_table", @@ -63,7 +78,8 @@ "name": "create_mv", "version": "1.0.0", "extension": "sql", - "type": "query" + "type": "query", + "workflows": ["dashboards"] } ], "sampleData": { diff --git a/server/adaptors/integrations/__data__/repository/aws_vpc_flow/aws_vpc_flow-1.0.0.json b/server/adaptors/integrations/__data__/repository/aws_vpc_flow/aws_vpc_flow-1.0.0.json index a445c626ba..11f5132931 100644 --- a/server/adaptors/integrations/__data__/repository/aws_vpc_flow/aws_vpc_flow-1.0.0.json +++ b/server/adaptors/integrations/__data__/repository/aws_vpc_flow/aws_vpc_flow-1.0.0.json @@ -8,6 +8,20 @@ "labels": ["Observability", "Logs", "AWS", "Cloud", "Flint S3"], "author": "Haidong Wang", "sourceUrl": "https://github.com/opensearch-project/dashboards-observability/tree/main/server/adaptors/integrations/__data__/repository/aws_vpc_flow/info", + "workflows": [ + { + "name": "queries", + "label": "Queries (recommended)", + "description": "Tables and pre-written queries for quickly getting insights on your data.", + "enabled_by_default": true + }, + { + "name": "dashboards", + "label": "Dashboards & Visualizations", + "description": "Dashboards and indices that enable you to easily visualize important metrics.", + "enabled_by_default": false + } + ], "statics": { "logo": { "annotation": "AWS VPC Logo", @@ -47,7 +61,8 @@ "name": "aws_vpc_flow", "version": "1.0.0", "extension": "ndjson", - "type": "savedObjectBundle" + "type": "savedObjectBundle", + "workflows": ["dashboards"] }, { "name": "create_table_vpc", @@ -59,7 +74,8 @@ "name": "create_mv_vpc", "version": "1.0.0", "extension": "sql", - "type": "query" + "type": "query", + "workflows": ["dashboards"] } ], "sampleData": { diff --git a/server/adaptors/integrations/__data__/repository/nginx/nginx-1.0.0.json b/server/adaptors/integrations/__data__/repository/nginx/nginx-1.0.0.json index ecc7cdc7b4..e04928f148 100644 --- a/server/adaptors/integrations/__data__/repository/nginx/nginx-1.0.0.json +++ b/server/adaptors/integrations/__data__/repository/nginx/nginx-1.0.0.json @@ -8,6 +8,20 @@ "labels": ["Observability", "Logs", "Flint S3"], "author": "OpenSearch", "sourceUrl": "https://github.com/opensearch-project/dashboards-observability/tree/main/server/adaptors/integrations/__data__/repository/nginx/info", + "workflows": [ + { + "name": "queries", + "label": "Queries (recommended)", + "description": "Tables and pre-written queries for quickly getting insights on your data.", + "enabled_by_default": true + }, + { + "name": "dashboards", + "label": "Dashboards & Visualizations", + "description": "Dashboards and indices that enable you to easily visualize important metrics.", + "enabled_by_default": false + } + ], "statics": { "logo": { "annotation": "NginX Logo", @@ -43,7 +57,8 @@ "name": "nginx", "version": "1.0.0", "extension": "ndjson", - "type": "savedObjectBundle" + "type": "savedObjectBundle", + "workflows": ["dashboards"] }, { "name": "create_table", @@ -55,7 +70,8 @@ "name": "create_mv", "version": "1.0.0", "extension": "sql", - "type": "query" + "type": "query", + "workflows": ["dashboards"] } ], "sampleData": { diff --git a/server/adaptors/integrations/__test__/builder.test.ts b/server/adaptors/integrations/__test__/builder.test.ts index f27f097ef9..1779c096b1 100644 --- a/server/adaptors/integrations/__test__/builder.test.ts +++ b/server/adaptors/integrations/__test__/builder.test.ts @@ -343,3 +343,74 @@ describe('IntegrationInstanceBuilder', () => { }); }); }); + +describe('getSavedObjectBundles', () => { + let builder: IntegrationInstanceBuilder; + + beforeEach(() => { + builder = new IntegrationInstanceBuilder(mockSavedObjectsClient); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should filter assets correctly without workflows and includeWorkflows', () => { + const assets = [ + { + type: 'savedObjectBundle' as const, + data: [{ id: '1', type: 'type1', attributes: { title: 'Title 1' } }], + }, + { + type: 'savedObjectBundle' as const, + data: [{ id: '2', type: 'type2', attributes: { title: 'Title 2' } }], + }, + { type: 'query' as const, query: 'query', language: 'language' }, + ]; + const result = builder.getSavedObjectBundles(assets); + expect(result.length).toBe(2); + }); + + it('should filter assets correctly with specified workflows', () => { + const assets = [ + { + type: 'savedObjectBundle' as const, + workflows: ['workflow1'], + data: [{ id: '1', type: 'type1', attributes: { title: 'Title 1' } }], + }, + { + type: 'savedObjectBundle' as const, + workflows: ['workflow2'], + data: [{ id: '2', type: 'type2', attributes: { title: 'Title 2' } }], + }, + { type: 'query' as const, query: 'query', language: 'language' }, + ]; + const result = builder.getSavedObjectBundles(assets, ['workflow1']); + expect(result.length).toBe(1); + expect(result[0].id).toBe('1'); + }); + + it('should filter assets correctly with no matching workflows', () => { + const assets = [ + { + type: 'savedObjectBundle' as const, + workflows: ['workflow1'], + data: [{ id: '1', type: 'type1', attributes: { title: 'Title 1' } }], + }, + { + type: 'savedObjectBundle' as const, + workflows: ['workflow2'], + data: [{ id: '2', type: 'type2', attributes: { title: 'Title 2' } }], + }, + { type: 'query' as const, query: 'query', language: 'language' }, + ]; + const result = builder.getSavedObjectBundles(assets, ['workflow3']); + expect(result.length).toBe(0); + }); + + it('should return an empty array if no savedObjectBundle assets are present', () => { + const assets = [{ type: 'query' as const, query: 'query', language: 'language' }]; + const result = builder.getSavedObjectBundles(assets); + expect(result.length).toBe(0); + }); +}); diff --git a/server/adaptors/integrations/integrations_adaptor.ts b/server/adaptors/integrations/integrations_adaptor.ts index 108e997b23..4b329a37c0 100644 --- a/server/adaptors/integrations/integrations_adaptor.ts +++ b/server/adaptors/integrations/integrations_adaptor.ts @@ -17,7 +17,8 @@ export interface IntegrationsAdaptor { loadIntegrationInstance: ( templateName: string, name: string, - dataSource: string + dataSource: string, + workflows?: string[] ) => Promise; deleteIntegrationInstance: (id: string) => Promise; diff --git a/server/adaptors/integrations/integrations_builder.ts b/server/adaptors/integrations/integrations_builder.ts index 36fc8f1848..fab905d1bd 100644 --- a/server/adaptors/integrations/integrations_builder.ts +++ b/server/adaptors/integrations/integrations_builder.ts @@ -12,6 +12,7 @@ import { deepCheck } from './repository/utils'; interface BuilderOptions { name: string; dataSource: string; + workflows?: string[]; } interface SavedObject { @@ -28,32 +29,44 @@ export class IntegrationInstanceBuilder { this.client = client; } - build(integration: IntegrationReader, options: BuilderOptions): Promise { - const instance = deepCheck(integration) - .then((result) => { - if (!result.ok) { - return Promise.reject(result.error); + async build( + integration: IntegrationReader, + options: BuilderOptions + ): Promise { + const instance = await deepCheck(integration); + if (!instance.ok) { + return Promise.reject(instance.error); + } + const assets = await integration.getAssets(); + if (!assets.ok) { + return Promise.reject(assets.error); + } + const remapped = this.remapIDs(this.getSavedObjectBundles(assets.value)); + const withDataSource = this.remapDataSource(remapped, options.dataSource); + const refs = await this.postAssets(withDataSource); + const builtInstance = await this.buildInstance(integration, refs, options); + return builtInstance; + } + + getSavedObjectBundles( + assets: ParsedIntegrationAsset[], + includeWorkflows?: string[] + ): SavedObject[] { + return assets + .filter((asset) => { + // At this stage we only care about installing bundles + if (asset.type !== 'savedObjectBundle') { + return false; } - return integration.getAssets(); - }) - .then((assets) => { - if (!assets.ok) { - return Promise.reject(assets.error); + // If no workflows present: default to all workflows + // Otherwise only install if workflow is present + if (!asset.workflows || !includeWorkflows) { + return true; } - return assets.value; + return includeWorkflows.some((w) => asset.workflows?.includes(w)); }) - .then((assets) => - this.remapIDs( - assets - .filter((asset) => asset.type === 'savedObjectBundle') - .map((asset) => (asset as { type: 'savedObjectBundle'; data: object[] }).data) - .flat() as SavedObject[] - ) - ) - .then((assets) => this.remapDataSource(assets, options.dataSource)) - .then((assets) => this.postAssets(assets)) - .then((refs) => this.buildInstance(integration, refs, options)); - return instance; + .map((asset) => (asset as { type: 'savedObjectBundle'; data: object[] }).data) + .flat() as SavedObject[]; } remapDataSource( diff --git a/server/adaptors/integrations/integrations_manager.ts b/server/adaptors/integrations/integrations_manager.ts index 256aa40c8e..c64f761be1 100644 --- a/server/adaptors/integrations/integrations_manager.ts +++ b/server/adaptors/integrations/integrations_manager.ts @@ -157,7 +157,8 @@ export class IntegrationsManager implements IntegrationsAdaptor { loadIntegrationInstance = async ( templateName: string, name: string, - dataSource: string + dataSource: string, + workflows?: string[] ): Promise => { const template = await this.repository.getIntegration(templateName); if (template === null) { @@ -171,6 +172,7 @@ export class IntegrationsManager implements IntegrationsAdaptor { const result = await this.instanceBuilder.build(template, { name, dataSource, + workflows, }); const test = await this.client.create('integration-instance', result); return Promise.resolve({ ...result, id: test.id }); diff --git a/server/adaptors/integrations/repository/integration_reader.ts b/server/adaptors/integrations/repository/integration_reader.ts index 9a09dc8632..98567c01ff 100644 --- a/server/adaptors/integrations/repository/integration_reader.ts +++ b/server/adaptors/integrations/repository/integration_reader.ts @@ -202,12 +202,14 @@ export class IntegrationReader { case 'savedObjectBundle': resultValue.push({ type: 'savedObjectBundle', + workflows: asset.workflows, data: JSON.parse(serializedResult.value.data), }); break; case 'query': resultValue.push({ type: 'query', + workflows: asset.workflows, query: serializedResult.value.data, language: asset.extension, }); diff --git a/server/adaptors/integrations/types.ts b/server/adaptors/integrations/types.ts index ed458b64a2..b6bf933631 100644 --- a/server/adaptors/integrations/types.ts +++ b/server/adaptors/integrations/types.ts @@ -19,6 +19,7 @@ interface IntegrationConfig { author?: string; description?: string; sourceUrl?: string; + workflows?: IntegrationWorkflow[]; statics?: IntegrationStatics; components: IntegrationComponent[]; assets: IntegrationAsset[]; @@ -57,11 +58,19 @@ interface IntegrationAsset { version: string; extension: string; type: SupportedAssetType; + workflows?: string[]; +} + +interface IntegrationWorkflow { + name: string; + label: string; + description: string; + enabled_by_default: boolean; } type ParsedIntegrationAsset = - | { type: 'savedObjectBundle'; data: object[] } - | { type: 'query'; query: string; language: string }; + | { type: 'savedObjectBundle'; workflows?: string[]; data: object[] } + | { type: 'query'; workflows?: string[]; query: string; language: string }; interface SerializedIntegrationAsset extends IntegrationAsset { data: string; diff --git a/server/adaptors/integrations/validators.ts b/server/adaptors/integrations/validators.ts index 73fb8800c9..497e380d10 100644 --- a/server/adaptors/integrations/validators.ts +++ b/server/adaptors/integrations/validators.ts @@ -31,6 +31,20 @@ const templateSchema: JSONSchemaType = { author: { type: 'string', nullable: true }, description: { type: 'string', nullable: true }, sourceUrl: { type: 'string', nullable: true }, + workflows: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + label: { type: 'string' }, + description: { type: 'string' }, + enabled_by_default: { type: 'boolean' }, + }, + required: ['name', 'label', 'description', 'enabled_by_default'], + }, + nullable: true, + }, statics: { type: 'object', properties: { @@ -64,6 +78,11 @@ const templateSchema: JSONSchemaType = { extension: { type: 'string' }, type: { type: 'string' }, data: { type: 'string', nullable: true }, + workflows: { + type: 'array', + items: { type: 'string' }, + nullable: true, + }, }, required: ['name', 'version', 'extension', 'type'], additionalProperties: false, diff --git a/server/routes/integrations/integrations_router.ts b/server/routes/integrations/integrations_router.ts index fba05b7b04..ca30fa0062 100644 --- a/server/routes/integrations/integrations_router.ts +++ b/server/routes/integrations/integrations_router.ts @@ -82,6 +82,7 @@ export function registerIntegrationsRoute(router: IRouter) { body: schema.object({ name: schema.string(), dataSource: schema.string(), + workflows: schema.maybe(schema.arrayOf(schema.string())), }), }, }, @@ -91,7 +92,8 @@ export function registerIntegrationsRoute(router: IRouter) { return a.loadIntegrationInstance( request.params.templateName, request.body.name, - request.body.dataSource + request.body.dataSource, + request.body.workflows ); }); } diff --git a/test/constants.ts b/test/constants.ts index c0eece3fe6..178b65506c 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -574,6 +574,14 @@ export const TEST_INTEGRATION_CONFIG: IntegrationConfig = { version: '2.0.0', license: 'Apache-2.0', type: 'logs', + workflows: [ + { + name: 'workflow1', + label: 'Workflow 1', + description: 'This is a test workflow.', + enabled_by_default: true, + }, + ], components: [ { name: 'logs',