Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,7 @@
"QHdKnm": "Drag and connect nodes to transform",
"QIzNzB": "Toggle the agent log panel.",
"QKC8fv": "Enter variable name",
"QMuDPI": "Select workflow with an Agent loop",
"QMyMOI": "Description",
"QNfUf/": "Full screen",
"QT4IaP": "Filtered!",
Expand Down Expand Up @@ -2714,6 +2715,7 @@
"_QHdKnm.comment": "default placeholder text",
"_QIzNzB.comment": "Toggle the agent log panel aria label text",
"_QKC8fv.comment": "Placeholder for variable name",
"_QMuDPI.comment": "Select workflow with an Agent loop",
"_QMyMOI.comment": "Description label",
"_QNfUf/.comment": "Full Screen token picker",
"_QT4IaP.comment": "Filtered text",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ const getDesignerServices = (
...defaultServiceParams,
clientSupportedOperations: [
['/connectionProviders/workflow', 'invokeWorkflow'],
['/connectionProviders/workflow', 'invokenestedagent'],
['connectionProviders/xmlOperations', 'xmlValidation'],
['connectionProviders/xmlOperations', 'xmlTransform'],
['connectionProviders/liquidOperations', 'liquidJsonToJson'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ const getDesignerServices = (
...defaultServiceParams,
clientSupportedOperations: [
['/connectionProviders/workflow', 'invokeWorkflow'],
['/connectionProviders/workflow', 'invokenestedagent'],
['connectionProviders/xmlOperations', 'xmlValidation'],
['connectionProviders/xmlOperations', 'xmlTransform'],
['connectionProviders/liquidOperations', 'liquidJsonToJson'],
Expand Down
1 change: 1 addition & 0 deletions apps/Standalone/src/templates/app/TemplatesConsumption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ const getResourceBasedServices = (
...defaultServiceParams,
clientSupportedOperations: [
['/connectionProviders/workflow', 'invokeWorkflow'],
['/connectionProviders/workflow', 'invokenestedagent'],
['connectionProviders/xmlOperations', 'xmlValidation'],
['connectionProviders/xmlOperations', 'xmlTransform'],
['connectionProviders/liquidOperations', 'liquidJsonToJson'],
Expand Down
1 change: 1 addition & 0 deletions libs/designer-v2/src/lib/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ export default {
SELECT_BATCH_WORKFLOW_ACTION: 'sendtobatch',
SELECT_BATCH_WORKFLOW_TRIGGER: 'sendtobatchtrigger',
SELECT_MANUAL_WORKFLOW_ACTION: 'invokeworkflow',
SELECT_NESTED_AGENT_WORKFLOW_ACTION: 'invokenestedagent',
},
CHANNELS: {
INPUT: '-inputchannel-',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export const AzureResourceSelection = (props: AzureResourceSelectionProps) => {
id: 'DXwS7e',
description: "Select workflow with 'An HTTP Request' trigger",
});
const nestedAgentWorkflowTitleText = intl.formatMessage({
defaultMessage: 'Select workflow with an Agent loop',
id: 'QMuDPI',
description: 'Select workflow with an Agent loop',
});
const batchWorkflowTitleText = intl.formatMessage({
defaultMessage: 'Select a Batch Workflow resource',
id: 'gvDMuq',
Expand Down Expand Up @@ -213,6 +218,21 @@ export const AzureResourceSelection = (props: AzureResourceSelectionProps) => {
break;
}

case Constants.AZURE_RESOURCE_ACTION_TYPES.SELECT_NESTED_AGENT_WORKFLOW_ACTION: {
setTitleText(nestedAgentWorkflowTitleText);
setResourceTypes(['agentWorkflow']);
setGetResourcesCallbacks(() => [() => SearchService().getAgentWorkflows?.()]);
setSubmitCallback(() => () => {
addResourceOperation({
name: getResourceName(selectedResources[0]),
presetParameterValues: {
'host.workflow.id': selectedResources[0].id,
},
});
});
break;
}

case Constants.AZURE_RESOURCE_ACTION_TYPES.SELECT_BATCH_WORKFLOW_ACTION: {
setTitleText(batchWorkflowTitleText);
setResourceTypes(['batchWorkflow', 'trigger']);
Expand Down Expand Up @@ -244,6 +264,7 @@ export const AzureResourceSelection = (props: AzureResourceSelectionProps) => {
swaggerFunctionAppTitleText,
getOptionsFromPaths,
manualWorkflowTitleText,
nestedAgentWorkflowTitleText,
operation.id,
selectedResources,
]);
Expand Down
1 change: 1 addition & 0 deletions libs/designer/src/lib/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,7 @@ export default {
SELECT_BATCH_WORKFLOW_ACTION: 'sendtobatch',
SELECT_BATCH_WORKFLOW_TRIGGER: 'sendtobatchtrigger',
SELECT_MANUAL_WORKFLOW_ACTION: 'invokeworkflow',
SELECT_NESTED_AGENT_WORKFLOW_ACTION: 'invokenestedagent',
},
CHANNELS: {
INPUT: '-inputchannel-',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export const AzureResourceSelection = (props: AzureResourceSelectionProps) => {
id: 'DXwS7e',
description: "Select workflow with 'An HTTP Request' trigger",
});
const nestedAgentWorkflowTitleText = intl.formatMessage({
defaultMessage: 'Select workflow with an Agent loop',
id: 'QMuDPI',
description: 'Select workflow with an Agent loop',
});
const batchWorkflowTitleText = intl.formatMessage({
defaultMessage: 'Select a Batch Workflow resource',
id: 'gvDMuq',
Expand Down Expand Up @@ -210,6 +215,21 @@ export const AzureResourceSelection = (props: AzureResourceSelectionProps) => {
break;
}

case Constants.AZURE_RESOURCE_ACTION_TYPES.SELECT_NESTED_AGENT_WORKFLOW_ACTION: {
setTitleText(nestedAgentWorkflowTitleText);
setResourceTypes(['agentWorkflow']);
setGetResourcesCallbacks(() => [() => SearchService().getAgentWorkflows?.()]);
setSubmitCallback(() => () => {
addResourceOperation({
name: getResourceName(selectedResources[0]),
presetParameterValues: {
'host.workflow.id': selectedResources[0].id,
},
});
});
break;
}

case Constants.AZURE_RESOURCE_ACTION_TYPES.SELECT_BATCH_WORKFLOW_ACTION: {
setTitleText(batchWorkflowTitleText);
setResourceTypes(['batchWorkflow', 'trigger']);
Expand Down Expand Up @@ -241,6 +261,7 @@ export const AzureResourceSelection = (props: AzureResourceSelectionProps) => {
swaggerFunctionAppTitleText,
getOptionsFromPaths,
manualWorkflowTitleText,
nestedAgentWorkflowTitleText,
operation.id,
selectedResources,
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,27 @@ export const almostAllBuiltInOperations: DiscoveryOperation<DiscoveryResultTypes
iconUri: 'https://logicappsv2resources.blob.core.windows.net/icons/workflow.svg',
},
},
{
name: 'invokeNestedAgent',
id: 'invokenestedagent',
type: 'nestedagent',
properties: {
api: {
id: 'connectionProviders/workflow',
name: 'connectionProviders/workflow',
displayName: 'Azure Logic Apps',
iconUri: 'https://logicappsv2resources.blob.core.windows.net/icons/workflow.svg',
brandColor: '#59b2d9',
description: 'Azure Logic Apps',
},
summary: 'Choose a Logic Apps workflow',
description: 'Send a task to a nested agent workflow in the same region',
visibility: 'Important',
operationType: 'NestedAgent',
brandColor: '#59b2d9',
iconUri: 'https://logicappsv2resources.blob.core.windows.net/icons/workflow.svg',
},
},
{
name: 'xmlTransform',
id: 'xmlTransform',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { describe, test, expect, beforeEach, vi } from 'vitest';
import type { IHttpClient } from '../../httpClient';

// Create a concrete implementation for testing
class TestSearchService {
constructor(private options: any) {}

private async getWorkflows(filter: string): Promise<any[]> {
const { httpClient, apiHubServiceDetails } = this.options;
const uri = `/subscriptions/${apiHubServiceDetails.subscriptionId}/providers/Microsoft.Logic/workflows`;
const queryParameters = {
'api-version': apiHubServiceDetails.apiVersion,
$filter: filter,
};
const response = await httpClient.get({ uri, queryParameters });
return response?.value ?? [];
}

public async getAgentWorkflows(): Promise<any[]> {
const requestWorkflows = await this.getWorkflows(
"contains(Trigger, 'Request') and (properties/integrationServiceEnvironmentResourceId eq null)"
);
return requestWorkflows.filter((workflow: any) => {
const triggers = workflow.properties?.definition?.triggers ?? {};
return Object.values(triggers).some((trigger: any) => trigger.kind?.toLowerCase() === 'agent');
});
}
}

describe('BaseSearchService', () => {
let mockHttpClient: IHttpClient;
let searchService: TestSearchService;
let mockOptions: any;

beforeEach(() => {
mockHttpClient = {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
patch: vi.fn(),
dispose: vi.fn(),
};

mockOptions = {
httpClient: mockHttpClient,
apiHubServiceDetails: {
subscriptionId: 'test-subscription',
location: 'westus',
apiVersion: '2016-06-01',
},
};

searchService = new TestSearchService(mockOptions);
});

describe('getAgentWorkflows', () => {
test('should return only workflows with Agent triggers', async () => {
const mockWorkflows = {
value: [
{
id: '/workflows/agent-workflow',
name: 'agent-workflow',
properties: {
definition: {
triggers: {
manual: {
type: 'Request',
kind: 'Agent',
},
},
},
},
},
{
id: '/workflows/regular-workflow',
name: 'regular-workflow',
properties: {
definition: {
triggers: {
manual: {
type: 'Request',
kind: 'Http',
},
},
},
},
},
],
};

vi.mocked(mockHttpClient.get).mockResolvedValue(mockWorkflows);

const result = await searchService.getAgentWorkflows();

expect(result).toHaveLength(1);
expect(result[0].name).toBe('agent-workflow');
});

test('should handle case-insensitive Agent kind matching', async () => {
const mockWorkflows = {
value: [
{
id: '/workflows/agent-uppercase',
name: 'agent-uppercase',
properties: {
definition: {
triggers: {
manual: { type: 'Request', kind: 'AGENT' },
},
},
},
},
{
id: '/workflows/agent-mixedcase',
name: 'agent-mixedcase',
properties: {
definition: {
triggers: {
manual: { type: 'Request', kind: 'AgEnT' },
},
},
},
},
],
};

vi.mocked(mockHttpClient.get).mockResolvedValue(mockWorkflows);

const result = await searchService.getAgentWorkflows();

expect(result).toHaveLength(2);
});

test('should return empty array when no Agent workflows exist', async () => {
const mockWorkflows = {
value: [
{
id: '/workflows/regular',
name: 'regular',
properties: {
definition: {
triggers: {
manual: { type: 'Request', kind: 'Http' },
},
},
},
},
],
};

vi.mocked(mockHttpClient.get).mockResolvedValue(mockWorkflows);

const result = await searchService.getAgentWorkflows();

expect(result).toHaveLength(0);
});

test('should handle workflows with no triggers defined', async () => {
const mockWorkflows = {
value: [
{
id: '/workflows/no-triggers',
name: 'no-triggers',
properties: {
definition: {},
},
},
],
};

vi.mocked(mockHttpClient.get).mockResolvedValue(mockWorkflows);

const result = await searchService.getAgentWorkflows();

expect(result).toHaveLength(0);
});

test('should handle workflows with undefined kind', async () => {
const mockWorkflows = {
value: [
{
id: '/workflows/no-kind',
name: 'no-kind',
properties: {
definition: {
triggers: {
manual: { type: 'Request' },
},
},
},
},
],
};

vi.mocked(mockHttpClient.get).mockResolvedValue(mockWorkflows);

const result = await searchService.getAgentWorkflows();

expect(result).toHaveLength(0);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,14 @@ export abstract class BaseSearchService implements ISearchService {
return this.getWorkflows(`contains(Trigger, 'Request') and (${ISE_RESOURCE_ID} eq null)`);
}

public async getAgentWorkflows(): Promise<ArmResource<DiscoveryWorkflow>[]> {
const requestWorkflows = await this.getWorkflows(`contains(Trigger, 'Request') and (${ISE_RESOURCE_ID} eq null)`);
return requestWorkflows.filter((workflow: any) => {
const triggers = workflow.properties?.definition?.triggers ?? {};
return Object.values(triggers).some((trigger: any) => trigger.kind?.toLowerCase() === 'agent');
});
}

public async getBatchWorkflows(): Promise<ArmResource<DiscoveryWorkflow>[]> {
return this.getWorkflows(`contains(Trigger, 'Batch') and (${ISE_RESOURCE_ID} eq null)`);
}
Expand Down
Loading
Loading