Skip to content

Commit 3e24ab6

Browse files
committed
Added useHasApiTrigger hook to determine if workflows were triggered from the API
1 parent abe3661 commit 3e24ab6

File tree

4 files changed

+99
-6
lines changed

4 files changed

+99
-6
lines changed

apps/dashboard/src/components/workflow-editor/workflow-checklist.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { useNavigate } from 'react-router-dom';
1212
import { useEnvironment, useFetchEnvironments } from '@/context/environment/hooks';
1313
import { useFetchIntegrations } from '@/hooks/use-fetch-integrations';
14+
import { useHasApiTrigger } from '@/hooks/use-has-api-trigger';
1415
import { useTelemetry } from '@/hooks/use-telemetry';
1516
import { StepTypeEnum } from '@/utils/enums';
1617
import { buildRoute, ROUTES } from '@/utils/routes';
@@ -193,6 +194,10 @@ function useChecklistItems(steps: Step[]) {
193194
const { workflow } = useWorkflow();
194195
const { integrations } = useFetchIntegrations();
195196
const telemetry = useTelemetry();
197+
const triggeredFromApi = useHasApiTrigger({
198+
workflowId: workflow?._id,
199+
lastTriggeredAt: workflow?.lastTriggeredAt,
200+
});
196201

197202
const foundInAppIntegration = integrations?.find(
198203
(integration) =>
@@ -257,7 +262,7 @@ function useChecklistItems(steps: Step[]) {
257262
key: 'trigger',
258263
title: 'Trigger workflow from your application',
259264
description: 'Trigger the workflow to test it in production',
260-
isCompleted: () => !!workflow?.lastTriggeredAt,
265+
isCompleted: () => !!workflow?.lastTriggeredAt && triggeredFromApi,
261266
onClick: () => {
262267
telemetry(TelemetryEvent.WORKFLOW_CHECKLIST_STEP_CLICKED, { stepTitle: 'Trigger workflow' });
263268
navigate(
@@ -273,7 +278,7 @@ function useChecklistItems(steps: Step[]) {
273278
},
274279
},
275280
],
276-
[currentEnvironment, workflow, foundInAppIntegration, navigate, steps, telemetry]
281+
[currentEnvironment, workflow, foundInAppIntegration, navigate, steps, telemetry, triggeredFromApi]
277282
);
278283
}
279284

apps/dashboard/src/components/workflow-row.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { FaCode } from 'react-icons/fa6';
1313
import { LuBookUp2 } from 'react-icons/lu';
1414
import {
1515
RiDeleteBin2Line,
16-
RiErrorWarningLine,
1716
RiFlashlightLine,
1817
RiMore2Fill,
1918
RiPauseCircleLine,
@@ -49,6 +48,7 @@ import { useAuth } from '@/context/auth/hooks';
4948
import { useEnvironment, useFetchEnvironments } from '@/context/environment/hooks';
5049
import { useDeleteWorkflow } from '@/hooks/use-delete-workflow';
5150
import { useFeatureFlag } from '@/hooks/use-feature-flag';
51+
import { useHasApiTrigger } from '@/hooks/use-has-api-trigger';
5252
import { useHasPermission } from '@/hooks/use-has-permission';
5353
import { usePatchWorkflow } from '@/hooks/use-patch-workflow';
5454
import { useSyncWorkflow } from '@/hooks/use-sync-workflow';
@@ -120,6 +120,10 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
120120
const navigate = useNavigate();
121121
const { safeSync, PromoteConfirmModal } = useSyncWorkflow(workflow);
122122
const isHttpLogsPageEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_HTTP_LOGS_PAGE_ENABLED, false);
123+
const triggeredFromApi = useHasApiTrigger({
124+
workflowId: workflow._id,
125+
lastTriggeredAt: workflow.lastTriggeredAt,
126+
});
123127
const isV0Workflow = workflow.origin === ResourceOriginEnum.NOVU_CLOUD_V1;
124128
const isDuplicable =
125129
workflow.origin === ResourceOriginEnum.NOVU_CLOUD && currentEnvironment?.type === EnvironmentTypeEnum.DEV;
@@ -355,7 +359,7 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
355359
</WorkflowLinkTableCell>
356360

357361
<WorkflowLinkTableCell className="text-foreground-600 text-sm font-medium">
358-
{workflow.lastTriggeredAt ? (
362+
{workflow.lastTriggeredAt && triggeredFromApi ? (
359363
<TimeDisplayHoverCard date={new Date(workflow.lastTriggeredAt)}>
360364
{formatDateSimple(workflow.lastTriggeredAt)}
361365
</TimeDisplayHoverCard>
@@ -369,9 +373,8 @@ export const WorkflowRow = ({ workflow }: WorkflowRowProps) => {
369373
return url.pathname + url.search;
370374
})()}
371375
reloadDocument={isV0Workflow}
372-
className="text-warning-base flex items-center gap-1 text-xs font-medium hover:underline"
376+
className="text-foreground-600 text-sm font-medium hover:underline"
373377
>
374-
<RiErrorWarningLine className="size-3.5" />
375378
Never triggered
376379
</Link>
377380
</TooltipTrigger>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { FeatureFlagsKeysEnum } from '@novu/shared';
2+
import { useQuery } from '@tanstack/react-query';
3+
import { getWorkflowRunsList } from '@/api/activity';
4+
import { useEnvironment } from '@/context/environment/hooks';
5+
import { useFeatureFlag } from '@/hooks/use-feature-flag';
6+
import { QueryKeys } from '@/utils/query-keys';
7+
8+
const STORAGE_KEY = 'novu_api_trigger_';
9+
10+
function getCacheKey(environmentId: string, workflowId: string): string {
11+
return `${STORAGE_KEY}${environmentId}_${workflowId}`;
12+
}
13+
14+
function getCachedApiTrigger(environmentId: string, workflowId: string): boolean | null {
15+
try {
16+
const cached = localStorage.getItem(getCacheKey(environmentId, workflowId));
17+
return cached === 'true' ? true : null;
18+
} catch {
19+
return null;
20+
}
21+
}
22+
23+
function cacheApiTrigger(environmentId: string, workflowId: string): void {
24+
try {
25+
localStorage.setItem(getCacheKey(environmentId, workflowId), 'true');
26+
} catch {
27+
// Silently fail
28+
}
29+
}
30+
31+
function hasApiTrigger(runs: Array<{ payload?: Record<string, unknown> }>): boolean {
32+
return runs.some((run) => {
33+
const source = run.payload?.__source;
34+
return !source || source !== 'dashboard';
35+
});
36+
}
37+
38+
type UseHasApiTriggerOptions = {
39+
workflowId?: string;
40+
lastTriggeredAt?: string;
41+
};
42+
43+
export function useHasApiTrigger({ workflowId, lastTriggeredAt }: UseHasApiTriggerOptions) {
44+
const { currentEnvironment } = useEnvironment();
45+
const isWorkflowRunsEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_WORKFLOW_RUN_PAGE_MIGRATION_ENABLED);
46+
47+
const { data: triggeredFromApi = false } = useQuery({
48+
queryKey: [QueryKeys.hasApiTrigger, currentEnvironment?._id, workflowId],
49+
queryFn: async ({ signal }) => {
50+
if (!currentEnvironment?._id || !workflowId || !lastTriggeredAt) {
51+
return false;
52+
}
53+
54+
const cached = getCachedApiTrigger(currentEnvironment._id, workflowId);
55+
if (cached) return true;
56+
57+
try {
58+
const { data: runs } = await getWorkflowRunsList({
59+
environment: currentEnvironment,
60+
limit: 50,
61+
filters: { workflows: [workflowId] },
62+
signal,
63+
});
64+
65+
if (!runs?.length) return false;
66+
67+
const isApiTriggered = hasApiTrigger(runs);
68+
if (isApiTriggered) {
69+
cacheApiTrigger(currentEnvironment._id, workflowId);
70+
}
71+
72+
return isApiTriggered;
73+
} catch {
74+
return false;
75+
}
76+
},
77+
enabled: isWorkflowRunsEnabled && !!currentEnvironment?._id && !!workflowId && !!lastTriggeredAt,
78+
staleTime: 60_000,
79+
gcTime: 300_000,
80+
retry: false,
81+
});
82+
83+
return triggeredFromApi;
84+
}

apps/dashboard/src/utils/query-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const QueryKeys = Object.freeze({
1212
fetchActivities: 'fetchActivities',
1313
fetchMostRecentWorkflowRun: 'fetchMostRecentWorkflowRun',
1414
fetchWorkflowRunsCount: 'fetchWorkflowRunsCount',
15+
hasApiTrigger: 'hasApiTrigger',
1516
fetchSubscribers: 'fetchSubscribers',
1617
fetchSubscriber: 'fetchSubscriber',
1718
fetchSubscriberPreferences: 'fetchSubscriberPreferences',

0 commit comments

Comments
 (0)