diff --git a/dev/k8s/manifests/ctrl-worker.yaml b/dev/k8s/manifests/ctrl-worker.yaml index e55027fb54..d2fd62fcf7 100644 --- a/dev/k8s/manifests/ctrl-worker.yaml +++ b/dev/k8s/manifests/ctrl-worker.yaml @@ -44,10 +44,16 @@ data: url = "clickhouse://default:password@clickhouse:9000?secure=false&skip_verify=true" admin_url = "clickhouse://unkey_user_admin:C57RqT5EPZBqCJkMxN9mEZZEzMPcw9yBlwhIizk99t7kx6uLi9rYmtWObsXzdl@clickhouse:9000?secure=false&skip_verify=true" + # GitHub App credentials for authenticating with the GitHub API. + # app_id is resolved at runtime via os.ExpandEnv from the github-credentials secret. + # Without a valid app_id the generated JWT has issuer "0" and GitHub returns 401. [github] - app_id = 0 + app_id = ${UNKEY_GITHUB_APP_ID} private_key_pem = """${UNKEY_GITHUB_PRIVATE_KEY_PEM}""" - allow_unauthenticated_deployments = true + # Set to true only for local dev without GitHub credentials configured. + # When true, deployments skip GitHub auth entirely works only with public repos. + # Controlled via UNKEY_ALLOW_UNAUTHENTICATED_DEPLOYMENTS in .env.github. + allow_unauthenticated_deployments = ${UNKEY_ALLOW_UNAUTHENTICATED_DEPLOYMENTS} --- apiVersion: apps/v1 @@ -110,6 +116,18 @@ spec: name: github-credentials key: UNKEY_GITHUB_PRIVATE_KEY_PEM optional: true + - name: UNKEY_GITHUB_APP_ID + valueFrom: + secretKeyRef: + name: github-credentials + key: UNKEY_GITHUB_APP_ID + optional: true + - name: UNKEY_ALLOW_UNAUTHENTICATED_DEPLOYMENTS + valueFrom: + secretKeyRef: + name: github-credentials + key: UNKEY_ALLOW_UNAUTHENTICATED_DEPLOYMENTS + optional: true volumeMounts: - name: docker-socket mountPath: /var/run/docker.sock diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx index 8a270fb98a..6b6da1fb46 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/layout.tsx @@ -12,7 +12,7 @@ function WorkspaceLoadingFallback() {
-

Loading workspace...

+

