diff --git a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx index 91f2e1f81f8..41b2a1bcebe 100644 --- a/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx +++ b/apps/studio/components/interfaces/BranchManagement/BranchPanels.tsx @@ -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 + {branchingWorkflowLogsEnabled && } } className="px-1" /> @@ -275,12 +282,14 @@ export const BranchRow = ({ )} - {shouldRenderLogsButton && ( + {shouldRenderGitHubLogsButton ? ( }> View Logs + ) : ( + )} diff --git a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx index 6af935d805d..db71fa16422 100644 --- a/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx +++ b/apps/studio/components/interfaces/BranchManagement/CreateBranchModal.tsx @@ -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) => { - - - - Each Preview branch costs $0.32 per day - - Each preview branch costs $0.32 per day until it is removed. This pricing is for - Early Access and is subject to change. - - + + diff --git a/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx b/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx new file mode 100644 index 00000000000..f631d52c94b --- /dev/null +++ b/apps/studio/components/interfaces/BranchManagement/WorkflowLogs.tsx @@ -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(undefined) + + const { + data: workflowRunLogs, + isSuccess: isWorkflowRunLogsSuccess, + isLoading: isWorkflowRunLogsLoading, + isError: isWorkflowRunLogsError, + error: workflowRunLogsError, + } = useWorkflowRunLogsQuery( + { + workflowRunId: selectedWorkflowRunId, + }, + { + enabled: isOpen && selectedWorkflowRunId !== undefined, + } + ) + + return ( + + + View Logs + + + + + Workflow Logs + + Select a workflow run to view logs + + + + + + {selectedWorkflowRunId === undefined ? ( + <> + {isWorkflowRunsLoading && } + {isWorkflowRunsError && } + {isWorkflowRunsSuccess && + (workflowRuns.length > 0 ? ( + + {workflowRuns.map((workflowRun) => ( + + setSelectedWorkflowRunId(workflowRun.id)} + className="flex items-center gap-2 w-full justify-between" + > + + + + {dayjs(workflowRun.created_at).format('DD MMM, YYYY HH:mm')} + + + + + + ))} + + ) : ( + + No workflow runs found. + + ))} + > + ) : ( + + setSelectedWorkflowRunId(undefined)} + type="text" + icon={} + className="self-start mx-5" + > + Back to workflow runs + + + {isWorkflowRunLogsLoading && } + {isWorkflowRunLogsError && ( + + )} + {isWorkflowRunLogsSuccess && ( + + {workflowRunLogs} + + )} + + )} + + + + ) +} + +export default WorkflowLogs diff --git a/apps/studio/data/workflow-runs/keys.ts b/apps/studio/data/workflow-runs/keys.ts new file mode 100644 index 00000000000..dfb79399023 --- /dev/null +++ b/apps/studio/data/workflow-runs/keys.ts @@ -0,0 +1,3 @@ +export const workflowRunKeys = { + list: (projectRef: string | undefined) => ['projects', projectRef, 'workflow-runs'] as const, +} diff --git a/apps/studio/data/workflow-runs/workflow-run-logs-query.ts b/apps/studio/data/workflow-runs/workflow-run-logs-query.ts new file mode 100644 index 00000000000..0e51abd0c8c --- /dev/null +++ b/apps/studio/data/workflow-runs/workflow-run-logs-query.ts @@ -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> +export type WorkflowRunLogsError = ResponseError + +export const useWorkflowRunLogsQuery = ( + { workflowRunId }: WorkflowRunLogsVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + workflowRunKeys.list(workflowRunId), + ({ signal }) => getWorkflowRunLogs({ workflowRunId }, signal), + { enabled: enabled && typeof workflowRunId !== 'undefined', staleTime: 0, ...options } + ) diff --git a/apps/studio/data/workflow-runs/workflow-runs-query.ts b/apps/studio/data/workflow-runs/workflow-runs-query.ts new file mode 100644 index 00000000000..564dea7240a --- /dev/null +++ b/apps/studio/data/workflow-runs/workflow-runs-query.ts @@ -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> +export type WorkflowRunsError = ResponseError + +export const useWorkflowRunsQuery = ( + { projectRef }: WorkflowRunsVariables, + { enabled = true, ...options }: UseQueryOptions = {} +) => + useQuery( + workflowRunKeys.list(projectRef), + ({ signal }) => getWorkflowRuns({ projectRef }, signal), + { enabled: enabled && typeof projectRef !== 'undefined', staleTime: 0, ...options } + ) diff --git a/packages/ui-patterns/ShimmeringLoader/index.tsx b/packages/ui-patterns/ShimmeringLoader/index.tsx index e7ec3e32a71..17457b88c71 100644 --- a/packages/ui-patterns/ShimmeringLoader/index.tsx +++ b/packages/ui-patterns/ShimmeringLoader/index.tsx @@ -22,8 +22,12 @@ const ShimmeringLoader = ({ ) } -const GenericSkeletonLoader = () => ( - +interface GenericSkeletonLoaderProps { + className?: string +} + +const GenericSkeletonLoader = ({ className }: GenericSkeletonLoaderProps) => ( +
+ No workflow runs found. +
+ {workflowRunLogs} +