Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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