Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/pretty-teeth-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/web": patch
---

Fix layout shift when empty run or hook tables auto-refreshes in local environment
28 changes: 27 additions & 1 deletion packages/web-shared/src/api/workflow-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ interface PaginatedList<T> {
hasNextPage: boolean;
hasPreviousPage: boolean;
reload: () => void;
refresh: () => void;
pageInfo: string;
}

Expand Down Expand Up @@ -336,6 +337,17 @@ export function useWorkflowRuns(
fetchPage(0, undefined, true);
}, [fetchPage]);

const refresh = useCallback(() => {
// Refetch current page without resetting state
// This preserves the existing data while loading, preventing flicker
const currentCursor = pageHistory[currentPage];
// Clear cache for current page to ensure fresh data
const cacheKey = currentCursor ?? 'initial';
pageCache.current.delete(cacheKey);
// Force fetch current page
fetchPage(currentPage, currentCursor, true);
}, [fetchPage, currentPage, pageHistory]);

const currentPageResult = allPageResults.get(currentPage) ?? {
data: null,
isLoading: true,
Expand Down Expand Up @@ -363,7 +375,7 @@ export function useWorkflowRuns(
showPlus
);

return {
const result: PaginatedList<WorkflowRun> = {
data: currentPageResult,
allData: Array.from(allPageResults.values()),
error: globalError,
Expand All @@ -375,8 +387,10 @@ export function useWorkflowRuns(
hasNextPage: hasMore,
hasPreviousPage: currentPage > 0,
reload,
refresh,
pageInfo,
};
return result;
}

/**
Expand Down Expand Up @@ -544,6 +558,17 @@ export function useWorkflowHooks(
fetchPage(0, undefined, true);
}, [fetchPage]);

const refresh = useCallback(() => {
Comment thread
karthikscale3 marked this conversation as resolved.
Comment thread
vercel[bot] marked this conversation as resolved.
// Refetch current page without resetting state
// This preserves the existing data while loading, preventing flicker
const currentCursor = pageHistory[currentPage];
// Clear cache for current page to ensure fresh data
const cacheKey = currentCursor ?? 'initial';
pageCache.current.delete(cacheKey);
// Force fetch current page
fetchPage(currentPage, currentCursor, true);
}, [fetchPage, currentPage, pageHistory]);

const currentPageResult = allPageResults.get(currentPage) ?? {
data: null,
isLoading: true,
Expand Down Expand Up @@ -583,6 +608,7 @@ export function useWorkflowHooks(
hasNextPage: hasMore,
hasPreviousPage: currentPage > 0,
reload,
refresh,
pageInfo,
};
}
Expand Down
14 changes: 8 additions & 6 deletions packages/web/src/components/hooks-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function HooksTable({
hasNextPage,
hasPreviousPage,
reload,
refresh,
pageInfo,
} = useWorkflowHooks(env, {
runId,
Expand All @@ -99,16 +100,17 @@ export function HooksTable({
const hookActions = useHookActions({
env,
callbacks: {
onSuccess: reload,
onSuccess: refresh,
},
});

const loading = data.isLoading;
const hooks = data.data ?? [];

const onReload = () => {
// Refresh current page without resetting state (prevents layout shift)
const onRefresh = () => {
setLastRefreshTime(() => new Date());
reload();
refresh();
};

// Track invocation counts per hook (fetched in background)
Expand Down Expand Up @@ -258,7 +260,7 @@ export function HooksTable({
<Button
variant="outline"
size="sm"
onClick={onReload}
onClick={onRefresh}
disabled={loading}
>
<RefreshCw className={loading ? 'animate-spin' : ''} />
Expand Down Expand Up @@ -374,7 +376,7 @@ export function HooksTable({
toast.success('New run started', {
description: `Run ID: ${newRunId}`,
});
reload();
refresh();
} catch (err) {
toast.error('Failed to re-run', {
description:
Expand All @@ -394,7 +396,7 @@ export function HooksTable({
try {
await cancelRun(env, hook.runId);
toast.success('Run cancelled');
reload();
refresh();
} catch (err) {
toast.error('Failed to cancel', {
description:
Expand Down
11 changes: 9 additions & 2 deletions packages/web/src/components/runs-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ export function RunsTable({ onRunClick }: RunsTableProps) {
hasNextPage,
hasPreviousPage,
reload,
refresh,
pageInfo,
} = useWorkflowRuns(env, {
sortOrder,
Expand Down Expand Up @@ -459,6 +460,12 @@ export function RunsTable({ onRunClick }: RunsTableProps) {
reload();
}, [reload]);

// Refresh current page without resetting state (prevents layout shift)
const onRefresh = useCallback(() => {
setLastRefreshTime(() => new Date());
refresh();
}, [refresh]);

// Get selected runs that are cancellable (pending or running)
const selectedRuns = useMemo(() => {
return runs.filter((run) => selection.selectedIds.has(run.runId));
Expand Down Expand Up @@ -555,11 +562,11 @@ export function RunsTable({ onRunClick }: RunsTableProps) {
useEffect(() => {
if (isLocalAndHasMissingData) {
const interval = setInterval(() => {
onReload();
onRefresh();
}, 5000);
return () => clearInterval(interval);
}
}, [isLocalAndHasMissingData, onReload]);
}, [isLocalAndHasMissingData, onRefresh]);

// Refresh when tab regains focus after a delay, to prevent stale UI.
// TODO: We should generally move to using SWR or similar for _all_ API calls here.
Expand Down
Loading