diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-info.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-info.tsx index 6693f2633f..256c6494f1 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-info.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-info.tsx @@ -1,13 +1,11 @@ "use client"; +import type { DeploymentStatus } from "@/lib/collections"; import { formatCpuParts, formatMemoryParts } from "@/lib/utils/deployment-formatters"; import { Bolt, Cloud, Grid, LayoutRight } from "@unkey/icons"; import { Button, InfoTooltip } from "@unkey/ui"; import { ActiveDeploymentCard } from "../../../../components/active-deployment-card"; -import { - type DeploymentStatus, - DeploymentStatusBadge, -} from "../../../../components/deployment-status-badge"; +import { DeploymentStatusBadge } from "../../../../components/deployment-status-badge"; import { InfoChip } from "../../../../components/info-chip"; import { RegionFlags } from "../../../../components/region-flags"; import { Section, SectionHeader } from "../../../../components/section"; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx index 068de93d0d..f9a0e9281a 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx @@ -36,7 +36,7 @@ export function DeploymentProgress({ stepsData }: { stepsData?: StepsData }) { const { getDomainsForDeployment, projectId } = useProjectData(); - const [now, setNow] = useState(0); + const [now, setNow] = useState(Date.now); useEffect(() => { if (isFailed) { return; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx index 643678d729..6e9bd4e8a0 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx @@ -1,9 +1,9 @@ "use client"; import { cn } from "@/lib/utils"; +import { formatCompoundDuration } from "@/lib/utils/metric-formatters"; import { Check, CircleHalfDottedClock, TriangleWarning2 } from "@unkey/icons"; import { Badge, Loading, SettingCard } from "@unkey/ui"; -import ms from "ms"; type DeploymentStepProps = { icon: React.ReactNode; @@ -87,7 +87,9 @@ export function DeploymentStep({ contentWidth="w-fit" >
- {duration ? ms(duration) : null} + + {duration !== null && duration !== undefined ? formatCompoundDuration(duration) : null} + {status === "completed" ? ( ) : status === "started" ? ( diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/deployment-utils.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/deployment-utils.ts new file mode 100644 index 0000000000..ec647dc7e0 --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/deployment-utils.ts @@ -0,0 +1,55 @@ +import type { DeploymentStatus } from "@/lib/collections"; +import type { StepsData } from "./(deployment-progress)/deployment-progress"; + +const DEPLOYMENT_STATUSES: ReadonlySet = new Set([ + "pending", + "starting", + "building", + "deploying", + "network", + "finalizing", + "ready", + "failed", +]); + +function isDeploymentStatus(value: string): value is DeploymentStatus { + return DEPLOYMENT_STATUSES.has(value); +} + +export function deriveStatusFromSteps( + steps: StepsData | undefined, + fallback: string, +): DeploymentStatus { + if (!steps) { + return isDeploymentStatus(fallback) ? fallback : "pending"; + } + + const { queued, building, deploying, network, finalizing, starting } = steps; + + if ([queued, building, deploying, network, finalizing, starting].some((s) => s?.error)) { + return "failed"; + } + if (finalizing && !finalizing.endedAt) { + return "finalizing"; + } + if (finalizing?.completed) { + return "ready"; + } + if (network && !network.endedAt) { + return "network"; + } + if (deploying && !deploying.endedAt) { + return "deploying"; + } + if (building && !building.endedAt) { + return "building"; + } + if (starting && !starting.endedAt) { + return "starting"; + } + if (queued && !queued.endedAt) { + return "pending"; + } + + return isDeploymentStatus(fallback) ? fallback : "pending"; +} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/page.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/page.tsx index 0c45515eb1..87d953bb4f 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/page.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/page.tsx @@ -2,12 +2,12 @@ import { trpc } from "@/lib/trpc/client"; import { useEffect, useMemo } from "react"; import { DeploymentDomainsCard } from "../../../components/deployment-domains-card"; -import type { DeploymentStatus } from "../../../components/deployment-status-badge"; import { ProjectContentWrapper } from "../../../components/project-content-wrapper"; import { useProjectData } from "../../data-provider"; import { DeploymentInfo } from "./(deployment-progress)/deployment-info"; -import { DeploymentProgress, type StepsData } from "./(deployment-progress)/deployment-progress"; +import { DeploymentProgress } from "./(deployment-progress)/deployment-progress"; import { DeploymentNetworkSection } from "./(overview)/components/sections/deployment-network-section"; +import { deriveStatusFromSteps } from "./deployment-utils"; import { useDeployment } from "./layout-provider"; export default function DeploymentOverview() { @@ -49,45 +49,3 @@ export default function DeploymentOverview() { ); } - -const DEPLOYMENT_STATUSES: ReadonlySet = new Set([ - "pending", - "building", - "deploying", - "network", - "ready", - "failed", -]); - -function isDeploymentStatus(value: string): value is DeploymentStatus { - return DEPLOYMENT_STATUSES.has(value); -} - -function deriveStatusFromSteps(steps: StepsData | undefined, fallback: string): DeploymentStatus { - if (!steps) { - return isDeploymentStatus(fallback) ? fallback : "pending"; - } - - const { queued, building, deploying, network } = steps; - - if ([queued, building, deploying, network].some((s) => s?.error)) { - return "failed"; - } - if (network?.completed) { - return "ready"; - } - if (network && !network.endedAt) { - return "network"; - } - if (deploying && !deploying.endedAt) { - return "deploying"; - } - if (building && !building.endedAt) { - return "building"; - } - if (queued && !queued.endedAt) { - return "pending"; - } - - return isDeploymentStatus(fallback) ? fallback : "pending"; -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/deployment-status-badge.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/deployment-status-badge.tsx deleted file mode 100644 index ffeec65b2f..0000000000 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/deployment-status-badge.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; -import { - ArrowDotAntiClockwise, - CircleCheck, - CircleHalfDottedClock, - CircleWarning, - HalfDottedCirclePlay, - Nut, -} from "@unkey/icons"; -import type { IconProps } from "@unkey/icons/src/props"; -import { cn } from "@unkey/ui/src/lib/utils"; -import type { FC } from "react"; -import type { DeploymentStatus } from "../../../filters.schema"; - -type StatusConfig = { - icon: FC; - label: string; - bgColor: string; - textColor: string; - iconColor: string; - animated?: boolean; -}; - -const statusConfigs: Record = { - pending: { - icon: CircleHalfDottedClock, - label: "Pending", - bgColor: "bg-grayA-3", - textColor: "text-grayA-11", - iconColor: "text-gray-11", - }, - starting: { - icon: HalfDottedCirclePlay, - label: "Starting", - bgColor: "bg-linear-to-r from-infoA-5 to-transparent", - textColor: "text-infoA-11", - iconColor: "text-info-11", - animated: true, - }, - building: { - icon: Nut, - label: "Building", - bgColor: "bg-linear-to-r from-infoA-5 to-transparent", - textColor: "text-infoA-11", - iconColor: "text-info-11", - animated: true, - }, - deploying: { - icon: HalfDottedCirclePlay, - label: "Deploying", - bgColor: "bg-linear-to-r from-infoA-5 to-transparent", - textColor: "text-infoA-11", - iconColor: "text-info-11", - animated: true, - }, - network: { - icon: ArrowDotAntiClockwise, - label: "Assigning Domains", - bgColor: "bg-linear-to-r from-infoA-5 to-transparent", - textColor: "text-infoA-11", - iconColor: "text-info-11", - animated: true, - }, - finalizing: { - icon: Nut, - label: "Finalizing", - bgColor: "bg-linear-to-r from-infoA-5 to-transparent", - textColor: "text-infoA-11", - iconColor: "text-info-11", - animated: true, - }, - ready: { - icon: CircleCheck, - label: "Ready", - bgColor: "bg-successA-3", - textColor: "text-successA-11", - iconColor: "text-success-11", - }, - failed: { - icon: CircleWarning, - label: "Failed", - bgColor: "bg-errorA-3", - textColor: "text-errorA-11", - iconColor: "text-error-11", - }, -}; - -type DeploymentStatusBadgeProps = { - status: DeploymentStatus; - className?: string; -}; - -export const DeploymentStatusBadge = ({ status, className }: DeploymentStatusBadgeProps) => { - const config = statusConfigs[status]; - - if (!config) { - throw new Error(`Invalid deployment status: ${status}`); - } - - const { icon: Icon, label, bgColor, textColor, iconColor, animated } = config; - - return ( -
- {animated && ( -
- )} - - {label} -
- ); -}; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/domain_list.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/domain_list.tsx index 0540645c32..2111afd566 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/domain_list.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/domain_list.tsx @@ -1,6 +1,6 @@ +import type { DeploymentStatus } from "@/lib/collections/deploy/deployment-status"; import { InfoTooltip } from "@unkey/ui"; import { useProjectData } from "../../../../data-provider"; -import type { DeploymentStatus } from "../../../filters.schema"; import { DomainListSkeleton } from "./skeletons"; type Props = { diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/deployments-list.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/deployments-list.tsx index 8fdf8d4c81..6d19462447 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/deployments-list.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/deployments-list.tsx @@ -11,11 +11,11 @@ import { cn } from "@unkey/ui/src/lib/utils"; import dynamic from "next/dynamic"; import { useRouter } from "next/navigation"; import { useCallback, useMemo } from "react"; +import { DeploymentStatusBadge } from "../../../../components/deployment-status-badge"; import { Avatar } from "../../../../components/git-avatar"; import { StatusIndicator } from "../../../../components/status-indicator"; import { useProjectData } from "../../../data-provider"; import { useDeployments } from "../../hooks/use-deployments"; -import { DeploymentStatusBadge } from "./components/deployment-status-badge"; import { DomainList } from "./components/domain_list"; import { EnvStatusBadge } from "./components/env-status-badge"; import { diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/filters.schema.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/filters.schema.ts index afb7bc2807..58f4da2147 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/filters.schema.ts +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/filters.schema.ts @@ -5,19 +5,9 @@ import type { } from "@/components/logs/validation/filter.types"; import { parseAsFilterValueArray } from "@/components/logs/validation/utils/nuqs-parsers"; import { createFilterOutputSchema } from "@/components/logs/validation/utils/structured-output-schema-generator"; +import type { DeploymentStatus } from "@/lib/collections/deploy/deployment-status"; import { z } from "zod"; -export const DEPLOYMENT_STATUSES = [ - "pending", - "starting", - "building", - "deploying", - "network", - "finalizing", - "ready", - "failed", -] as const; - // Define grouped statuses for client filtering const GROUPED_DEPLOYMENT_STATUSES = [ "pending", @@ -28,7 +18,6 @@ const GROUPED_DEPLOYMENT_STATUSES = [ const DEPLOYMENT_ENVIRONMENTS = ["production", "preview"] as const; -export type DeploymentStatus = (typeof DEPLOYMENT_STATUSES)[number]; export type GroupedDeploymentStatus = (typeof GROUPED_DEPLOYMENT_STATUSES)[number]; export type DeploymentEnvironment = (typeof DEPLOYMENT_ENVIRONMENTS)[number]; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/deployment-status-badge.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/deployment-status-badge.tsx index 6d58d205b8..66ed4eef3a 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/deployment-status-badge.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/deployment-status-badge.tsx @@ -1,25 +1,19 @@ +"use client"; +import type { DeploymentStatus } from "@/lib/collections/deploy/deployment-status"; import { - ArrowDotAntiClockwise, CircleCheck, - CircleHalfDottedClock, CircleWarning, - HalfDottedCirclePlay, - Nut, + CloudUp, + Earth, + Hammer2, + LayerFront, + Pulse, + Sparkle3, } from "@unkey/icons"; import type { IconProps } from "@unkey/icons/src/props"; import { cn } from "@unkey/ui/src/lib/utils"; import type { FC } from "react"; -export type DeploymentStatus = - | "pending" - | "starting" - | "building" - | "deploying" - | "network" - | "ready" - | "finalizing" - | "failed"; - type StatusConfig = { icon: FC; label: string; @@ -29,16 +23,16 @@ type StatusConfig = { animated?: boolean; }; -const STATUS_CONFIG: Record = { +const statusConfigs: Record = { pending: { - icon: CircleHalfDottedClock, - label: "Queued", + icon: LayerFront, + label: "Pending", bgColor: "bg-grayA-3", textColor: "text-grayA-11", iconColor: "text-gray-11", }, starting: { - icon: HalfDottedCirclePlay, + icon: Pulse, label: "Starting", bgColor: "bg-linear-to-r from-infoA-5 to-transparent", textColor: "text-infoA-11", @@ -46,7 +40,7 @@ const STATUS_CONFIG: Record = { animated: true, }, building: { - icon: Nut, + icon: Hammer2, label: "Building", bgColor: "bg-linear-to-r from-infoA-5 to-transparent", textColor: "text-infoA-11", @@ -54,7 +48,7 @@ const STATUS_CONFIG: Record = { animated: true, }, deploying: { - icon: HalfDottedCirclePlay, + icon: CloudUp, label: "Deploying", bgColor: "bg-linear-to-r from-infoA-5 to-transparent", textColor: "text-infoA-11", @@ -62,7 +56,7 @@ const STATUS_CONFIG: Record = { animated: true, }, network: { - icon: ArrowDotAntiClockwise, + icon: Earth, label: "Assigning Domains", bgColor: "bg-linear-to-r from-infoA-5 to-transparent", textColor: "text-infoA-11", @@ -70,7 +64,7 @@ const STATUS_CONFIG: Record = { animated: true, }, finalizing: { - icon: Nut, + icon: Sparkle3, label: "Finalizing", bgColor: "bg-linear-to-r from-infoA-5 to-transparent", textColor: "text-infoA-11", @@ -86,24 +80,25 @@ const STATUS_CONFIG: Record = { }, failed: { icon: CircleWarning, - label: "Error", + label: "Failed", bgColor: "bg-errorA-3", textColor: "text-errorA-11", iconColor: "text-error-11", }, }; -type Props = { - status?: DeploymentStatus; +type DeploymentStatusBadgeProps = { + status: DeploymentStatus; className?: string; }; -export const DeploymentStatusBadge = ({ status, className }: Props) => { - if (!status) { +export const DeploymentStatusBadge = ({ status, className }: DeploymentStatusBadgeProps) => { + const config = statusConfigs[status]; + + if (!config) { throw new Error(`Invalid deployment status: ${status}`); } - const config = STATUS_CONFIG[status]; const { icon: Icon, label, bgColor, textColor, iconColor, animated } = config; return ( @@ -118,10 +113,7 @@ export const DeploymentStatusBadge = ({ status, className }: Props) => { {animated && (
)} - + {label}
); diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/new/steps/deployment-live.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/new/steps/deployment-live.tsx index 19f289c887..4bdbcec614 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/new/steps/deployment-live.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/new/steps/deployment-live.tsx @@ -1,13 +1,15 @@ "use client"; import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation"; +import { trpc } from "@/lib/trpc/client"; import { Check } from "@unkey/icons"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ProjectDataProvider } from "../../[projectId]/(overview)/data-provider"; import { DeploymentInfo } from "../../[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-info"; import { DeploymentProgress } from "../../[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress"; +import { deriveStatusFromSteps } from "../../[projectId]/(overview)/deployments/[deploymentId]/deployment-utils"; import { DeploymentLayoutProvider, useDeployment, @@ -38,6 +40,16 @@ const DeploymentLiveStepContent = ({ projectId }: { projectId: string }) => { const ready = deployment.status === "ready"; const [countdown, setCountdown] = useState(REDIRECT_DELAY_SECONDS); + const stepsQuery = trpc.deploy.deployment.steps.useQuery( + { deploymentId: deployment.id }, + { refetchInterval: ready ? false : 1_000, refetchOnWindowFocus: false }, + ); + + const derivedStatus = useMemo( + () => deriveStatusFromSteps(stepsQuery.data, deployment.status), + [stepsQuery.data, deployment.status], + ); + const deploymentUrl = `/${workspace.slug}/projects/${projectId}/deployments/${deployment.id}`; useEffect(() => { @@ -92,8 +104,8 @@ const DeploymentLiveStepContent = ({ projectId }: { projectId: string }) => { } />
- - + +
); diff --git a/web/apps/dashboard/components/navigation/sidebar/product-switcher.tsx b/web/apps/dashboard/components/navigation/sidebar/product-switcher.tsx index ddf48627ed..d01594d201 100644 --- a/web/apps/dashboard/components/navigation/sidebar/product-switcher.tsx +++ b/web/apps/dashboard/components/navigation/sidebar/product-switcher.tsx @@ -70,7 +70,7 @@ export const ProductSwitcher: React.FC = ({