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:
Alaister YoungandJoshen Lim authored and GitHub committed 2024-10-10 08:29:04 +00:00
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
+3
View File
@@ -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" />