mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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 }
|
||||
)
|
||||
@@ -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,
|
||||
}
|
||||
Vendored
+2
-2
@@ -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
|
||||
|
||||
Reference in new issue
Block a user