Skip to content

Commit 0f28433

Browse files
fix(Designer): Fixed nested loop monitoring view issues (#6633)
fix(Designer): Fixed nested loop monitoring view issues (#6624) * Fixed nested loop issues * Add initial e2e test * Update tests * Update tests * Update e2e tests --------- Co-authored-by: Riley Evans <[email protected]>
1 parent e2ca2f2 commit 0f28433

File tree

5 files changed

+92
-38
lines changed

5 files changed

+92
-38
lines changed

e2e/designer/fixtures/real-api.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ export class RealDataApi {
1414
this.siteName = process.env.AZURE_SITE_NAME ?? '';
1515
this.siteId = `/subscriptions/${process.env.AZURE_SUBSCRIPTION_ID}/resourceGroups/${process.env.AZURE_RESOURCE_GROUP}/providers/Microsoft.Web/sites/${this.siteName}`;
1616
}
17-
async goToWorkflow() {
17+
async goToWorkflow(workflowName?: string) {
1818
await this.page.getByPlaceholder('Select an App').click();
1919
await this.page.getByPlaceholder('Select an App').fill(this.siteName);
2020
await this.page.getByPlaceholder('Select an App').press('Enter');
21-
await this.page.getByLabel('Workflow').locator('span').filter({ hasText: '' }).click();
22-
await this.page.getByRole('option', { name: this.workflowName, exact: true }).click();
21+
await this.page.getByText('Select a Workflow').click();
22+
await this.page.getByRole('option', { name: workflowName ?? this.workflowName, exact: true }).click();
2323
await this.page.getByRole('button', { name: 'Toolbox' }).click();
24+
await this.page.waitForTimeout(2000);
2425
await this.page.getByLabel('fit view').click({ force: true });
2526
}
2627
async saveWorkflow() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect, test } from '../../../fixtures/real-api';
2+
test.describe(
3+
'Nested loops',
4+
{
5+
tag: '@real',
6+
},
7+
() => {
8+
test('Sanity check', async ({ page, realDataApi }) => {
9+
await page.goto('/');
10+
await realDataApi.goToWorkflow('NestedLoops');
11+
12+
// Check workfow loads correctly
13+
await expect(page.getByLabel('Foreach operation')).toBeVisible();
14+
await expect(page.getByLabel('Foreach-2 operation')).toBeVisible();
15+
16+
// Load run history
17+
await page.getByRole('menuitem', { name: 'Run History' }).click();
18+
await page.waitForTimeout(3000);
19+
await page.getByRole('gridcell', { name: '/18/2025, 3:25:03 PM' }).click();
20+
21+
// Check for outermost foreach
22+
await expect(page.getByTestId('msla-pill-foreach_status')).toBeVisible();
23+
await expect(page.getByTestId('msla-pager-v2-foreach')).toBeVisible();
24+
25+
// Check for inner actions
26+
await expect(page.getByTestId('card-increment_counter')).toBeVisible();
27+
await expect(page.getByLabel('1.1 seconds. Succeeded')).toContainText('1s');
28+
await expect(page.getByLabel('11.3 seconds. Succeeded')).toContainText('11s');
29+
30+
// Check for innermost loop
31+
await expect(page.getByTestId('msla-pill-foreach_2_status')).toBeVisible();
32+
await expect(page.getByTestId('msla-pager-v2-foreach_2')).toBeVisible();
33+
34+
// Open panel
35+
await page.getByTestId('card-increment_counter').getByRole('button', { name: 'Increment counter' }).click();
36+
await expect(page.getByLabel('Value', { exact: true }).locator('pre')).toContainText('1');
37+
38+
// Move to next iteration
39+
await page.getByTestId('msla-pager-v2-foreach').getByLabel('Next').click();
40+
await expect(page.getByLabel('Value', { exact: true }).locator('pre')).toContainText('2');
41+
await expect(page.getByLabel('0.1 seconds. Succeeded')).toContainText('0.1s');
42+
await expect(page.getByLabel('13.3 seconds. Succeeded')).toContainText('13s');
43+
44+
// Move to previous iteration to check state is preserved
45+
await page.getByTestId('msla-pager-v2-foreach').getByLabel('Previous').click();
46+
await expect(page.getByLabel('Value', { exact: true }).locator('pre')).toContainText('1');
47+
48+
// Check inner loop to be interactive
49+
await page.getByTestId('msla-pager-v2-foreach_2').getByLabel('Next').click();
50+
});
51+
}
52+
);

libs/designer/src/lib/core/state/workflow/workflowSlice.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ export const workflowSlice = createSlice({
335335
...runData,
336336
inputsLink: runData?.inputsLink ?? null,
337337
outputsLink: runData?.outputsLink ?? null,
338-
duration: getDurationStringPanelMode(Date.parse(runData.endTime) - Date.parse(runData.startTime), /* abbreviated */ true),
338+
duration: getDurationStringPanelMode(Date.parse(runData?.endTime) - Date.parse(runData?.startTime), /* abbreviated */ true),
339339
};
340340
nodeMetadata.runData = nodeRunData as LogicAppsV2.WorkflowRunAction;
341341
},

libs/designer/src/lib/ui/CustomNodes/OperationCardNode.tsx

+11-8
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ import { setRepetitionRunData } from '../../core/state/workflow/workflowSlice';
4747
import { getRepetitionName } from '../common/LoopsPager/helper';
4848
import { DropZone } from '../connections/dropzone';
4949
import { MessageBarType } from '@fluentui/react';
50+
import type { LogicAppsV2 } from '@microsoft/logic-apps-shared';
5051
import { isNullOrUndefined, RunService, useNodeIndex } from '@microsoft/logic-apps-shared';
5152
import { Card } from '@microsoft/designer-ui';
52-
import { memo, useCallback, useMemo, useState } from 'react';
53+
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
5354
import { useDrag } from 'react-dnd';
5455
import { useIntl } from 'react-intl';
5556
import { useQuery } from '@tanstack/react-query';
@@ -103,14 +104,10 @@ const DefaultNode = ({ targetPosition = Position.Top, sourcePosition = Position.
103104
return RunService().getRepetition({ nodeId: id, runId: runInstance?.id }, repetitionName);
104105
}, [id, parentRunData?.status, repetitionName, runInstance?.id]);
105106

106-
const { isFetching: isRepetitionLoading } = useQuery<any>(
107+
const { isFetching: isRepetitionFetching, data: repetitionRunData } = useQuery<any>(
107108
['runInstance', { nodeId: id, runId: runInstance?.id, repetitionName, parentStatus: parentRunData?.status, parentRunIndex }],
108109
async () => {
109-
const data = await getRunRepetition();
110-
if (!isNullOrUndefined(data)) {
111-
dispatch(setRepetitionRunData({ nodeId: id, runData: data.properties as any }));
112-
}
113-
return data;
110+
return await getRunRepetition();
114111
},
115112
{
116113
refetchOnWindowFocus: false,
@@ -121,6 +118,12 @@ const DefaultNode = ({ targetPosition = Position.Top, sourcePosition = Position.
121118
}
122119
);
123120

121+
useEffect(() => {
122+
if (!isNullOrUndefined(repetitionRunData)) {
123+
dispatch(setRepetitionRunData({ nodeId: id, runData: repetitionRunData.properties as LogicAppsV2.WorkflowRunAction }));
124+
}
125+
}, [dispatch, repetitionRunData, id]);
126+
124127
const { dependencies, loopSources } = useTokenDependencies(id);
125128

126129
const [{ isDragging }, drag, dragPreview] = useDrag(
@@ -234,7 +237,7 @@ const DefaultNode = ({ targetPosition = Position.Top, sourcePosition = Position.
234237

235238
const { isFetching: isOperationQueryLoading, isError: isOperationQueryError } = useOperationQuery(id);
236239

237-
const isLoading = useMemo(() => isRepetitionLoading || isOperationQueryLoading, [isRepetitionLoading, isOperationQueryLoading]);
240+
const isLoading = useMemo(() => isRepetitionFetching || isOperationQueryLoading, [isRepetitionFetching, isOperationQueryLoading]);
238241

239242
const opManifestErrorText = intl.formatMessage({
240243
defaultMessage: 'Error fetching manifest',

libs/designer/src/lib/ui/CustomNodes/ScopeCardNode.tsx

+24-26
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { MessageBarType } from '@fluentui/react';
3636
import { RunService, equals, isNullOrUndefined, removeIdTag, useNodeIndex } from '@microsoft/logic-apps-shared';
3737
import { ScopeCard } from '@microsoft/designer-ui';
3838
import type { LogicAppsV2 } from '@microsoft/logic-apps-shared';
39-
import { memo, useCallback, useMemo, useRef, useState } from 'react';
39+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
4040
import { useDrag } from 'react-dnd';
4141
import { useIntl } from 'react-intl';
4242
import { useQuery } from '@tanstack/react-query';
@@ -72,31 +72,24 @@ const ScopeCardNode = ({ data, targetPosition = Position.Top, sourcePosition = P
7272
);
7373
const rootRef = useRef<HTMLDivElement | null>(null);
7474

75-
const getRunRepetition = useCallback(() => {
76-
if (parentRunData?.status === constants.FLOW_STATUS.SKIPPED) {
77-
return {
78-
properties: {
79-
status: constants.FLOW_STATUS.SKIPPED,
80-
inputsLink: null,
81-
outputsLink: null,
82-
startTime: null,
83-
endTime: null,
84-
trackingId: null,
85-
correlation: null,
86-
},
87-
};
88-
}
89-
return RunService().getRepetition({ nodeId: scopeId, runId: runInstance?.id }, repetitionName);
90-
}, [scopeId, parentRunData?.status, repetitionName, runInstance?.id]);
91-
92-
const { isLoading: isRepetitionLoading, isRefetching: isRepetitionRefetching } = useQuery<any>(
75+
const { isFetching: isRepetitionFetching, data: repetitionRunData } = useQuery<any>(
9376
['runInstance', { nodeId: scopeId, runId: runInstance?.id, repetitionName, parentStatus: parentRunData?.status }],
9477
async () => {
95-
const data = await getRunRepetition();
96-
if (!isNullOrUndefined(data)) {
97-
dispatch(setRepetitionRunData({ nodeId: scopeId, runData: data.properties as LogicAppsV2.WorkflowRunAction }));
78+
if (parentRunData?.status === constants.FLOW_STATUS.SKIPPED) {
79+
return {
80+
properties: {
81+
status: constants.FLOW_STATUS.SKIPPED,
82+
inputsLink: null,
83+
outputsLink: null,
84+
startTime: null,
85+
endTime: null,
86+
trackingId: null,
87+
correlation: null,
88+
},
89+
};
9890
}
99-
return data;
91+
92+
return await RunService().getRepetition({ nodeId: scopeId, runId: runInstance?.id }, repetitionName);
10093
},
10194
{
10295
refetchOnWindowFocus: false,
@@ -107,6 +100,12 @@ const ScopeCardNode = ({ data, targetPosition = Position.Top, sourcePosition = P
107100
}
108101
);
109102

103+
useEffect(() => {
104+
if (!isNullOrUndefined(repetitionRunData)) {
105+
dispatch(setRepetitionRunData({ nodeId: scopeId, runData: repetitionRunData.properties as LogicAppsV2.WorkflowRunAction }));
106+
}
107+
}, [dispatch, repetitionRunData, scopeId]);
108+
110109
const { dependencies, loopSources } = useTokenDependencies(scopeId);
111110
const [{ isDragging }, drag, dragPreview] = useDrag(
112111
() => ({
@@ -196,8 +195,8 @@ const ScopeCardNode = ({ data, targetPosition = Position.Top, sourcePosition = P
196195
const opQuery = useOperationQuery(scopeId);
197196

198197
const isLoading = useMemo(
199-
() => isRepetitionLoading || isRepetitionRefetching || opQuery.isLoading || (!brandColor && !iconUri),
200-
[brandColor, iconUri, opQuery.isLoading, isRepetitionLoading, isRepetitionRefetching]
198+
() => isRepetitionFetching || opQuery.isLoading || (!brandColor && !iconUri),
199+
[brandColor, iconUri, opQuery.isLoading, isRepetitionFetching]
201200
);
202201

203202
const comment = useMemo(
@@ -265,7 +264,6 @@ const ScopeCardNode = ({ data, targetPosition = Position.Top, sourcePosition = P
265264
if (metadata?.runData?.status && !equals(metadata.runData.status, 'InProgress')) {
266265
return <LoopsPager metadata={metadata} scopeId={scopeId} collapsed={graphCollapsed} />;
267266
}
268-
269267
return null;
270268
}, [graphCollapsed, metadata, scopeId]);
271269

0 commit comments

Comments
 (0)