mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat: workflow run logs (#29678)
* feat: workflow run logs * progress * adds logs modal * remove menu item * output raw text * update parseAs * accept text/plain * cleanup ui * feature flag new logs * fix types * fix condition * cleanup spacing * loading spacing * cheeky console.log * more log * split logs properly * add workflow logs to prod branches * address some feedback * Some UI tweaks * More UI tweak * More UI tweak --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
85156a929c
commit
c6dc2c566e
7 files changed
+259
-18
No files matched your search
@@ -24,6 +24,7 @@ import { useBranchResetMutation } from 'data/branches/branch-reset-mutation'
|
||||
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
|
||||
import type { Branch } from 'data/branches/branches-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import BranchStatusBadge from './BranchStatusBadge'
|
||||
import WorkflowLogs from './WorkflowLogs'
|
||||
|
||||
interface BranchManagementSectionProps {
|
||||
header: string
|
||||
@@ -113,8 +115,12 @@ export const BranchRow = ({
|
||||
const createPullRequestURL =
|
||||
generateCreatePullRequestURL?.(branch.git_branch) ?? 'https://github.com'
|
||||
|
||||
const shouldRenderLogsButton =
|
||||
branch.pr_number !== undefined && branch.latest_check_run_id !== undefined
|
||||
const branchingWorkflowLogsEnabled = useFlag('branchingWorkflowLogs')
|
||||
|
||||
const shouldRenderGitHubLogsButton =
|
||||
!branchingWorkflowLogsEnabled &&
|
||||
branch.pr_number !== undefined &&
|
||||
branch.latest_check_run_id !== undefined
|
||||
const checkRunLogsURL = `https://github.com/${repo}/pull/${branch.pr_number}/checks?check_run_id=${branch.latest_check_run_id}`
|
||||
|
||||
const { ref, inView } = useInView()
|
||||
@@ -228,6 +234,7 @@ export const BranchRow = ({
|
||||
View Repository
|
||||
</Link>
|
||||
</Button>
|
||||
{branchingWorkflowLogsEnabled && <WorkflowLogs projectRef={branch.project_ref} />}
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" icon={<MoreVertical />} className="px-1" />
|
||||
@@ -275,12 +282,14 @@ export const BranchRow = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldRenderLogsButton && (
|
||||
{shouldRenderGitHubLogsButton ? (
|
||||
<Button asChild type="default" iconRight={<ExternalLink size={14} />}>
|
||||
<Link passHref target="_blank" rel="noreferrer" href={checkRunLogsURL}>
|
||||
View Logs
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<WorkflowLogs projectRef={branch.project_ref} />
|
||||
)}
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useParams } from 'common'
|
||||
import { AlertCircle, Check, ExternalLink, Loader2 } from 'lucide-react'
|
||||
import { Check, ExternalLink, Loader2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
@@ -16,9 +16,6 @@ import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
@@ -28,6 +25,7 @@ import {
|
||||
Input_Shadcn_,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
interface CreateBranchModalProps {
|
||||
visible: boolean
|
||||
@@ -204,15 +202,13 @@ const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
|
||||
|
||||
<Modal.Separator />
|
||||
|
||||
<Modal.Content className="py-2">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<AlertCircle strokeWidth={1.5} />
|
||||
<AlertTitle_Shadcn_>Each Preview branch costs $0.32 per day</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Each preview branch costs $0.32 per day until it is removed. This pricing is for
|
||||
Early Access and is subject to change.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<Modal.Content className="p-0">
|
||||
<Admonition
|
||||
type="warning"
|
||||
className="rounded-none !mb-0 border-0"
|
||||
title="Each preview branch costs $0.32 per day"
|
||||
description="Each preview branch costs $0.32 per day until it is removed. This pricing is for Early Access and is subject to change."
|
||||
/>
|
||||
</Modal.Content>
|
||||
|
||||
<Modal.Separator />
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
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 dayjs from 'dayjs'
|
||||
import { ArrowLeft, ArrowRight } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogSectionSeparator,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import BranchStatusBadge from './BranchStatusBadge'
|
||||
|
||||
interface WorkflowLogsProps {
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
const WorkflowLogs = ({ projectRef }: WorkflowLogsProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const {
|
||||
data: workflowRuns,
|
||||
isSuccess: isWorkflowRunsSuccess,
|
||||
isLoading: isWorkflowRunsLoading,
|
||||
isError: isWorkflowRunsError,
|
||||
error: workflowRunsError,
|
||||
} = useWorkflowRunsQuery(
|
||||
{
|
||||
projectRef,
|
||||
},
|
||||
{
|
||||
enabled: isOpen,
|
||||
}
|
||||
)
|
||||
|
||||
const [selectedWorkflowRunId, setSelectedWorkflowRunId] = useState<string | undefined>(undefined)
|
||||
|
||||
const {
|
||||
data: workflowRunLogs,
|
||||
isSuccess: isWorkflowRunLogsSuccess,
|
||||
isLoading: isWorkflowRunLogsLoading,
|
||||
isError: isWorkflowRunLogsError,
|
||||
error: workflowRunLogsError,
|
||||
} = useWorkflowRunLogsQuery(
|
||||
{
|
||||
workflowRunId: selectedWorkflowRunId,
|
||||
},
|
||||
{
|
||||
enabled: isOpen && selectedWorkflowRunId !== undefined,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="default">View Logs</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent size="xlarge">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Workflow Logs</DialogTitle>
|
||||
|
||||
<DialogDescription>Select a workflow run to view logs</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
<DialogSection className={cn('px-0', isWorkflowRunLogsSuccess ? 'py-0 pt-2' : '!py-0')}>
|
||||
{selectedWorkflowRunId === undefined ? (
|
||||
<>
|
||||
{isWorkflowRunsLoading && <GenericSkeletonLoader className="py-2 px-5" />}
|
||||
{isWorkflowRunsError && <AlertError error={workflowRunsError} />}
|
||||
{isWorkflowRunsSuccess &&
|
||||
(workflowRuns.length > 0 ? (
|
||||
<ul className="divide-y">
|
||||
{workflowRuns.map((workflowRun) => (
|
||||
<li key={workflowRun.id} className="py-3 px-5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedWorkflowRunId(workflowRun.id)}
|
||||
className="flex items-center gap-2 w-full justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<BranchStatusBadge status={workflowRun.status} />
|
||||
<span className="text-sm">
|
||||
{dayjs(workflowRun.created_at).format('DD MMM, YYYY HH:mm')}
|
||||
</span>
|
||||
</div>
|
||||
<ArrowRight size={16} />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-center text-sm text-foreground-light py-4">
|
||||
No workflow runs found.
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2 py-2">
|
||||
<Button
|
||||
onClick={() => setSelectedWorkflowRunId(undefined)}
|
||||
type="text"
|
||||
icon={<ArrowLeft />}
|
||||
className="self-start mx-5"
|
||||
>
|
||||
Back to workflow runs
|
||||
</Button>
|
||||
|
||||
{isWorkflowRunLogsLoading && <GenericSkeletonLoader className="py-2 px-5" />}
|
||||
{isWorkflowRunLogsError && (
|
||||
<AlertError
|
||||
className="rounded-none"
|
||||
subject="Failed to retrieve workflow logs"
|
||||
error={workflowRunLogsError}
|
||||
/>
|
||||
)}
|
||||
{isWorkflowRunLogsSuccess && (
|
||||
<pre className="whitespace-pre max-h-[500px] overflow-scroll px-5 pb-5">
|
||||
{workflowRunLogs}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DialogSection>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowLogs
|
||||
@@ -0,0 +1,3 @@
|
||||
export const workflowRunKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'workflow-runs'] as const,
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { workflowRunKeys } from './keys'
|
||||
|
||||
export type WorkflowRunLogsVariables = {
|
||||
workflowRunId?: string
|
||||
}
|
||||
|
||||
export async function getWorkflowRunLogs(
|
||||
{ workflowRunId }: WorkflowRunLogsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!workflowRunId) throw new Error('workflowRunId is required')
|
||||
|
||||
const { data, error } = await get(`/platform/workflow-runs/{workflow_run_id}/logs`, {
|
||||
params: {
|
||||
path: {
|
||||
workflow_run_id: workflowRunId,
|
||||
},
|
||||
},
|
||||
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 WorkflowRunLogsData = Awaited<ReturnType<typeof getWorkflowRunLogs>>
|
||||
export type WorkflowRunLogsError = ResponseError
|
||||
|
||||
export const useWorkflowRunLogsQuery = <TData = WorkflowRunLogsData>(
|
||||
{ workflowRunId }: WorkflowRunLogsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<WorkflowRunLogsData, WorkflowRunLogsError, TData> = {}
|
||||
) =>
|
||||
useQuery<WorkflowRunLogsData, WorkflowRunLogsError, TData>(
|
||||
workflowRunKeys.list(workflowRunId),
|
||||
({ signal }) => getWorkflowRunLogs({ workflowRunId }, signal),
|
||||
{ enabled: enabled && typeof workflowRunId !== 'undefined', staleTime: 0, ...options }
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { workflowRunKeys } from './keys'
|
||||
|
||||
export type WorkflowRunsVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export async function getWorkflowRuns({ projectRef }: WorkflowRunsVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const { data, error } = await get(`/platform/workflow-runs`, {
|
||||
params: {
|
||||
query: {
|
||||
project_ref: projectRef,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
})
|
||||
if (error) handleError(error)
|
||||
return data
|
||||
}
|
||||
|
||||
export type WorkflowRunsData = Awaited<ReturnType<typeof getWorkflowRuns>>
|
||||
export type WorkflowRunsError = ResponseError
|
||||
|
||||
export const useWorkflowRunsQuery = <TData = WorkflowRunsData>(
|
||||
{ projectRef }: WorkflowRunsVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<WorkflowRunsData, WorkflowRunsError, TData> = {}
|
||||
) =>
|
||||
useQuery<WorkflowRunsData, WorkflowRunsError, TData>(
|
||||
workflowRunKeys.list(projectRef),
|
||||
({ signal }) => getWorkflowRuns({ projectRef }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', staleTime: 0, ...options }
|
||||
)
|
||||
@@ -22,8 +22,12 @@ const ShimmeringLoader = ({
|
||||
)
|
||||
}
|
||||
|
||||
const GenericSkeletonLoader = () => (
|
||||
<div className="space-y-2">
|
||||
interface GenericSkeletonLoaderProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const GenericSkeletonLoader = ({ className }: GenericSkeletonLoaderProps) => (
|
||||
<div className={cn(className, 'space-y-2')}>
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
|
||||
Reference in new issue
Block a user