-
Notifications
You must be signed in to change notification settings - Fork 3.6k
feat(deploy-modal): logs section + URL-driven log selection #4522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from 55 commits
0b9019d
a54dcbe
28af223
d889f32
316bc8c
3f508e4
d6ec115
d7da35b
cf233bb
f8f3758
3c8bb40
d33acf4
4f40c4c
cbfab1c
4309d06
8b57476
e3d0e74
0ac0539
3838b6e
fc07922
3a1b1a8
46ffc49
010435c
c0bc62c
387cc97
2dbc7fd
8a50f18
dcf3302
bc09865
5f56e46
ca3bbf1
bbf400f
7c619e7
64cfda5
7ca736a
6066fc1
3422f64
595c4c3
d6c1bc2
58a3ae2
489f2d3
6aa3fe3
ecbf5e5
2aaf2b7
d445b9c
4bc6a17
5be12f8
4253e57
8d6b615
efcd51a
8d934f3
5ea80a8
3cc581e
273e608
94389b5
9cce6fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export { Logs } from './logs' | ||
| export { Versions } from './versions' |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,183 @@ | ||||||
| 'use client' | ||||||
|
|
||||||
| import { useMemo } from 'react' | ||||||
| import { formatDateTime, formatDuration } from '@sim/utils/formatting' | ||||||
| import clsx from 'clsx' | ||||||
| import { useParams, useRouter } from 'next/navigation' | ||||||
| import { Skeleton } from '@/components/emcn' | ||||||
| import type { WorkflowLogSummary } from '@/lib/api/contracts/logs' | ||||||
| import { type LogFilters, useLogsList } from '@/hooks/queries/logs' | ||||||
| import { | ||||||
| getDisplayStatus, | ||||||
| StatusBadge, | ||||||
| TriggerBadge, | ||||||
| } from '@/app/workspace/[workspaceId]/logs/utils' | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Context Used: Core architecture principles for the Sim app (source) Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||
|
|
||||||
| const HEADER_TEXT_CLASS = 'font-medium text-[var(--text-tertiary)] text-caption' | ||||||
| const ROW_TEXT_CLASS = 'font-medium text-[var(--text-primary)] text-caption' | ||||||
| const COLUMN_BASE_CLASS = 'flex-shrink-0' | ||||||
|
|
||||||
| const COLUMN_WIDTHS = { | ||||||
| STATUS: 'w-[100px]', | ||||||
| TRIGGER: 'w-[120px]', | ||||||
| DURATION: 'w-[80px]', | ||||||
| TIMESTAMP: 'flex-1', | ||||||
| } as const | ||||||
|
|
||||||
| const LOGS_LIMIT = 5 as const | ||||||
|
|
||||||
| const BASE_FILTERS = { | ||||||
| timeRange: 'All time', | ||||||
| level: 'all', | ||||||
| folderIds: [] as string[], | ||||||
| triggers: [] as string[], | ||||||
| searchQuery: '', | ||||||
| limit: LOGS_LIMIT, | ||||||
| sortBy: 'date', | ||||||
| sortOrder: 'desc', | ||||||
| } as const satisfies Omit<LogFilters, 'workflowIds'> | ||||||
|
|
||||||
| interface LogsProps { | ||||||
| workflowId: string | null | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Displays the latest workflow runs inside the deploy modal. | ||||||
| * Clicking a row opens that execution in the Logs page. | ||||||
| */ | ||||||
| export function Logs({ workflowId }: LogsProps) { | ||||||
| const params = useParams() | ||||||
| const router = useRouter() | ||||||
| const workspaceId = params?.workspaceId as string | undefined | ||||||
|
|
||||||
| const filters = useMemo<LogFilters>( | ||||||
| () => ({ ...BASE_FILTERS, workflowIds: workflowId ? [workflowId] : [] }), | ||||||
| [workflowId] | ||||||
| ) | ||||||
|
|
||||||
| const { data, isLoading } = useLogsList(workspaceId, filters, { | ||||||
| enabled: Boolean(workflowId) && Boolean(workspaceId), | ||||||
| }) | ||||||
|
|
||||||
| const logs = useMemo<WorkflowLogSummary[]>( | ||||||
| () => (data?.pages?.[0]?.logs ?? []).slice(0, LOGS_LIMIT), | ||||||
| [data] | ||||||
| ) | ||||||
|
|
||||||
| const handleRowClick = (log: WorkflowLogSummary) => { | ||||||
| if (!workspaceId || !log.executionId) return | ||||||
| router.push(`/workspace/${workspaceId}/logs?executionId=${log.executionId}`) | ||||||
| } | ||||||
|
|
||||||
| if (isLoading && logs.length === 0) { | ||||||
| return ( | ||||||
| <div className='overflow-hidden rounded-sm border border-[var(--border)]'> | ||||||
| <div className='flex h-[30px] items-center bg-[var(--surface-1)] px-4'> | ||||||
| <div className={clsx(COLUMN_WIDTHS.STATUS, COLUMN_BASE_CLASS)}> | ||||||
| <Skeleton className='h-[12px] w-[44px]' /> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.TRIGGER, COLUMN_BASE_CLASS)}> | ||||||
| <Skeleton className='h-[12px] w-[52px]' /> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.DURATION, COLUMN_BASE_CLASS)}> | ||||||
| <Skeleton className='h-[12px] w-[56px]' /> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.TIMESTAMP, 'min-w-0')}> | ||||||
| <Skeleton className='h-[12px] w-[68px]' /> | ||||||
| </div> | ||||||
| </div> | ||||||
| <div className='bg-[var(--surface-2)]'> | ||||||
| {[0, 1, 2].map((i) => ( | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| <div key={i} className='flex h-[36px] items-center px-4'> | ||||||
| <div className={clsx(COLUMN_WIDTHS.STATUS, COLUMN_BASE_CLASS, 'min-w-0 pr-2')}> | ||||||
| <Skeleton className='h-[18px] w-[60px] rounded-sm' /> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.TRIGGER, COLUMN_BASE_CLASS, 'min-w-0 pr-2')}> | ||||||
| <Skeleton className='h-[18px] w-[70px] rounded-sm' /> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.DURATION, COLUMN_BASE_CLASS, 'min-w-0 pr-2')}> | ||||||
| <Skeleton className='h-[12px] w-[48px]' /> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.TIMESTAMP, 'min-w-0')}> | ||||||
| <Skeleton className='h-[12px] w-[160px]' /> | ||||||
| </div> | ||||||
| </div> | ||||||
| ))} | ||||||
| </div> | ||||||
| </div> | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| if (logs.length === 0) { | ||||||
| return ( | ||||||
| <div className='flex h-[80px] items-center justify-center rounded-sm border border-[var(--border)] text-[var(--text-placeholder)] text-small'> | ||||||
| No runs yet | ||||||
| </div> | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| return ( | ||||||
| <div className='overflow-hidden rounded-sm border border-[var(--border)]'> | ||||||
| <div className='flex h-[30px] items-center bg-[var(--surface-1)] px-4'> | ||||||
| <div className={clsx(COLUMN_WIDTHS.STATUS, COLUMN_BASE_CLASS)}> | ||||||
| <span className={HEADER_TEXT_CLASS}>Status</span> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.TRIGGER, COLUMN_BASE_CLASS)}> | ||||||
| <span className={HEADER_TEXT_CLASS}>Trigger</span> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.DURATION, COLUMN_BASE_CLASS)}> | ||||||
| <span className={HEADER_TEXT_CLASS}>Duration</span> | ||||||
| </div> | ||||||
| <div className={clsx(COLUMN_WIDTHS.TIMESTAMP, 'min-w-0')}> | ||||||
| <span className={HEADER_TEXT_CLASS}>Timestamp</span> | ||||||
| </div> | ||||||
| </div> | ||||||
|
|
||||||
| <div className='bg-[var(--surface-2)]'> | ||||||
| {logs.map((log) => { | ||||||
| const isClickable = Boolean(log.executionId && workspaceId) | ||||||
| return ( | ||||||
| <div | ||||||
| key={log.id} | ||||||
| className={clsx( | ||||||
| 'flex h-[36px] items-center px-4 transition-colors duration-100', | ||||||
| isClickable | ||||||
| ? 'cursor-pointer hover-hover:bg-[var(--surface-6)] dark:hover-hover:bg-[var(--border)]' | ||||||
| : 'cursor-default' | ||||||
| )} | ||||||
| onClick={isClickable ? () => handleRowClick(log) : undefined} | ||||||
| > | ||||||
| <div className={clsx(COLUMN_WIDTHS.STATUS, COLUMN_BASE_CLASS, 'min-w-0 pr-2')}> | ||||||
| <StatusBadge status={getDisplayStatus(log.status)} /> | ||||||
| </div> | ||||||
|
|
||||||
| <div className={clsx(COLUMN_WIDTHS.TRIGGER, COLUMN_BASE_CLASS, 'min-w-0 pr-2')}> | ||||||
| {log.trigger ? ( | ||||||
| <TriggerBadge trigger={log.trigger} /> | ||||||
| ) : ( | ||||||
| <span className={ROW_TEXT_CLASS}>—</span> | ||||||
| )} | ||||||
| </div> | ||||||
|
|
||||||
| <div className={clsx(COLUMN_WIDTHS.DURATION, COLUMN_BASE_CLASS, 'min-w-0 pr-2')}> | ||||||
| <span | ||||||
| className={clsx('block truncate text-[var(--text-tertiary)]', ROW_TEXT_CLASS)} | ||||||
| > | ||||||
| {formatDuration(log.duration, { precision: 2 }) || '—'} | ||||||
| </span> | ||||||
| </div> | ||||||
|
|
||||||
| <div className={clsx(COLUMN_WIDTHS.TIMESTAMP, 'min-w-0')}> | ||||||
| <span | ||||||
| className={clsx('block truncate text-[var(--text-tertiary)]', ROW_TEXT_CLASS)} | ||||||
| > | ||||||
| {formatDateTime(new Date(log.createdAt))} | ||||||
| </span> | ||||||
| </div> | ||||||
| </div> | ||||||
| ) | ||||||
| })} | ||||||
| </div> | ||||||
| </div> | ||||||
| ) | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The guard
resolvedId !== selectedLogIdRef.currentprevents dispatchingTOGGLE_LOGwhen the IDs match. BecauseselectedLogIdRefretains the last-selected log id even afterhandleCloseSidebar(which only changesisSidebarOpen, not the selected id), navigating back to/logs?executionId=<X>after manually closing the panel for log X will leave the sidebar closed — the effect seesresolvedId === selectedLogIdRef.currentand silently no-ops. The guard is necessary to avoid double-opening on data refetch, but it needs an additional check: if the sidebar is currently closed it should still open.