feat: use new action status endpoint (#39159)

* feat: use new action status endpoint

* fix: flip incorrect conditionals

* chore: add step name to badge

* refactor(action-runs-query): cleaner types

* style(workflow logs): improve design of action run steps

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
This commit is contained in:
Han QiaoandCharis Lam authored and GitHub committed 2025-10-07 04:11:28 +08:00
1 parent 98a3f0e340
commit d5d8d954cb
7 files changed
+254 -30

No files matched your search

@@ -0,0 +1,89 @@
import type { PropsWithChildren } from 'react'
import { ActionName, ActionStatus, type ActionRunStep } from 'data/actions/action-runs-query'
import { Badge, StatusIcon, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
export interface ActionStatusBadgeProps {
name: ActionName
status: ActionStatus
}
const UNHEALTHY_STATUES: ActionStatus[] = ['DEAD', 'REMOVING']
const WAITING_STATUSES: ActionStatus[] = ['CREATED', 'RESTARTING', 'RUNNING']
export const STATUS_TO_LABEL: Record<ActionStatus, string> = {
CREATED: 'pending',
DEAD: 'failed',
EXITED: 'succeeded',
PAUSED: 'skipped',
REMOVING: 'failed',
RESTARTING: 'restarting',
RUNNING: 'running',
}
const NAME_TO_LABEL: Record<ActionName, string> = {
clone: 'Cloning repo',
pull: 'Pulling data',
health: 'Health check',
configure: 'Configurations',
migrate: 'Migrations',
seed: 'Data seeding',
deploy: 'Functions deployment',
}
export const ActionStatusBadgeCondensed = ({
children,
status,
details,
}: PropsWithChildren<{
status: ActionStatus
details: Array<ActionRunStep>
}>) => {
if (status === 'EXITED') {
return null
}
const isUnhealthy = UNHEALTHY_STATUES.includes(status)
const isWaiting = WAITING_STATUSES.includes(status)
return (
<Tooltip>
<TooltipTrigger>
<Badge variant={isUnhealthy ? 'destructive' : 'default'} className="gap-1.5">
{(isUnhealthy || isWaiting) && (
<StatusIcon variant={isUnhealthy ? 'destructive' : 'default'} hideBackground />
)}
{children}
</Badge>
</TooltipTrigger>
<TooltipContent>
Additional {STATUS_TO_LABEL[status]} steps:
<ul>
{details.map((step) => (
<li key={step.name} className="before:content-['-'] before:mr-0.5">
{NAME_TO_LABEL[step.name]}
</li>
))}
</ul>
</TooltipContent>
</Tooltip>
)
}
export const ActionStatusBadge = ({ name, status }: ActionStatusBadgeProps) => {
if (status === 'EXITED') {
return null
}
const isUnhealthy = UNHEALTHY_STATUES.includes(status)
const isWaiting = WAITING_STATUSES.includes(status)
return (
<Badge variant={isUnhealthy ? 'destructive' : 'default'} className="gap-1.5">
{(isUnhealthy || isWaiting) && (
<StatusIcon variant={isUnhealthy ? 'destructive' : 'default'} hideBackground />
)}
{NAME_TO_LABEL[name]}: {STATUS_TO_LABEL[status]}
</Badge>
)
}
@@ -1,12 +1,17 @@
import type { Branch } from 'data/branches/branches-query'
import dayjs from 'dayjs'
import { groupBy } from 'lodash'
import { ArrowLeft, ArrowRight } from 'lucide-react'
import { useState } from 'react'
import { StatusIcon } from 'ui'
import AlertError from 'components/ui/AlertError'
import { useWorkflowRunLogsQuery } from 'data/workflow-runs/workflow-run-logs-query'
import { useWorkflowRunsQuery } from 'data/workflow-runs/workflow-runs-query'
import { ActionRunData } from 'data/actions/action-detail-query'
import { useActionRunLogsQuery } from 'data/actions/action-logs-query'
import {
useActionsQuery,
type ActionRunStep,
type ActionStatus,
} from 'data/actions/action-runs-query'
import type { Branch } from 'data/branches/branches-query'
import {
Button,
cn,
@@ -18,24 +23,21 @@ import {
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
StatusIcon,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { ActionStatusBadge, ActionStatusBadgeCondensed, STATUS_TO_LABEL } from './ActionStatusBadge'
import BranchStatusBadge from './BranchStatusBadge'
interface WorkflowLogsProps {
projectRef: string
status?: Branch['status'] | string
status: Branch['status']
}
type StatusType = Branch['status'] | string
type StatusType = Branch['status']
const UNHEALTHY_STATUSES: StatusType[] = [
'ACTIVE_UNHEALTHY',
'INIT_FAILED',
'UNKNOWN',
'MIGRATIONS_FAILED',
'FUNCTIONS_FAILED',
]
const HEALTHY_STATUSES: StatusType[] = ['FUNCTIONS_DEPLOYED', 'MIGRATIONS_PASSED']
const UNHEALTHY_STATUSES: StatusType[] = ['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED']
export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
const [isOpen, setIsOpen] = useState(false)
@@ -46,9 +48,11 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
isLoading: isWorkflowRunsLoading,
isError: isWorkflowRunsError,
error: workflowRunsError,
} = useWorkflowRunsQuery({ projectRef }, { enabled: isOpen })
} = useActionsQuery({ ref: projectRef }, { enabled: isOpen })
const [selectedWorkflowRunId, setSelectedWorkflowRunId] = useState<string | undefined>(undefined)
const [selectedWorkflowRun, setSelectedWorkflowRun] = useState<ActionRunData | undefined>(
undefined
)
const {
data: workflowRunLogs,
@@ -56,17 +60,13 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
isLoading: isWorkflowRunLogsLoading,
isError: isWorkflowRunLogsError,
error: workflowRunLogsError,
} = useWorkflowRunLogsQuery(
{ workflowRunId: selectedWorkflowRunId },
{ enabled: isOpen && selectedWorkflowRunId !== undefined }
} = useActionRunLogsQuery(
{ ref: projectRef, run_id: selectedWorkflowRun?.id ?? '' },
{ enabled: isOpen && Boolean(selectedWorkflowRun) }
)
const showStatusIcon =
status !== undefined &&
status !== 'ACTIVE_HEALTHY' &&
status !== 'FUNCTIONS_DEPLOYED' &&
status !== 'MIGRATIONS_PASSED'
const isUnhealthy = status !== undefined && UNHEALTHY_STATUSES.includes(status)
const showStatusIcon = !HEALTHY_STATUSES.includes(status)
const isUnhealthy = UNHEALTHY_STATUSES.includes(status)
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -93,7 +93,7 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
<DialogSectionSeparator />
<DialogSection className={cn('px-0', isWorkflowRunLogsSuccess ? 'py-0 pt-2' : '!py-0')}>
{selectedWorkflowRunId === undefined ? (
{!selectedWorkflowRun ? (
<>
{isWorkflowRunsLoading && <GenericSkeletonLoader className="py-4" />}
{isWorkflowRunsError && (
@@ -108,11 +108,15 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
<li key={workflowRun.id} className="py-3">
<button
type="button"
onClick={() => setSelectedWorkflowRunId(workflowRun.id)}
onClick={() => setSelectedWorkflowRun(workflowRun)}
className="flex items-center gap-2 w-full justify-between"
>
<div className="flex items-center gap-4">
<BranchStatusBadge status={workflowRun.status} />
{workflowRun.run_steps.length > 0 ? (
<RunSteps steps={workflowRun.run_steps} />
) : (
<BranchStatusBadge status={status} />
)}
<span className="text-sm">
{dayjs(workflowRun.created_at).format('DD MMM, YYYY HH:mm')}
</span>
@@ -131,7 +135,7 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
) : (
<div className="flex flex-col gap-2 py-2">
<Button
onClick={() => setSelectedWorkflowRunId(undefined)}
onClick={() => setSelectedWorkflowRun(undefined)}
type="text"
icon={<ArrowLeft />}
className="self-start"
@@ -161,3 +165,29 @@ export const WorkflowLogs = ({ projectRef, status }: WorkflowLogsProps) => {
</Dialog>
)
}
function RunSteps({ steps }: { steps: Array<ActionRunStep> }) {
const stepsByStatus = groupBy(steps, 'status') as Record<ActionStatus, Array<ActionRunStep>>
const firstFailedStep = stepsByStatus.DEAD?.[0]
const numberFailedSteps = stepsByStatus.DEAD?.length ?? 0
return (
<>
{firstFailedStep && (
<ActionStatusBadge name={firstFailedStep.name} status={firstFailedStep.status} />
)}
{numberFailedSteps > 1 && (
<ActionStatusBadgeCondensed status={'DEAD'} details={stepsByStatus.DEAD.slice(1)}>
{numberFailedSteps - 1} more
</ActionStatusBadgeCondensed>
)}
{(Object.keys(stepsByStatus) as Array<ActionStatus>)
.filter((status) => status !== 'DEAD')
.map((status) => (
<ActionStatusBadgeCondensed key={status} status={status} details={stepsByStatus[status]}>
{stepsByStatus[status].length} {STATUS_TO_LABEL[status]}
</ActionStatusBadgeCondensed>
))}
</>
)
}
@@ -0,0 +1,30 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import type { operations } from 'data/api'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { actionKeys } from './keys'
export type ActionRunVariables = operations['v1-get-action-run']['parameters']['path']
export async function getActionRun(params: ActionRunVariables, signal?: AbortSignal) {
const { data, error } = await get('/v1/projects/{ref}/actions/{run_id}', {
params: { path: params },
signal,
})
if (error) handleError(error)
return data
}
export type ActionRunData = Awaited<ReturnType<typeof getActionRun>>
export type ActionRunError = ResponseError
export const useActionRunQuery = <TData = ActionRunData>(
{ ref, run_id }: ActionRunVariables,
{ enabled = true, ...options }: UseQueryOptions<ActionRunData, ActionRunError, TData> = {}
) =>
useQuery<ActionRunData, ActionRunError, TData>(
actionKeys.detail(ref, run_id),
({ signal }) => getActionRun({ ref, run_id }, signal),
{ enabled: enabled && Boolean(ref) && Boolean(run_id), staleTime: 0, ...options }
)
@@ -0,0 +1,36 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { operations } from 'data/api'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { actionKeys } from './keys'
export type ActionLogsVariables = operations['v1-get-action-run-logs']['parameters']['path']
export async function getActionRunLogs(params: ActionLogsVariables, signal?: AbortSignal) {
const { data, error } = await get(`/v1/projects/{ref}/actions/{run_id}/logs`, {
params: { path: params },
parseAs: 'text',
headers: { Accept: 'text/plain' },
signal,
})
if (error) handleError(error)
return data
.split('\n')
.flatMap((line) => line.split('\r'))
.join('\n')
.trim()
}
export type ActionLogsData = Awaited<ReturnType<typeof getActionRunLogs>>
export type WorkflowRunLogsError = ResponseError
export const useActionRunLogsQuery = <TData = ActionLogsData>(
{ ref, run_id }: ActionLogsVariables,
{ enabled = true, ...options }: UseQueryOptions<ActionLogsData, WorkflowRunLogsError, TData> = {}
) =>
useQuery<ActionLogsData, WorkflowRunLogsError, TData>(
actionKeys.detail(ref, run_id),
({ signal }) => getActionRunLogs({ ref, run_id }, signal),
{ enabled: enabled && Boolean(ref) && Boolean(run_id), staleTime: 0, ...options }
)
@@ -0,0 +1,34 @@
import { useQuery, type UseQueryOptions } from '@tanstack/react-query'
import type { operations } from 'data/api'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { actionKeys } from './keys'
export type ActionsVariables = operations['v1-list-action-runs']['parameters']['path']
export async function listActionRuns(params: ActionsVariables, signal?: AbortSignal) {
const { data, error } = await get(`/v1/projects/{ref}/actions`, {
params: { path: params },
signal,
})
if (error) handleError(error)
return data
}
export type ActionsData = Awaited<ReturnType<typeof listActionRuns>>
export type ActionsError = ResponseError
export type ActionRunStep = ActionsData[number]['run_steps'][number]
export type ActionName = ActionRunStep['name']
export type ActionStatus = ActionRunStep['status']
export const useActionsQuery = <TData = ActionsData>(
{ ref }: ActionsVariables,
{ enabled = true, ...options }: UseQueryOptions<ActionsData, ActionsError, TData> = {}
) =>
useQuery<ActionsData, ActionsError, TData>(
actionKeys.list(ref),
({ signal }) => listActionRuns({ ref }, signal),
{ enabled: enabled && Boolean(ref), staleTime: 0, ...options }
)
+5
View File
@@ -0,0 +1,5 @@
export const actionKeys = {
list: (projectRef: string | undefined) => ['projects', projectRef, 'actions'] as const,
detail: (projectRef: string | undefined, runId: string | undefined) =>
['projects', projectRef, 'actions', runId] as const,
}
+2 -2
View File
@@ -2882,7 +2882,7 @@ export interface components {
* @description Resource indicator for MCP (Model Context Protocol) clients
* @enum {string}
*/
resource?: 'http://localhost:8080/mcp' | 'http://localhost:8080/mcp'
resource?: 'https://api.supabase.green/mcp' | 'https://mcp.supabase.green/mcp'
}
OAuthTokenResponse: {
access_token: string
@@ -4280,7 +4280,7 @@ export interface operations {
organization_slug?: string
redirect_uri: string
/** @description Resource indicator for MCP (Model Context Protocol) clients */
resource?: 'http://localhost:8080/mcp' | 'http://localhost:8080/mcp'
resource?: 'https://api.supabase.green/mcp' | 'https://mcp.supabase.green/mcp'
response_mode?: string
response_type: 'code' | 'token' | 'id_token token'
scope?: string