Lasdasdoading workspace...

); diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/components/card.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/components/card.tsx index 68bf8d9380..56de80d1ab 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/components/card.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/components/card.tsx @@ -8,5 +8,7 @@ export function Card({ children: ReactNode; className?: string; }) { - return
{children}
; + return ( +
{children}
+ ); } diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/build-step-logs-expanded.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/build-step-logs-expanded.tsx new file mode 100644 index 0000000000..c10e5b47b9 --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/build-step-logs-expanded.tsx @@ -0,0 +1,46 @@ +import { TimestampInfo } from "@unkey/ui"; +import { Fragment } from "react/jsx-runtime"; +import type { BuildStepRow } from "./columns"; + +export function BuildStepLogsExpanded({ step }: { step: BuildStepRow }) { + if (!step.logs || step.logs.length === 0) { + return ( + + + No logs available for this step + + + ); + } + + return ( + <> + + + + {step.logs.map((log, idx) => ( + + + + + + + + + + + + + {log.message} + + + + + ))} + + ); +} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/columns/build-steps.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/columns.tsx similarity index 90% rename from web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/columns/build-steps.tsx rename to web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/columns.tsx index f056df58a1..c2b271eb8d 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/columns/build-steps.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/columns.tsx @@ -13,14 +13,15 @@ export type BuildStepRow = BuildStep & { export const buildStepsColumns: Column[] = [ { key: "expand", - width: "32px", + width: "25px", + cellClassName: "p-0", render: (step) => step.has_logs ? ( -
+
@@ -29,8 +30,7 @@ export const buildStepsColumns: Column[] = [ }, { key: "started_at", - //header: "Started At", - width: "180px", + width: "85px", render: (step) => (
[] = [ }, { key: "name", - // header: "Step", width: "250px", render: (step) => (
{step.name}
), }, - { key: "error", - //header: "Error", - width: "300px", + width: "auto", render: (step) => { if (!step.error) { return null; @@ -89,8 +86,7 @@ export const buildStepsColumns: Column[] = [ }, { key: "duration", - //header: "Duration", - width: "10%", + width: "115px", render: (step) => { const duration = step.completed_at - step.started_at; return ( diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/deployment-build-steps-table.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/deployment-build-steps-table.tsx similarity index 64% rename from web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/deployment-build-steps-table.tsx rename to web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/deployment-build-steps-table.tsx index 7bce1b5046..6ec5db4814 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/deployment-build-steps-table.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/deployment-build-steps-table.tsx @@ -1,12 +1,19 @@ "use client"; import { VirtualTable } from "@/components/virtual-table/index"; +import { cn } from "@/lib/utils"; import { BookBookmark } from "@unkey/icons"; import { Button, Empty } from "@unkey/ui"; import { useState } from "react"; import { BuildStepLogsExpanded } from "./build-step-logs-expanded"; -import { type BuildStepRow, buildStepsColumns } from "./columns/build-steps"; -import { getBuildStepRowClass } from "./utils/get-build-step-row-class"; +import { type BuildStepRow, buildStepsColumns } from "./columns"; +import { getBuildStepRowClass } from "./get-row-class"; +import { + DurationColumnSkeleton, + NameColumnSkeleton, + StartedAtColumnSkeleton, + StatusColumnSkeleton, +} from "./skeletons"; type Props = { steps: BuildStepRow[]; @@ -15,7 +22,6 @@ type Props = { export const DeploymentBuildStepsTable: React.FC = ({ steps }) => { const [expandedIds, setExpandedIds] = useState>(new Set()); - // Enrich steps with expansion state for chevron rendering const enrichedSteps = steps.map((step) => ({ ...step, _isExpanded: expandedIds.has(step.step_id), @@ -26,6 +32,24 @@ export const DeploymentBuildStepsTable: React.FC = ({ steps }) => { data={enrichedSteps} isLoading={steps.length === 0} columns={buildStepsColumns} + renderSkeletonRow={({ columns, rowHeight }) => + columns.map((column, idx) => ( + + {column.key === "started_at" && } + {column.key === "status" && } + {column.key === "name" && } + {column.key === "duration" && } + + )) + } keyExtractor={(step) => step.step_id} rowClassName={(step) => getBuildStepRowClass(step)} expandedIds={expandedIds} @@ -33,6 +57,10 @@ export const DeploymentBuildStepsTable: React.FC = ({ steps }) => { fixedHeight={256} isExpandable={(step) => step.has_logs} renderExpanded={(step) => } + config={{ + containerPadding: "px-0 py-0", + className: "bg-transparent", + }} emptyState={
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/utils/get-build-step-row-class.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/get-row-class.ts similarity index 92% rename from web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/utils/get-build-step-row-class.ts rename to web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/get-row-class.ts index a860b087e7..acf7d92621 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/utils/get-build-step-row-class.ts +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/get-row-class.ts @@ -1,5 +1,5 @@ import { cn } from "@unkey/ui/src/lib/utils"; -import type { BuildStepRow } from "../columns/build-steps"; +import type { BuildStepRow } from "./columns"; export function getBuildStepRowClass(step: BuildStepRow): string { if (!step?.step_id) { diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/skeletons.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/skeletons.tsx new file mode 100644 index 0000000000..0eb68e46b7 --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/build-steps-table/skeletons.tsx @@ -0,0 +1,15 @@ +export const StartedAtColumnSkeleton = () => ( +
+); + +export const StatusColumnSkeleton = () => ( +
+); + +export const NameColumnSkeleton = () => ( +
+); + +export const DurationColumnSkeleton = () => ( +
+); 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 new file mode 100644 index 0000000000..145f719e3b --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-info.tsx @@ -0,0 +1,67 @@ +"use client"; + +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 { DeploymentStatusBadge } from "../../../../components/deployment-status-badge"; +import { InfoChip } from "../../../../components/info-chip"; +import { RegionFlags } from "../../../../components/region-flags"; +import { Section, SectionHeader } from "../../../../components/section"; +import { useOptionalProjectLayout } from "../../../layout-provider"; +import { useDeployment } from "../layout-provider"; + +export function DeploymentInfo({ title = "Deployment" }: { title?: string }) { + const { deployment } = useDeployment(); + const projectLayout = useOptionalProjectLayout(); + const deploymentStatus = deployment.status; + + return ( +
+ } title={title} /> + +
+ +
+ + {formatCpuParts(deployment.cpuMillicores).value} + + + {formatCpuParts(deployment.cpuMillicores).unit} + +
+
+ +
+ + {formatMemoryParts(deployment.memoryMib).value} + + + {formatMemoryParts(deployment.memoryMib).unit} + +
+
+
+ + {projectLayout && ( + + + + )} +
+ } + statusBadge={} + /> + + ); +} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-progress-section.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx similarity index 50% rename from web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-progress-section.tsx rename to web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx index 687cf1379a..a1ee7a321f 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-progress-section.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-progress.tsx @@ -1,18 +1,23 @@ "use client"; import { trpc } from "@/lib/trpc/client"; -import { cn } from "@/lib/utils"; -import { Check, ChevronRight, TriangleWarning2, Ufo } from "@unkey/icons"; -import { Badge, Loading } from "@unkey/ui"; -import ms from "ms"; +import { CloudUp, Earth, Hammer2, LayerFront } from "@unkey/icons"; +import { SettingCardGroup } from "@unkey/ui"; +import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; -import { Card } from "../../../../../components/card"; -import { useDeployment } from "../../../layout-provider"; -import { DeploymentBuildStepsTable } from "../table/deployment-build-steps-table"; -import { DeploymentInfoSection } from "./deployment-info-section"; +import { DeploymentDomainsCard } from "../../../../components/deployment-domains-card"; +import { useProjectData } from "../../../data-provider"; +import { useDeployment } from "../layout-provider"; +import { DeploymentBuildStepsTable } from "./build-steps-table/deployment-build-steps-table"; +import { DeploymentStep } from "./deployment-step"; -export function DeploymentProgressSection() { +export function DeploymentProgress() { const { deployment } = useDeployment(); + const router = useRouter(); + const params = useParams(); + const workspaceSlug = params.workspaceSlug as string; + const projectId = params.projectId as string; + const steps = trpc.deploy.deployment.steps.useQuery( { deploymentId: deployment.id, @@ -25,12 +30,15 @@ export function DeploymentProgressSection() { const buildSteps = trpc.deploy.deployment.buildSteps.useQuery( { deploymentId: deployment.id, + includeStepLogs: true, }, { refetchInterval: 1000, }, ); + const { getDomainsForDeployment } = useProjectData(); + const [now, setNow] = useState(0); useEffect(() => { const interval = setInterval(() => setNow(Date.now()), 500); @@ -40,13 +48,19 @@ export function DeploymentProgressSection() { }, []); const { building, deploying, network, queued } = steps.data ?? {}; - return ( - <> - + const domainsForDeployment = getDomainsForDeployment(deployment.id); + + useEffect(() => { + if (network?.completed) { + router.push(`/${workspaceSlug}/projects/${projectId}/deployments/${deployment.id}`); + } + }, [network?.completed, router, workspaceSlug, projectId, deployment.id]); - - } + return ( +
+ + } title="Deployment Queued" description={ queued @@ -65,10 +79,9 @@ export function DeploymentProgressSection() { ? "started" : "pending" } - defaultExpanded={true} /> - } + } title="Building Image" description={ building @@ -87,11 +100,15 @@ export function DeploymentProgressSection() { ? "started" : "pending" } - expanded={} - defaultExpanded={true} + expandable={ +
+ +
+ } + defaultExpanded /> - } + } title="Deploying Containers" description={ deploying @@ -111,13 +128,13 @@ export function DeploymentProgressSection() { : "pending" } /> - } + } title="Assigning Domains" description={ network ? network.endedAt - ? (network.error ?? "Assigned all domains") + ? (network.error ?? `Domains assigned · ${domainsForDeployment.length} records`) : "Assigning domains" : "Waiting for deployments" } @@ -132,74 +149,12 @@ export function DeploymentProgressSection() { : "pending" } /> - - - ); -} - -type StepProps = { - icon: React.ReactNode; - title: string; - description: string; - duration?: number; - status: "pending" | "started" | "completed" | "error"; - defaultExpanded?: boolean; - expanded?: React.ReactNode; -}; - -const Step: React.FC = ({ - icon, - title, - description, - duration, - status, - defaultExpanded, - expanded, -}) => { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); - - return ( -
-
-
- {icon} -
-
- {title} - {status === "completed" ? ( - - Complete - - ) : null} -
-

{description}

-
+ + {network?.completed && ( +
+
-
-
- {duration ? ms(duration) : null} - {status === "completed" ? ( - - ) : status === "started" ? ( - - ) : status === "error" ? ( - - ) : null} -
- {expanded ? ( - - ) : null} -
-
-
-
- -
{isExpanded ? expanded : null}
+ )}
); -}; +} 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 new file mode 100644 index 0000000000..e2b1063a76 --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(deployment-progress)/deployment-step.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Check, TriangleWarning2 } from "@unkey/icons"; +import { Badge, Loading, SettingCard } from "@unkey/ui"; +import ms from "ms"; + +type DeploymentStepProps = { + icon: React.ReactNode; + title: string; + description: string; + duration?: number; + status: "pending" | "started" | "completed" | "error"; + expandable?: React.ReactNode; + defaultExpanded?: boolean; +}; + +export function DeploymentStep({ + icon, + title, + description, + duration, + status, + expandable, + defaultExpanded, +}: DeploymentStepProps) { + const showGlow = status === "started"; + return ( + +
+
+ {icon} +
+
+ } + title={ +
+ {title} + + Complete + +
+ } + iconClassName={showGlow ? "bg-transparent shadow-none dark:ring-0" : undefined} + className="relative" + description={description} + expandable={expandable} + defaultExpanded={defaultExpanded} + contentWidth="w-fit" + > +
+ {duration ? ms(duration) : null} + {status === "completed" ? ( + + ) : status === "started" ? ( + + ) : status === "error" ? ( + + ) : null} +
+
+ ); +} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/metrics/metric-card.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/metrics/metric-card.tsx index a4153425ad..66f4263681 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/metrics/metric-card.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/metrics/metric-card.tsx @@ -57,7 +57,7 @@ export function MetricCard({ const config = METRIC_CONFIGS[metricType]; return ( -
+
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-domains-section.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-domains-section.tsx deleted file mode 100644 index 73ba42ca65..0000000000 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-domains-section.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { Earth } from "@unkey/icons"; -import { Section, SectionHeader } from "../../../../../../components/section"; -import { EmptySection } from "../../../../../components/empty-section"; -import { useProjectData } from "../../../../../data-provider"; -import { DomainRow, DomainRowSkeleton } from "../../../../../details/domain-row"; -import { useDeployment } from "../../../layout-provider"; - -export function DeploymentDomainsSection() { - const { deployment } = useDeployment(); - const { getDomainsForDeployment, isDomainsLoading } = useProjectData(); - const domains = getDomainsForDeployment(deployment.id); - return ( -
- } - title="Domains" - /> -
- {isDomainsLoading ? ( - <> - - - - ) : domains.length > 0 ? ( - domains.map((domain) => ( - - )) - ) : ( - - )} -
-
- ); -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-info-section.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-info-section.tsx deleted file mode 100644 index d67a016c8e..0000000000 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-info-section.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { Bolt, Cloud, Grid, Harddrive, LayoutRight } from "@unkey/icons"; -import { Button, InfoTooltip } from "@unkey/ui"; -import { ActiveDeploymentCard } from "../../../../../../components/active-deployment-card"; -import { DeploymentStatusBadge } from "../../../../../../components/deployment-status-badge"; -import { DisabledWrapper } from "../../../../../../components/disabled-wrapper"; -import { InfoChip } from "../../../../../../components/info-chip"; -import { RegionFlags } from "../../../../../../components/region-flags"; -import { Section, SectionHeader } from "../../../../../../components/section"; -import { useProjectLayout } from "../../../../../layout-provider"; -import { useDeployment } from "../../../layout-provider"; - -export function DeploymentInfoSection() { - const { deployment } = useDeployment(); - const { setIsDetailsOpen, isDetailsOpen } = useProjectLayout(); - const deploymentStatus = deployment.status; - - return ( -
- } - title="Deployment" - /> - - - -
- vCPUs -
-
- -
- GiB -
-
- -
- GB -
-
-
- - - - -
- } - statusBadge={} - /> - - ); -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-network-section.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-network-section.tsx index 4f0e6b6931..8fdcfa65c8 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-network-section.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/sections/deployment-network-section.tsx @@ -29,7 +29,7 @@ export function DeploymentNetworkSection() { title="Network" />
- +
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/build-step-logs-expanded.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/build-step-logs-expanded.tsx deleted file mode 100644 index 2565da1f20..0000000000 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/build-step-logs-expanded.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { TimestampInfo } from "@unkey/ui"; -import type { BuildStepRow } from "./columns/build-steps"; - -export function BuildStepLogsExpanded({ step }: { step: BuildStepRow }) { - if (!step.logs || step.logs.length === 0) { - return
No logs available for this step
; - } - - return ( -
-
-        {step.logs.map((log, idx) => (
-          
- - {log.message} -
- ))} -
-
- ); -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/hooks/use-deployment-build-steps-query.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/hooks/use-deployment-build-steps-query.ts deleted file mode 100644 index c186947717..0000000000 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/(overview)/components/table/hooks/use-deployment-build-steps-query.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { trpc } from "@/lib/trpc/client"; -import { useParams } from "next/navigation"; - -export function useDeploymentBuildStepsQuery() { - const params = useParams(); - const deploymentId = (params?.deploymentId as string) ?? ""; - - const { data, isLoading, error } = trpc.deploy.deployment.buildSteps.useQuery( - { - deploymentId, - includeStepLogs: true, - }, - { - enabled: Boolean(deploymentId), - refetchInterval: 2000, - }, - ); - - return { - steps: data?.steps ?? [], - isLoading, - error, - }; -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/layout-provider.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/layout-provider.tsx index eafbb33fbf..5bb2200b10 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/layout-provider.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/layout-provider.tsx @@ -12,12 +12,22 @@ type DeploymentLayoutContextType = { const DeploymentLayoutContext = createContext(null); -export const DeploymentLayoutProvider = ({ children }: { children: React.ReactNode }) => { +type DeploymentLayoutProviderProps = { + children: React.ReactNode; + deploymentId?: string; +}; + +export const DeploymentLayoutProvider = ({ + children, + deploymentId: deploymentIdProp, +}: DeploymentLayoutProviderProps) => { const params = useParams(); - const deploymentId = params?.deploymentId; + const deploymentId = + deploymentIdProp ?? + (typeof params?.deploymentId === "string" ? params.deploymentId : undefined); - if (!deploymentId || typeof deploymentId !== "string") { - throw new Error("DeploymentLayoutProvider must be used within a deployment route"); + if (!deploymentId) { + throw new Error("DeploymentLayoutProvider requires a deploymentId (via prop or route params)"); } const { getDeploymentById, isDeploymentsLoading } = useProjectData(); diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/deployment-network-view.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/deployment-network-view.tsx index aa0fc148a3..6d31eb8df3 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/deployment-network-view.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/deployment-network-view.tsx @@ -47,8 +47,7 @@ export function DeploymentNetworkView({ return ( {showNodeDetails && ( diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/card-footer.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/card-footer.tsx index 9231d10985..63a36dfde0 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/card-footer.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/card-footer.tsx @@ -1,5 +1,5 @@ import { RegionFlag } from "@/app/(app)/[workspaceSlug]/projects/[projectId]/components/region-flag"; -import { formatCpu, formatMemory } from "@/lib/utils/deployment-formatters"; +import { formatCpuParts, formatMemoryParts } from "@/lib/utils/deployment-formatters"; import { Bolt, ChartActivity, Focus } from "@unkey/icons"; import type { SentinelNode } from "../types"; import { MetricPill } from "./metric-pill"; @@ -37,24 +37,42 @@ export function CardFooter(props: CardFooterProps) { /> )}
- {cpu !== undefined && ( - } - value={formatCpu(cpu)} - tooltip={ - isSentinel ? "CPU allocated to this sentinel" : "CPU allocated to this instance" - } - /> - )} - {memory !== undefined && ( - } - value={formatMemory(memory)} - tooltip={ - isSentinel ? "Memory allocated to this sentinel" : "Memory allocated to this instance" - } - /> - )} + {cpu !== undefined && + (() => { + const parts = formatCpuParts(cpu); + return ( + } + value={ + <> + {parts.value} {parts.unit} + + } + tooltip={ + isSentinel ? "CPU allocated to this sentinel" : "CPU allocated to this instance" + } + /> + ); + })()} + {memory !== undefined && + (() => { + const parts = formatMemoryParts(memory); + return ( + } + value={ + <> + {parts.value} {parts.unit} + + } + tooltip={ + isSentinel + ? "Memory allocated to this sentinel" + : "Memory allocated to this instance" + } + /> + ); + })()}
); diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/metric-pill.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/metric-pill.tsx index bfde1490ec..44a3ede3e1 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/metric-pill.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/nodes/components/metric-pill.tsx @@ -2,7 +2,7 @@ import { InfoTooltip } from "@unkey/ui"; type MetricPillProps = { icon: React.ReactNode; - value: string | number; + value: React.ReactNode; tooltip: string; }; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/overlay/node-details-panel/region-node/sentinel-instances.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/overlay/node-details-panel/region-node/sentinel-instances.tsx index a25c207870..52557c1f1a 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/overlay/node-details-panel/region-node/sentinel-instances.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/[deploymentId]/network/unkey-flow/components/overlay/node-details-panel/region-node/sentinel-instances.tsx @@ -1,4 +1,4 @@ -import { formatCpu, formatMemory } from "@/lib/utils/deployment-formatters"; +import { formatCpuParts, formatMemoryParts } from "@/lib/utils/deployment-formatters"; import { Bolt, ChartActivity, CircleCheck, Focus, Heart, Layers2 } from "@unkey/icons"; import type { DeploymentNode } from "../../../nodes"; import { MetricPill } from "../../../nodes/components/metric-pill"; @@ -44,16 +44,34 @@ export function SentinelInstances({ instances }: SentinelInstancesProps) { value={`${rps ?? 0} RPS`} tooltip="Avg. RPS over last 15 min (updated every 5s)" /> - } - value={formatCpu(cpu ?? 0)} - tooltip="CPU allocated to this instance" - /> - } - value={formatMemory(memory ?? 0)} - tooltip="Memory allocated to this instance" - /> + {(() => { + const parts = formatCpuParts(cpu ?? 0); + return ( + } + value={ + <> + {parts.value} {parts.unit} + + } + tooltip="CPU allocated to this instance" + /> + ); + })()} + {(() => { + const parts = formatMemoryParts(memory ?? 0); + return ( + } + value={ + <> + {parts.value} {parts.unit} + + } + tooltip="Memory allocated to this instance" + /> + ); + })()}
- - - ); - } - return ( - - - + + {ready ? ( +
+ + +
+ ) : ( +
+ +
+ )}
); } diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/components/deployment-card.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/components/deployment-card.tsx index 4878db1931..c4b70a0320 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/components/deployment-card.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/deployments/components/table/components/actions/components/deployment-card.tsx @@ -11,7 +11,7 @@ type DeploymentCardProps = { }; export const DeploymentCard = ({ deployment, isLive, showSignal }: DeploymentCardProps) => ( -
+
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 2873e105fa..ba13081ba4 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 @@ -4,8 +4,8 @@ import type { Column } from "@/components/virtual-table/types"; import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation"; import type { Deployment, Environment } from "@/lib/collections"; import { shortenId } from "@/lib/shorten-id"; -import { formatCpu, formatMemory } from "@/lib/utils/deployment-formatters"; -import { BookBookmark, CodeBranch, Cube } from "@unkey/icons"; +import { formatCpuParts, formatMemoryParts } from "@/lib/utils/deployment-formatters"; +import { Bolt, BookBookmark, CodeBranch, Connections3, ScanCode } from "@unkey/icons"; import { Button, Empty, TimestampInfo } from "@unkey/ui"; import { cn } from "@unkey/ui/src/lib/utils"; import dynamic from "next/dynamic"; @@ -65,7 +65,7 @@ export const DeploymentsList = () => { { key: "deployment_id", header: "Deployment ID", - width: "15%", + width: "12%", headerClassName: "pl-[18px]", render: ({ deployment, environment }) => { const isLive = liveDeploymentId === deployment.id; @@ -89,7 +89,7 @@ export const DeploymentsList = () => { {project?.isRolledBack ? ( ) : ( - + )}
) : null} @@ -111,13 +111,13 @@ export const DeploymentsList = () => { { key: "status", header: "Status", - width: "15%", + width: "10%", render: ({ deployment }) => , }, { key: "domains", header: "Domains", - width: "25%", + width: "20%", render: ({ deployment }) => { return (
@@ -129,15 +129,15 @@ export const DeploymentsList = () => { { key: "instances" as const, header: "Instances", - width: "10%", + width: "8%", headerClassName: "hidden 2xl:table-cell", cellClassName: "hidden 2xl:table-cell", render: ({ deployment }: { deployment: Deployment }) => { return deployment.status === "failed" ? ( ) : ( -
- +
+
{deployment.instances.length} @@ -151,24 +151,27 @@ export const DeploymentsList = () => { { key: "size" as const, header: "Size", - width: "15%", + width: "14%", render: ({ deployment }: { deployment: Deployment }) => { - return deployment.status === "failed" ? ( - - ) : ( -
- -
+ if (deployment.status === "failed") { + return ; + } + const cpu = formatCpuParts(deployment.cpuMillicores); + const mem = formatMemoryParts(deployment.memoryMib); + return ( +
+
+
- - {formatCpu(deployment.cpuMillicores)} - + {cpu.value} + {cpu.unit}
- / +
+
+
- - {formatMemory(deployment.memoryMib)} - + {mem.value} + {mem.unit}
@@ -178,7 +181,7 @@ export const DeploymentsList = () => { { key: "source", header: "Source", - width: "15%", + width: "12%", headerClassName: "hidden 2xl:table-cell", cellClassName: "hidden 2xl:table-cell", render: ({ deployment }) => { @@ -214,7 +217,7 @@ export const DeploymentsList = () => { { key: "created_at" as const, header: "Created", - width: "10%", + width: "8%", render: ({ deployment }: { deployment: Deployment }) => { return ( { { key: "author" as const, header: "Author", - width: "10%", + width: "8%", headerClassName: "hidden 2xl:table-cell", cellClassName: "hidden 2xl:table-cell", render: ({ deployment }: { deployment: Deployment }) => { @@ -248,7 +251,7 @@ export const DeploymentsList = () => { { key: "action", header: "", - width: "5%", + width: "4%", render: ({ deployment, environment, diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/project-details-expandables/sections.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/project-details-expandables/sections.tsx index e57ba7c242..18a16822f9 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/project-details-expandables/sections.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/details/project-details-expandables/sections.tsx @@ -1,5 +1,5 @@ import type { Deployment } from "@/lib/collections"; -import { formatCpu, formatMemory } from "@/lib/utils/deployment-formatters"; +import { formatCpuParts, formatMemoryParts } from "@/lib/utils/deployment-formatters"; import { Bolt, ChartActivity, @@ -141,16 +141,28 @@ export const createDetailSections = ( { icon: , label: "CPU", - content: ( - {formatCpu(details.cpuMillicores)} - ), + content: (() => { + const cpu = formatCpuParts(details.cpuMillicores); + return ( + + {cpu.value}{" "} + {cpu.unit} + + ); + })(), }, { icon: , label: "Memory", - content: ( - {formatMemory(details.memoryMib)} - ), + content: (() => { + const mem = formatMemoryParts(details.memoryMib); + return ( + + {mem.value}{" "} + {mem.unit} + + ); + })(), }, { icon: , diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/layout-provider.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/layout-provider.tsx index 59667ebee5..500e63f4c4 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/layout-provider.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/layout-provider.tsx @@ -14,3 +14,7 @@ export const useProjectLayout = () => { } return context; }; + +export const useOptionalProjectLayout = (): ProjectLayoutContextType | null => { + return useContext(ProjectLayoutContext); +}; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/navigations/use-breadcrumb-config.ts b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/navigations/use-breadcrumb-config.ts index a03c1af426..ce0be0311e 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/navigations/use-breadcrumb-config.ts +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/navigations/use-breadcrumb-config.ts @@ -55,12 +55,6 @@ export const useBreadcrumbConfig = ({ // Sub-pages configuration - matches the existing structure const subPages: SubPage[] = [ - { - id: "overview", - label: "Overview", - href: `${basePath}/${projectId}`, - segment: undefined, - }, { id: "deployments", label: "Deployments", diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/openapi-diff/page.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/openapi-diff/page.tsx index 95b94448e1..2a74696cfb 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/openapi-diff/page.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/openapi-diff/page.tsx @@ -100,7 +100,7 @@ export default function DiffPage() { return ( - + {/* Header Section */}
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx index 8d0ca81ab8..0c9393bb13 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx @@ -1,7 +1,7 @@ "use client"; import { collection } from "@/lib/collections"; -import { formatCpu } from "@/lib/utils/deployment-formatters"; +import { formatCpuParts } from "@/lib/utils/deployment-formatters"; import { zodResolver } from "@hookform/resolvers/zod"; import { Bolt } from "@unkey/icons"; import { Slider } from "@unkey/ui"; @@ -73,11 +73,11 @@ export const Cpu = () => { title="CPU" description="CPU allocation for each instance" displayValue={(() => { - const [value, unit] = parseCpuDisplay(defaultCpu); + const parts = formatCpuParts(defaultCpu); return (
- {value} - {unit} + {parts.value} + {parts.unit}
); })()} @@ -105,7 +105,8 @@ export const Cpu = () => { }} /> - {formatCpu(currentCpu)} + {formatCpuParts(currentCpu).value}{" "} + {formatCpuParts(currentCpu).unit}
@@ -115,19 +116,3 @@ export const Cpu = () => { ); }; - -function parseCpuDisplay(millicores: number): [string, string] { - if (millicores === 256) { - return ["1/4", "vCPU"]; - } - if (millicores === 512) { - return ["1/2", "vCPU"]; - } - if (millicores === 768) { - return ["3/4", "vCPU"]; - } - if (millicores >= 1024 && millicores % 1024 === 0) { - return [`${millicores / 1024}`, "vCPU"]; - } - return [`${millicores}m`, "vCPU"]; -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx index 487865383a..a6ab7a084b 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx @@ -1,7 +1,7 @@ "use client"; import { collection } from "@/lib/collections"; -import { formatMemory } from "@/lib/utils/deployment-formatters"; +import { formatMemoryParts } from "@/lib/utils/deployment-formatters"; import { zodResolver } from "@hookform/resolvers/zod"; import { ScanCode } from "@unkey/icons"; import { Slider } from "@unkey/ui"; @@ -73,11 +73,11 @@ export const Memory = () => { title="Memory" description="Memory allocation for each instance" displayValue={(() => { - const [value, unit] = parseMemoryDisplay(defaultMemory); + const parts = formatMemoryParts(defaultMemory); return (
- {value} - {unit} + {parts.value} + {parts.unit}
); })()} @@ -108,7 +108,10 @@ export const Memory = () => { }} /> - {formatMemory(currentMemory)} + + {formatMemoryParts(currentMemory).value} + {" "} + {formatMemoryParts(currentMemory).unit}
@@ -119,10 +122,3 @@ export const Memory = () => { ); }; - -function parseMemoryDisplay(mib: number): [string, string] { - if (mib >= 1024) { - return [`${(mib / 1024).toFixed(mib % 1024 === 0 ? 0 : 1)}`, "GiB"]; - } - return [`${mib}`, "MiB"]; -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx index 26da87aeaf..3960822cff 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx @@ -1,6 +1,6 @@ "use client"; -import { formatMemory } from "@/lib/utils/deployment-formatters"; +import { formatMemoryParts } from "@/lib/utils/deployment-formatters"; import { zodResolver } from "@hookform/resolvers/zod"; import { Harddrive } from "@unkey/icons"; import { Slider } from "@unkey/ui"; @@ -63,11 +63,11 @@ const StorageForm: React.FC = ({ defaultStorage }) => { title="Storage" description="Ephemeral disk space per instance" displayValue={(() => { - const [value, unit] = parseStorageDisplay(defaultStorage); + const parts = formatMemoryParts(defaultStorage); return (
- {value} - {unit} + {parts.value} + {parts.unit}
); })()} @@ -98,7 +98,10 @@ const StorageForm: React.FC = ({ defaultStorage }) => { }} /> - {formatMemory(currentStorage)} + + {formatMemoryParts(currentStorage).value} + {" "} + {formatMemoryParts(currentStorage).unit}
@@ -108,10 +111,3 @@ const StorageForm: React.FC = ({ defaultStorage }) => { ); }; - -function parseStorageDisplay(mib: number): [string, string] { - if (mib >= 1024) { - return [`${(mib / 1024).toFixed(mib % 1024 === 0 ? 0 : 1)}`, "GiB"]; - } - return [`${mib}`, "MiB"]; -} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx index f53e18ffbb..6c95af15b8 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/settings-group.tsx @@ -5,9 +5,10 @@ import React, { useState } from "react"; type SettingsGroupProps = { icon: React.ReactNode; - title: string; + title: React.ReactNode; children: React.ReactNode; defaultExpanded?: boolean; + hideChevron?: boolean; }; export const SettingsGroup = ({ @@ -15,6 +16,7 @@ export const SettingsGroup = ({ title, children, defaultExpanded = true, + hideChevron = false, }: SettingsGroupProps) => { const [expanded, setExpanded] = useState(defaultExpanded); @@ -30,15 +32,19 @@ export const SettingsGroup = ({ onClick={() => setExpanded((prev) => !prev)} className="flex items-center gap-1 text-xs text-gray-10 hover:text-gray-11 transition-colors group duration-300" > - {expanded ? "Hide" : "Show"} - + {!hideChevron && ( + <> + {expanded ? "Hide" : "Show"} + + + )}
( - +
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/active-deployment-card/index.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/active-deployment-card/index.tsx index febb5c6d50..1afd6b5614 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/active-deployment-card/index.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/active-deployment-card/index.tsx @@ -34,7 +34,7 @@ export const ActiveDeploymentCard = ({ } return ( - +
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/deployment-domains-card.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/deployment-domains-card.tsx new file mode 100644 index 0000000000..ecfc3ddfe1 --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/deployment-domains-card.tsx @@ -0,0 +1,155 @@ +"use client"; + +import type { Domain } from "@/lib/collections"; +import { cn } from "@/lib/utils"; +import { ChevronDown, Cube, Earth, Link4 } from "@unkey/icons"; +import { + Button, + CopyButton, + Popover, + PopoverContent, + PopoverTrigger, + SettingCard, + SettingCardGroup, +} from "@unkey/ui"; +import { type ReactNode, useState } from "react"; +import { useProjectData } from "../(overview)/data-provider"; +import { useDeployment } from "../(overview)/deployments/[deploymentId]/layout-provider"; +import { SettingsGroup } from "../(overview)/settings/components/shared/settings-group"; + +export function DeploymentDomainsCard({ + emptyState, + glow, + domainFilter, +}: { emptyState?: ReactNode; glow?: boolean; domainFilter?: (d: Domain) => boolean }) { + const [urlsOpen, setUrlsOpen] = useState(false); + const { deployment } = useDeployment(); + const { getDomainsForDeployment, isDomainsLoading, project } = useProjectData(); + + const domainsForDeployment = getDomainsForDeployment(deployment.id); + const filtered = domainFilter ? domainsForDeployment.filter(domainFilter) : domainsForDeployment; + const sortedDomains = [...filtered].sort((a, b) => + a.fullyQualifiedDomainName.localeCompare(b.fullyQualifiedDomainName), + ); + const environmentDomain = sortedDomains.find((d) => d.sticky === "environment"); + const primaryDomain = environmentDomain ?? sortedDomains[0]; + const additionalDomains = primaryDomain + ? sortedDomains.filter((d) => d.id !== primaryDomain.id) + : []; + + if (!isDomainsLoading && sortedDomains.length === 0) { + return emptyState ?? null; + } + + return ( + } + title={Domains} + hideChevron + > + + {isDomainsLoading ? ( + + +
+ } + title={
} + description="Loading domains..." + /> + ) : ( + +
+
+ +
+
+ ) : ( +
+ +
+ ) + } + title={project?.name} + description={ +
+ + {primaryDomain.fullyQualifiedDomainName} + + {additionalDomains.length > 0 && ( + + )} +
+ } + contentWidth="w-fit" + > +
+ {additionalDomains.length > 0 && ( + + + + + + {sortedDomains.map((d) => ( + + ))} + + + )} +
+
+ )} + + + ); +} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/project-content-wrapper.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/project-content-wrapper.tsx index 2210103fa6..5bf1c01cdc 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/project-content-wrapper.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/project-content-wrapper.tsx @@ -36,7 +36,7 @@ export function ProjectContentWrapper({ )} > {centered ? ( -
+
{children}
) : ( diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/section.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/section.tsx index 4c696e7c9e..61a9dd8fae 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/section.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/components/section.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; export function SectionHeader({ icon, title }: { icon: ReactNode; title: string }) { return ( -
+
{icon}
{title}
@@ -10,5 +10,5 @@ export function SectionHeader({ icon, title }: { icon: ReactNode; title: string } export function Section({ children }: { children: ReactNode }) { - return
{children}
; + return
{children}
; } diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/page.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/page.tsx index fe14a0f7a7..92fcfb8962 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/page.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/page.tsx @@ -1,67 +1,8 @@ -"use client"; -import { Cloud, Earth } from "@unkey/icons"; -import { EmptySection } from "./(overview)/components/empty-section"; -import { useProjectData } from "./(overview)/data-provider"; -import { DeploymentLogsProvider } from "./(overview)/details/active-deployment-card-logs/providers/deployment-logs-provider"; -import { DomainRow, DomainRowSkeleton } from "./(overview)/details/domain-row"; -import { ActiveDeploymentCard } from "./components/active-deployment-card"; -import { DeploymentStatusBadge } from "./components/deployment-status-badge"; -import { ProjectContentWrapper } from "./components/project-content-wrapper"; -import { Section, SectionHeader } from "./components/section"; +import { redirect } from "next/navigation"; -export default function ProjectDetails() { - const { getDomainsForDeployment, isDomainsLoading, getDeploymentById, project } = - useProjectData(); - - const liveDeploymentId = project?.liveDeploymentId; - - // Get domains for live deployment - const domains = liveDeploymentId - ? getDomainsForDeployment(liveDeploymentId).filter((d) => d.sticky === "live") - : []; - - // Get deployment from provider - const deploymentStatus = liveDeploymentId - ? getDeploymentById(liveDeploymentId)?.status - : undefined; - - return ( - -
- } - title="Live Deployment" - /> - - } - /> - {" "} -
-
- } - title="Domains" - /> -
- {isDomainsLoading ? ( - <> - - - - ) : domains.length > 0 ? ( - domains.map((domain) => ( - - )) - ) : ( - - )} -
-
-
- ); +export default async function ProjectDetails(props: { + params: Promise<{ workspaceSlug: string; projectId: string }>; +}) { + const { workspaceSlug, projectId } = await props.params; + redirect(`/${workspaceSlug}/projects/${projectId}/deployments`); } diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card-skeleton.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card-skeleton.tsx index e52ae9eae0..c865e793b8 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card-skeleton.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card-skeleton.tsx @@ -6,8 +6,8 @@ export const ProjectCardSkeleton = () => {
{/* Top Section */}
-
-
+
+
diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card.tsx index 6d474bf51f..bbffded09a 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/projects-card.tsx @@ -54,7 +54,7 @@ export const ProjectCard = ({ /> {/*Top Section*/}
-
+
{isNavigating ? ( ) : ( diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/index.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/index.tsx index 567aa8d30e..f9f86391ab 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/index.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/index.tsx @@ -1,5 +1,6 @@ "use client"; import { collection } from "@/lib/collections"; +import { trpc } from "@/lib/trpc/client"; import { useLiveQuery } from "@tanstack/react-db"; import { StepWizard } from "@unkey/ui"; import { useSearchParams } from "next/navigation"; @@ -8,6 +9,7 @@ import { OnboardingStepHeader } from "./onboarding-step-header"; import { ConfigureDeploymentStep } from "./steps/configure-deployment"; import { ConnectGithubStep } from "./steps/connect-github"; import { CreateProjectStep } from "./steps/create-project"; +import { DeploymentLiveStep } from "./steps/deployment-live"; import { SelectRepo } from "./steps/select-repo"; export const Onboarding = () => { @@ -23,6 +25,9 @@ export const Onboarding = () => { const initialProjectId = searchParams.get("projectId") ?? undefined; const [projectId, setProjectId] = useState(initialProjectId ?? null); + const [deploymentId, setDeploymentId] = useState(null); + + const { data: installationData } = trpc.github.hasInstallations.useQuery(); return (
@@ -43,24 +48,26 @@ export const Onboarding = () => {
- - {projectId ? ( -
- - Connect a GitHub repo and get a live URL in minutes. -
- Unkey handles builds, infra, scaling, and routing. - - } - /> - -
- ) : null} -
+ {!installationData?.hasInstallation && ( + + {projectId ? ( +
+ + Connect a GitHub repo and get a live URL in minutes. +
+ Unkey handles builds, infra, scaling, and routing. + + } + /> + +
+ ) : null} +
+ )} {projectId ? (
@@ -86,10 +93,18 @@ export const Onboarding = () => { subtitle="Review the defaults. Edit anything you'd like to adjust." allowBack /> - +
) : null}
+ + {projectId && deploymentId ? ( + + ) : null} +
); diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/onboarding-step-header.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/onboarding-step-header.tsx index 38c879bf67..03629ce298 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/onboarding-step-header.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/onboarding-step-header.tsx @@ -13,7 +13,7 @@ type IconBoxProps = { const IconBox = ({ children, large, className }: IconBoxProps) => (
( ); type OnboardingStepHeaderProps = { - title: string; + title: ReactNode; subtitle?: ReactNode; showIconRow?: boolean; allowBack?: boolean; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/configure-deployment.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/configure-deployment.tsx index f7e70a9845..4a54e998db 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/configure-deployment.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/configure-deployment.tsx @@ -1,29 +1,31 @@ "use client"; -import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation"; import { queryClient } from "@/lib/collections/client"; import { trpc } from "@/lib/trpc/client"; -import { Button, toast } from "@unkey/ui"; -import { useRouter } from "next/navigation"; +import { Button, toast, useStepWizard } from "@unkey/ui"; import { ProjectDataProvider } from "../../[projectId]/(overview)/data-provider"; import { DeploymentSettings } from "../../[projectId]/(overview)/settings/deployment-settings"; import { OnboardingEnvironmentSettingsProvider } from "./onboarding-environment-provider"; type ConfigureDeploymentStepProps = { projectId: string; + onDeploymentCreated: (deploymentId: string) => void; }; -export const ConfigureDeploymentStep = ({ projectId }: ConfigureDeploymentStepProps) => { - const router = useRouter(); - const workspace = useWorkspaceNavigation(); +export const ConfigureDeploymentStep = ({ + projectId, + onDeploymentCreated, +}: ConfigureDeploymentStepProps) => { + const { next } = useStepWizard(); const deploy = trpc.deploy.deployment.create.useMutation({ - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["deployments", projectId] }); + onSuccess: async (data) => { + await queryClient.invalidateQueries({ queryKey: ["deployments", projectId] }); toast.success("Deployment triggered", { description: "Your project is being built and deployed", }); - router.push(`/${workspace.slug}/projects/${projectId}/deployments/${data.deploymentId}`); + onDeploymentCreated(data.deploymentId); + next(); }, onError: (error) => { toast.error("Deployment failed", { description: error.message }); diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/deployment-live.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/deployment-live.tsx new file mode 100644 index 0000000000..19f289c887 --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/deployment-live.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useWorkspaceNavigation } from "@/hooks/use-workspace-navigation"; +import { Check } from "@unkey/icons"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, 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 { + DeploymentLayoutProvider, + useDeployment, +} from "../../[projectId]/(overview)/deployments/[deploymentId]/layout-provider"; +import { OnboardingStepHeader } from "../onboarding-step-header"; + +type DeploymentLiveStepProps = { + projectId: string; + deploymentId: string; +}; + +export const DeploymentLiveStep = ({ projectId, deploymentId }: DeploymentLiveStepProps) => { + return ( + + + + + + ); +}; + +const REDIRECT_DELAY_SECONDS = 15; + +const DeploymentLiveStepContent = ({ projectId }: { projectId: string }) => { + const { deployment } = useDeployment(); + const workspace = useWorkspaceNavigation(); + const router = useRouter(); + const ready = deployment.status === "ready"; + const [countdown, setCountdown] = useState(REDIRECT_DELAY_SECONDS); + + const deploymentUrl = `/${workspace.slug}/projects/${projectId}/deployments/${deployment.id}`; + + useEffect(() => { + if (!ready) { + return; + } + + const interval = setInterval(() => { + setCountdown((prev) => (prev <= 1 ? 0 : prev - 1)); + }, 1000); + + return () => clearInterval(interval); + }, [ready]); + + useEffect(() => { + if (countdown === 0) { + router.replace(deploymentUrl); + } + }, [countdown, router, deploymentUrl]); + + return ( +
+ + Deployment complete! + + + ) : ( + "Deploying your project" + ) + } + subtitle={ + ready ? ( + <> + Redirecting to project overview in{" "} + + {countdown} + {" "} + seconds …{" "} + + Go now + + + ) : ( + "Building, provisioning infrastructure, and assigning domains..." + ) + } + /> +
+ + +
+
+ ); +}; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/index.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/index.tsx index 42ae2c72ee..8db1d5f396 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/index.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/index.tsx @@ -1,8 +1,9 @@ import { Combobox } from "@/components/ui/combobox"; import { trpc } from "@/lib/trpc/client"; +import { useVirtualizer } from "@tanstack/react-virtual"; import { Check, Github, Magnifier, XMark } from "@unkey/icons"; import { Input, toast, useStepWizard } from "@unkey/ui"; -import { useMemo, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { RepoListItem } from "./repo-list-item"; import { SelectRepoSkeleton } from "./skeleton"; @@ -68,6 +69,14 @@ export const SelectRepo = ({ [reposData?.repositories, selectedOwner, searchQuery], ); + const parentRef = useRef(null); + const virtualizer = useVirtualizer({ + count: filteredRepos.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 77, + overscan: 3, + }); + const handleSelectOwner = (value: string) => { setSelectedOwner(value); setSearchQuery(""); @@ -141,19 +150,41 @@ export const SelectRepo = ({ {(reposData?.repositories ?? []).length > 0 && (filteredRepos.length > 0 ? ( -
    - {filteredRepos.map((repo) => ( -
  • - -
  • - ))} -
+
+
+ {virtualizer.getVirtualItems().map((virtualRow) => { + const repo = filteredRepos[virtualRow.index]; + return ( +
+ +
+ ); + })} +
+
) : (

No repositories found

diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/language-icon.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/language-icon.tsx new file mode 100644 index 0000000000..566527f0ae --- /dev/null +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/language-icon.tsx @@ -0,0 +1,41 @@ +import type { IconProps } from "@unkey/icons"; +import { + BracketsCurly, + LangElixir, + LangGo, + LangJava, + LangJavascript, + LangPhp, + LangPython, + LangRuby, + LangRust, + LangTypescript, +} from "@unkey/icons"; + +const languageIconMap: Record React.JSX.Element> = { + TypeScript: LangTypescript, + JavaScript: LangJavascript, + Python: LangPython, + Go: LangGo, + Rust: LangRust, + Java: LangJava, + Ruby: LangRuby, + PHP: LangPhp, + Elixir: LangElixir, +}; + +export const LanguageIcon = ({ language }: { language: string | null }) => { + const Icon = language ? languageIconMap[language] : undefined; + + return Icon ? ( +
+ +
+ ) : ( +
+
+ +
+
+ ); +}; diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/repo-list-item.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/repo-list-item.tsx index 5bbf5e63ed..412be2f303 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/repo-list-item.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/repo-list-item.tsx @@ -1,5 +1,5 @@ import { trpc } from "@/lib/trpc/client"; -import { Check, ChevronDown, CircleDotted, CodeBranch } from "@unkey/icons"; +import { Check, ChevronDown, CodeBranch } from "@unkey/icons"; import { Button, Select, @@ -10,6 +10,7 @@ import { TimestampInfo, } from "@unkey/ui"; import { useState } from "react"; +import { LanguageIcon } from "./language-icon"; export type RepoItem = { id: number; @@ -17,6 +18,7 @@ export type RepoItem = { installationId: number; defaultBranch: string; pushedAt: string | null; + language: string | null; }; export const RepoListItem = ({ @@ -52,9 +54,7 @@ export const RepoListItem = ({ return (
-
- -
+
{repoName} diff --git a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/skeleton.tsx b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/skeleton.tsx index 8855bd5765..9220fb6408 100644 --- a/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/skeleton.tsx +++ b/web/apps/dashboard/app/(app)/[workspaceSlug]/projects/onboarding/steps/select-repo/skeleton.tsx @@ -2,7 +2,7 @@ import { CircleDotted } from "@unkey/icons"; export const RepoListItemSkeleton = () => (
-
+
@@ -27,7 +27,7 @@ export const SelectRepoSkeleton = () => (
-
    +
      {Array.from({ length: 3 }).map((_, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: static skeleton list
    • diff --git a/web/apps/dashboard/app/(app)/integrations/github/callback/page.tsx b/web/apps/dashboard/app/(app)/integrations/github/callback/page.tsx index 8baa9b357f..08cd28954a 100644 --- a/web/apps/dashboard/app/(app)/integrations/github/callback/page.tsx +++ b/web/apps/dashboard/app/(app)/integrations/github/callback/page.tsx @@ -1,6 +1,5 @@ "use client"; - -import { PageLoading } from "@/components/dashboard/page-loading"; +import { LoadingState } from "@/components/loading-state"; import { trpc } from "@/lib/trpc/client"; import { Empty, toast } from "@unkey/ui"; import { useRouter, useSearchParams } from "next/navigation"; @@ -78,5 +77,5 @@ export default function Page() { ); } - return ; + return ; } diff --git a/web/apps/dashboard/components/loading-state.tsx b/web/apps/dashboard/components/loading-state.tsx index 18689f6b78..c4eed9d711 100644 --- a/web/apps/dashboard/components/loading-state.tsx +++ b/web/apps/dashboard/components/loading-state.tsx @@ -6,7 +6,7 @@ export function LoadingState({ message = "Loading..." }: { message?: string }) {
      -

      {message}

      +

      {message}

diff --git a/web/apps/dashboard/components/stats-card/index.tsx b/web/apps/dashboard/components/stats-card/index.tsx index 426bc8c663..4297301098 100644 --- a/web/apps/dashboard/components/stats-card/index.tsx +++ b/web/apps/dashboard/components/stats-card/index.tsx @@ -24,10 +24,10 @@ export const StatsCard = ({ icon = , }: StatsCardProps) => { return ( -
+
{chart}
-
+
diff --git a/web/apps/dashboard/components/virtual-table/constants.ts b/web/apps/dashboard/components/virtual-table/constants.ts index 3fdf1603a5..f1af705a6b 100644 --- a/web/apps/dashboard/components/virtual-table/constants.ts +++ b/web/apps/dashboard/components/virtual-table/constants.ts @@ -10,4 +10,5 @@ export const DEFAULT_CONFIG: TableConfig = { rowBorders: false, // Default to no borders containerPadding: "px-2", // Default container padding rowSpacing: 4, // Default spacing between rows (classic mode) + className: "bg-white dark:bg-black", // Default background } as const; diff --git a/web/apps/dashboard/components/virtual-table/index.tsx b/web/apps/dashboard/components/virtual-table/index.tsx index a29510552c..7a05bf7d03 100644 --- a/web/apps/dashboard/components/virtual-table/index.tsx +++ b/web/apps/dashboard/components/virtual-table/index.tsx @@ -104,6 +104,7 @@ export const VirtualTable = forwardRef>( }; const hasPadding = config.containerPadding !== "px-0"; + const hasHeaders = columns.some((col) => col.header); const calculatedHeight = useTableHeight(containerRef); const fixedHeight = fixedHeightProp ?? calculatedHeight; @@ -152,7 +153,8 @@ export const VirtualTable = forwardRef>( ); const containerClassName = cn( - "overflow-auto relative pb-4 bg-white dark:bg-black ", + "overflow-auto relative pb-4", + config.className, config.containerPadding || "px-2", ); @@ -181,27 +183,29 @@ export const VirtualTable = forwardRef>( ))} - - - {columns.map((column) => ( - -
{column.header}
+ {hasHeaders && ( + + + {columns.map((column) => ( + +
{column.header}
+ + ))} + + + +
- ))} - - - -
- - - + + + )} {emptyState ? (
{emptyState}
@@ -225,34 +229,36 @@ export const VirtualTable = forwardRef>( ))} - - - {columns.map((column) => ( - - - - ))} - - - -
-
+ + {columns.map((column) => ( + -
- - - + > + + + ))} + + + +
+
+
+ + + + )} >( ))} - {isExpanded && renderExpanded && ( - - - {renderExpanded(typedItem)} - - - )} + {isExpanded && renderExpanded?.(typedItem)} ); } @@ -467,13 +467,7 @@ export const VirtualTable = forwardRef>( ))} - {isExpanded && renderExpanded && ( - - - {renderExpanded(typedItem)} - - - )} + {isExpanded && renderExpanded?.(typedItem)} ); })} diff --git a/web/apps/dashboard/components/virtual-table/types.ts b/web/apps/dashboard/components/virtual-table/types.ts index 06ea04af1e..89cac72dc5 100644 --- a/web/apps/dashboard/components/virtual-table/types.ts +++ b/web/apps/dashboard/components/virtual-table/types.ts @@ -39,6 +39,7 @@ export interface TableConfig { rowBorders?: boolean; // Add borders between rows containerPadding?: string; // Custom padding for container (e.g., 'px-0', 'px-4', 'p-2') rowSpacing?: number; // Space between rows in pixels (for classic mode) + className?: string; // Custom background class for container and sticky header } export type VirtualTableProps = { diff --git a/web/apps/dashboard/lib/github.ts b/web/apps/dashboard/lib/github.ts index 4844dbbe4d..c90857e3f6 100644 --- a/web/apps/dashboard/lib/github.ts +++ b/web/apps/dashboard/lib/github.ts @@ -10,6 +10,7 @@ const gitHubRepositorySchema = z.object({ html_url: z.string(), default_branch: z.string(), pushed_at: z.string().nullable(), + language: z.string().nullable(), }); export type GitHubRepository = z.infer; diff --git a/web/apps/dashboard/lib/trpc/routers/github.ts b/web/apps/dashboard/lib/trpc/routers/github.ts index d0f0702b11..f71ebb4170 100644 --- a/web/apps/dashboard/lib/trpc/routers/github.ts +++ b/web/apps/dashboard/lib/trpc/routers/github.ts @@ -114,6 +114,14 @@ const fetchProjectInstallation = async ( }; export const githubRouter = t.router({ + hasInstallations: workspaceProcedure.query(async ({ ctx }) => { + const installation = await db.query.githubAppInstallations.findFirst({ + where: (table, { eq }) => eq(table.workspaceId, ctx.workspace.id), + columns: { pk: true }, + }); + return { hasInstallation: Boolean(installation) }; + }), + registerInstallation: workspaceProcedure .input( z.object({ @@ -238,6 +246,7 @@ export const githubRouter = t.router({ defaultBranch: string; installationId: number; pushedAt: string | null; + language: string | null; }> = []; for (const installation of githubContext.installations) { @@ -260,6 +269,7 @@ export const githubRouter = t.router({ defaultBranch: repo.default_branch, installationId: installation.installationId, pushedAt: repo.pushed_at, + language: repo.language, }); } } diff --git a/web/apps/dashboard/lib/utils/deployment-formatters.ts b/web/apps/dashboard/lib/utils/deployment-formatters.ts index d26dd3dbeb..401e69cc98 100644 --- a/web/apps/dashboard/lib/utils/deployment-formatters.ts +++ b/web/apps/dashboard/lib/utils/deployment-formatters.ts @@ -1,35 +1,33 @@ -export function formatCpu(millicores: number): string { +export type FormattedParts = { value: string; unit: string }; + +export function formatCpuParts(millicores: number): FormattedParts { if (millicores === 0) { - return "—"; + return { value: "—", unit: "" }; } if (millicores === 256) { - return "1/4 vCPU"; + return { value: "1/4", unit: "vCPU" }; } if (millicores === 512) { - return "1/2 vCPU"; + return { value: "1/2", unit: "vCPU" }; } if (millicores === 768) { - return "3/4 vCPU"; + return { value: "3/4", unit: "vCPU" }; } if (millicores === 1024) { - return "1 vCPU"; + return { value: "1", unit: "vCPU" }; } - if (millicores >= 1024 && millicores % 1024 === 0) { - return `${millicores / 1024} vCPU`; + return { value: `${millicores / 1024}`, unit: "vCPU" }; } - - return `${millicores}m vCPU`; + return { value: `${millicores}m`, unit: "vCPU" }; } -export function formatMemory(mib: number): string { +export function formatMemoryParts(mib: number): FormattedParts { if (mib === 0) { - return "—"; + return { value: "—", unit: "" }; } - // Convert to GiB when >= 1024 MiB if (mib >= 1024) { - // Show decimals only if not a whole number - return `${(mib / 1024).toFixed(mib % 1024 === 0 ? 0 : 1)} GiB`; + return { value: `${(mib / 1024).toFixed(mib % 1024 === 0 ? 0 : 1)}`, unit: "GiB" }; } - return `${mib} MiB`; + return { value: `${mib}`, unit: "MiB" }; } diff --git a/web/apps/dashboard/tailwind.config.js b/web/apps/dashboard/tailwind.config.js new file mode 100644 index 0000000000..020760e3ff --- /dev/null +++ b/web/apps/dashboard/tailwind.config.js @@ -0,0 +1,172 @@ +/** @type {import('tailwindcss').Config} */ + +import defaultTheme from "@unkey/ui/tailwind.config"; +import "tailwindcss/plugin"; + +module.exports = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + "../../internal/ui/src/**/*.tsx", + "../../internal/icons/src/**/*.tsx", + ], + theme: merge(defaultTheme.theme, { + /** + * We need to remove almost all of these and move them into `@unkey/ui`. + * Especially colors and font sizes need to go + */ + fontSize: { + xxs: ["10px", "16px"], + xs: ["0.75rem", { lineHeight: "1rem" }], + sm: ["0.875rem", { lineHeight: "1.5rem" }], + base: ["1rem", { lineHeight: "1.75rem" }], + lg: ["1.125rem", { lineHeight: "1.75rem" }], + xl: ["1.25rem", { lineHeight: "2rem" }], + "2xl": ["1.5rem", { lineHeight: "2.25rem" }], + "3xl": ["1.75rem", { lineHeight: "2.25rem" }], + "4xl": ["2rem", { lineHeight: "2.5rem" }], + "5xl": ["2.5rem", { lineHeight: "3rem" }], + "6xl": ["3rem", { lineHeight: "3.5rem" }], + "7xl": ["4rem", { lineHeight: "4.5rem" }], + }, + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + + colors: { + background: { + DEFAULT: "hsl(var(--background))", + subtle: "hsl(var(--background-subtle))", + }, + content: { + DEFAULT: "hsl(var(--content))", + subtle: "hsl(var(--content-subtle))", + info: "hsl(var(--content-info))", + warn: "hsl(var(--content-warn))", + alert: "hsl(var(--content-alert))", + }, + + brand: { + DEFAULT: "hsl(var(--brand))", + foreground: "hsl(var(--brand-foreground))", + }, + + warn: { + DEFAULT: "hsl(var(--warn))", + foreground: "hsl(var(--warn-foreground))", + }, + + alert: { + DEFAULT: "hsl(var(--alert))", + foreground: "hsl(var(--alert-foreground))", + }, + success: { + DEFAULT: "hsl(var(--success))", + }, + + "amber-2": "var(--amber-2)", + "amber-3": "var(--amber-3)", + "amber-4": "var(--amber-4)", + "amber-6": "var(--amber-6)", + "amber-11": "var(--amber-11)", + + "red-2": "var(--red-2)", + "red-3": "var(--red-3)", + "red-4": "var(--red-4)", + "red-6": "var(--red-6)", + "red-11": "var(--red-11)", + + subtle: { + DEFAULT: "hsl(var(--subtle))", + foreground: "hsl(var(--subtle-foreground))", + }, + + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + + border: "hsl(var(--border))", + ring: "hsl(var(--ring))", + }, + extend: { + keyframes: { + "accordion-down": { + from: { height: 0 }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: 0 }, + }, + "shiny-text": { + "0%, 90%, 100%": { + "background-position": "calc(-100% - var(--shiny-width)) 0", + }, + "30%, 60%": { + "background-position": "calc(100% + var(--shiny-width)) 0", + }, + }, + shimmer: { + "0%": { transform: "translateX(-100%)" }, + "100%": { transform: "translateX(100%)" }, + }, + "expand-down": { + from: { "grid-template-rows": "0fr" }, + to: { "grid-template-rows": "1fr" }, + }, + "fade-slide-in": { + from: { opacity: 0, transform: "translateY(8px)" }, + to: { opacity: 1, transform: "translateY(0)" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + "expand-down": "expand-down 0.2s ease-out forwards", + "fade-slide-in": "fade-slide-in 0.3s ease-out", + "shiny-text": "shiny-text 10s infinite", + shimmer: "shimmer 1.2s ease-in-out infinite", + }, + fontFamily: { + sans: ["var(--font-geist-sans)"], + mono: ["var(--font-geist-mono)"], + }, + }, + }), + plugins: [ + require("tailwindcss-animate"), + require("@tailwindcss/typography"), + require("@tailwindcss/aspect-ratio"), + require("@tailwindcss/container-queries"), + ], +}; + +export function merge(obj1, obj2) { + for (const key in obj2) { + // biome-ignore lint/suspicious/noPrototypeBuiltins: don't tell me what to do + if (obj2.hasOwnProperty(key)) { + if (typeof obj2[key] === "object" && !Array.isArray(obj2[key])) { + if (!obj1[key]) { + obj1[key] = {}; + } + obj1[key] = merge(obj1[key], obj2[key]); + } else { + obj1[key] = obj2[key]; + } + } + } + return obj1; +} diff --git a/web/internal/icons/src/icons/hammer-2.tsx b/web/internal/icons/src/icons/hammer-2.tsx new file mode 100644 index 0000000000..10b1829b00 --- /dev/null +++ b/web/internal/icons/src/icons/hammer-2.tsx @@ -0,0 +1,46 @@ +/** + * Copyright © Nucleo + * Version 1.3, January 3, 2024 + * Nucleo Icons + * https://nucleoapp.com/ + * - Redistribution of icons is prohibited. + * - Icons are restricted for use only within the product they are bundled with. + * + * For more details: + * https://nucleoapp.com/license + */ + +import { type IconProps, sizeMap } from "../props"; + +export function Hammer2({ iconSize = "xl-thin", filled, ...props }: IconProps) { + const { iconSize: pixelSize, strokeWidth } = sizeMap[iconSize]; + + return ( + + + + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-elixir.tsx b/web/internal/icons/src/icons/lang-elixir.tsx new file mode 100644 index 0000000000..c8aa053d5d --- /dev/null +++ b/web/internal/icons/src/icons/lang-elixir.tsx @@ -0,0 +1,162 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangElixir({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-go.tsx b/web/internal/icons/src/icons/lang-go.tsx new file mode 100644 index 0000000000..7c57e3065f --- /dev/null +++ b/web/internal/icons/src/icons/lang-go.tsx @@ -0,0 +1,23 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangGo({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-java.tsx b/web/internal/icons/src/icons/lang-java.tsx new file mode 100644 index 0000000000..ce50f669ea --- /dev/null +++ b/web/internal/icons/src/icons/lang-java.tsx @@ -0,0 +1,36 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangJava({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-javascript.tsx b/web/internal/icons/src/icons/lang-javascript.tsx new file mode 100644 index 0000000000..e8823551de --- /dev/null +++ b/web/internal/icons/src/icons/lang-javascript.tsx @@ -0,0 +1,20 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangJavascript({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-php.tsx b/web/internal/icons/src/icons/lang-php.tsx new file mode 100644 index 0000000000..2d0ca7add0 --- /dev/null +++ b/web/internal/icons/src/icons/lang-php.tsx @@ -0,0 +1,54 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangPhp({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-python.tsx b/web/internal/icons/src/icons/lang-python.tsx new file mode 100644 index 0000000000..ffa69e3f7d --- /dev/null +++ b/web/internal/icons/src/icons/lang-python.tsx @@ -0,0 +1,66 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangPython({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-ruby.tsx b/web/internal/icons/src/icons/lang-ruby.tsx new file mode 100644 index 0000000000..72c158fcaf --- /dev/null +++ b/web/internal/icons/src/icons/lang-ruby.tsx @@ -0,0 +1,20 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangRuby({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-rust.tsx b/web/internal/icons/src/icons/lang-rust.tsx new file mode 100644 index 0000000000..b3b583c758 --- /dev/null +++ b/web/internal/icons/src/icons/lang-rust.tsx @@ -0,0 +1,17 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangRust({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + ); +} diff --git a/web/internal/icons/src/icons/lang-typescript.tsx b/web/internal/icons/src/icons/lang-typescript.tsx new file mode 100644 index 0000000000..b3a874f8d7 --- /dev/null +++ b/web/internal/icons/src/icons/lang-typescript.tsx @@ -0,0 +1,20 @@ +import { type IconProps, sizeMap } from "../props"; + +export function LangTypescript({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize } = sizeMap[iconSize]; + + return ( + + + + ); +} diff --git a/web/internal/icons/src/icons/layer-front.tsx b/web/internal/icons/src/icons/layer-front.tsx new file mode 100644 index 0000000000..57b93946d1 --- /dev/null +++ b/web/internal/icons/src/icons/layer-front.tsx @@ -0,0 +1,62 @@ +/** + * Copyright © Nucleo + * Version 1.3, January 3, 2024 + * Nucleo Icons + * https://nucleoapp.com/ + * - Redistribution of icons is prohibited. + * - Icons are restricted for use only within the product they are bundled with. + * + * For more details: + * https://nucleoapp.com/license + */ + +import { type IconProps, sizeMap } from "../props"; + +export function LayerFront({ iconSize = "xl-thin", ...props }: IconProps) { + const { iconSize: pixelSize, strokeWidth } = sizeMap[iconSize]; + + return ( + + + + + + + + + ); +} diff --git a/web/internal/icons/src/index.ts b/web/internal/icons/src/index.ts index a59eb6e741..280f340d31 100644 --- a/web/internal/icons/src/index.ts +++ b/web/internal/icons/src/index.ts @@ -85,6 +85,7 @@ export * from "./icons/github"; export * from "./icons/grid"; export * from "./icons/grid-circle"; export * from "./icons/half-dotted-circle-play"; +export * from "./icons/hammer-2"; export * from "./icons/hard-drive"; export * from "./icons/heart-pulse"; export * from "./icons/heart"; @@ -93,7 +94,17 @@ export * from "./icons/input-password-settings"; export * from "./icons/input-search"; export * from "./icons/key"; export * from "./icons/key-2"; +export * from "./icons/lang-elixir"; +export * from "./icons/lang-go"; +export * from "./icons/lang-java"; +export * from "./icons/lang-javascript"; +export * from "./icons/lang-php"; +export * from "./icons/lang-python"; +export * from "./icons/lang-ruby"; +export * from "./icons/lang-rust"; +export * from "./icons/lang-typescript"; export * from "./icons/laptop-2"; +export * from "./icons/layer-front"; export * from "./icons/layers-2"; export * from "./icons/layers-3"; export * from "./icons/layout-right"; diff --git a/web/internal/ui/src/components/settings-card.tsx b/web/internal/ui/src/components/settings-card.tsx index 4166b7e992..cd3fad1010 100644 --- a/web/internal/ui/src/components/settings-card.tsx +++ b/web/internal/ui/src/components/settings-card.tsx @@ -16,6 +16,7 @@ type SettingCardProps = { border?: SettingCardBorder; contentWidth?: string; icon?: React.ReactNode; + iconClassName?: string; expandable?: React.ReactNode; defaultExpanded?: boolean; chevronState?: ChevronState; @@ -26,7 +27,7 @@ const SettingCardGroupContext = React.createContext(false); function SettingCardGroup({ children }: { children: React.ReactNode }) { return ( -
+
{children}
@@ -42,6 +43,7 @@ function SettingCard({ border = "default", contentWidth = "w-[420px]", icon, + iconClassName, expandable, defaultExpanded = false, chevronState, @@ -81,14 +83,14 @@ function SettingCard({ return ""; } if (border === "top") { - return "rounded-t-xl"; + return "rounded-t-[14px]"; } if (border === "bottom") { - return !expandable || !isExpanded ? "rounded-b-xl" : ""; + return !expandable || !isExpanded ? "rounded-b-[14px]" : ""; } if (border === "both") { - const bottom = !expandable || !isExpanded ? "rounded-b-xl" : ""; - return cn("rounded-t-xl", bottom); + const bottom = !expandable || !isExpanded ? "rounded-b-[14px]" : ""; + return cn("rounded-t-[14px]", bottom); } return ""; }; @@ -103,7 +105,7 @@ function SettingCard({ const expandedBottomRadius = !inGroup && expandable && isExpanded && (border === "bottom" || border === "both") - ? "rounded-b-xl" + ? "rounded-b-[14px]" : ""; const handleToggle = () => { @@ -135,7 +137,7 @@ function SettingCard({
{icon && ( -
+
{icon}
)}