From bd4f8a2102fe13956182607d8726c86de9903ad7 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Mon, 15 Sep 2025 18:25:04 +0300 Subject: [PATCH 01/12] fix: project collection and db idx for environment --- .../create-project/create-project-dialog.tsx | 59 +++++++----- .../create-project/create-project.schema.ts | 15 --- .../create-project/use-create-project.ts | 52 ---------- apps/dashboard/lib/collections/projects.ts | 94 +++++++++++++------ .../lib/trpc/routers/project/create.ts | 9 +- go/pkg/db/schema.sql | 2 +- internal/db/src/schema/environments.ts | 2 +- 7 files changed, 106 insertions(+), 127 deletions(-) delete mode 100644 apps/dashboard/app/(app)/projects/_components/create-project/create-project.schema.ts delete mode 100644 apps/dashboard/app/(app)/projects/_components/create-project/use-create-project.ts diff --git a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx index 8608b92641..2a9b148ab5 100644 --- a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx +++ b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx @@ -1,17 +1,17 @@ "use client"; - import { NavbarActionButton } from "@/components/navigation/action-button"; import { Navbar } from "@/components/navigation/navbar"; +import { collection } from "@/lib/collections"; +import { + type CreateProjectRequestSchema, + createProjectRequestSchema, +} from "@/lib/collections/projects"; import { zodResolver } from "@hookform/resolvers/zod"; +import { DuplicateKeyError } from "@tanstack/react-db"; import { Plus } from "@unkey/icons"; -import { Button, DialogContainer, FormInput, toast } from "@unkey/ui"; +import { Button, DialogContainer, FormInput } from "@unkey/ui"; import { useState } from "react"; import { useForm } from "react-hook-form"; -import type { z } from "zod"; -import { createProjectSchema } from "./create-project.schema"; -import { useCreateProject } from "./use-create-project"; - -type FormValues = z.infer; export const CreateProjectDialog = () => { const [isModalOpen, setIsModalOpen] = useState(false); @@ -20,34 +20,42 @@ export const CreateProjectDialog = () => { register, handleSubmit, setValue, + setError, reset, - formState: { errors, isSubmitting }, - } = useForm({ - resolver: zodResolver(createProjectSchema), + formState: { errors, isSubmitting, isValid }, + } = useForm({ + resolver: zodResolver(createProjectRequestSchema), defaultValues: { name: "", slug: "", gitRepositoryUrl: "", }, + mode: "onChange", }); - const createProject = useCreateProject((data) => { - toast.success("Project has been created", { - description: `${data.name} is ready to use`, - }); - reset(); - setIsModalOpen(false); - }); - - const onSubmitForm = async (values: FormValues) => { + const onSubmitForm = async (values: CreateProjectRequestSchema) => { try { - await createProject.mutateAsync({ + collection.projects.insert({ name: values.name, slug: values.slug, - gitRepositoryUrl: values.gitRepositoryUrl ?? null, + gitRepositoryUrl: values.gitRepositoryUrl || null, + activeDeploymentId: null, + updatedAt: null, + id: "will-be-replace-by-server", }); + + reset(); + setIsModalOpen(false); } catch (error) { - console.error("Form submission error:", error); + if (error instanceof DuplicateKeyError) { + setError("slug", { + type: "custom", + message: "Project with this slug already exists", + }); + } else { + console.error("Form submission error:", error); + // The collection's onInsert will handle showing error toasts + } } }; @@ -59,7 +67,6 @@ export const CreateProjectDialog = () => { .replace(/\s+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); - setValue("slug", slug); }; @@ -91,8 +98,8 @@ export const CreateProjectDialog = () => { form="project-form" variant="primary" size="xlg" - disabled={isSubmitting || createProject.isLoading} - loading={isSubmitting || createProject.isLoading} + disabled={isSubmitting || !isValid} + loading={isSubmitting} className="w-full rounded-lg" > Create Project @@ -119,6 +126,7 @@ export const CreateProjectDialog = () => { })} placeholder="My Awesome Project" /> + { {...register("slug")} placeholder="my-awesome-project" /> + void) => { - const project = trpc.project.create.useMutation({ - onSuccess(data) { - onSuccess(data); - }, - onError(err) { - if (err.data?.code === "NOT_FOUND") { - toast.error("Project Creation Failed", { - description: "Unable to find the workspace. Please refresh and try again.", - }); - } else if (err.data?.code === "CONFLICT") { - toast.error("Project Already Exists", { - description: err.message || "A project with this slug already exists in your workspace.", - }); - } else if (err.data?.code === "INTERNAL_SERVER_ERROR") { - toast.error("Server Error", { - description: - "We encountered an issue while creating your project. Please try again later or contact support at support@unkey.dev", - }); - } else if (err.data?.code === "BAD_REQUEST") { - toast.error("Invalid Configuration", { - description: `Please check your project settings. ${err.message || ""}`, - }); - } else if (err.data?.code === "FORBIDDEN") { - toast.error("Permission Denied", { - description: - err.message || "You don't have permission to create projects in this workspace.", - }); - } else { - toast.error("Failed to Create Project", { - description: err.message || "An unexpected error occurred. Please try again later.", - action: { - label: "Contact Support", - onClick: () => window.open("https://support.unkey.dev", "_blank"), - }, - }); - } - }, - }); - return project; -}; diff --git a/apps/dashboard/lib/collections/projects.ts b/apps/dashboard/lib/collections/projects.ts index 418babe24a..0e1733b153 100644 --- a/apps/dashboard/lib/collections/projects.ts +++ b/apps/dashboard/lib/collections/projects.ts @@ -1,4 +1,3 @@ -"use client"; import { queryCollectionOptions } from "@tanstack/query-db-collection"; import { createCollection } from "@tanstack/react-db"; import { toast } from "@unkey/ui"; @@ -14,7 +13,22 @@ const schema = z.object({ activeDeploymentId: z.string().nullable(), }); +export const createProjectRequestSchema = z.object({ + name: z.string().trim().min(1, "Project name is required").max(256, "Project name too long"), + slug: z + .string() + .trim() + .min(1, "Project slug is required") + .max(256, "Project slug too long") + .regex( + /^[a-z0-9-]+$/, + "Project slug must contain only lowercase letters, numbers, and hyphens", + ), + gitRepositoryUrl: z.string().trim().url("Must be a valid URL").nullable().or(z.literal("")), +}); + export type Project = z.infer; +export type CreateProjectRequestSchema = z.infer; export const projects = createCollection( queryCollectionOptions({ @@ -26,39 +40,59 @@ export const projects = createCollection( }, getKey: (item) => item.id, onInsert: async ({ transaction }) => { - const { changes: newNamespace } = transaction.mutations[0]; + const { changes } = transaction.mutations[0]; - const p = trpcClient.project.create.mutate( - schema.parse({ - id: "created", // will be replaced by the actual ID after creation - name: newNamespace.name, - slug: newNamespace.slug, - gitRepositoryUrl: newNamespace.gitRepositoryUrl ?? null, - updatedAt: null, - }), - ); - toast.promise(p, { + const createInput = createProjectRequestSchema.parse({ + name: changes.name, + slug: changes.slug, + gitRepositoryUrl: changes.gitRepositoryUrl, + }); + const mutation = trpcClient.project.create.mutate(createInput); + + toast.promise(mutation, { loading: "Creating project...", - success: "Project created", - error: (res) => { - console.error("Failed to create project", res); - return { - message: "Failed to create project", - description: res.message, - }; + success: "Project created successfully", + error: (err) => { + console.error("Failed to create project", err); + + switch (err.data?.code) { + case "CONFLICT": + return { + message: "Project Already Exists", + description: + err.message || "A project with this slug already exists in your workspace.", + }; + case "FORBIDDEN": + return { + message: "Permission Denied", + description: + err.message || "You don't have permission to create projects in this workspace.", + }; + case "BAD_REQUEST": + return { + message: "Invalid Configuration", + description: `Please check your project settings. ${err.message || ""}`, + }; + case "INTERNAL_SERVER_ERROR": + return { + message: "Server Error", + description: + "We encountered an issue while creating your project. Please try again later or contact support at support@unkey.dev", + }; + case "NOT_FOUND": + return { + message: "Project Creation Failed", + description: "Unable to find the workspace. Please refresh and try again.", + }; + default: + return { + message: "Failed to Create Project", + description: err.message || "An unexpected error occurred. Please try again later.", + }; + } }, }); - await p; + await mutation; }, - // onDelete: async ({ transaction }) => { - // const { original } = transaction.mutations[0]; - // const p = trpcClient.deploy.project.delete.mutate({ projectId: original.id }); - // toast.promise(p, { - // loading: "Deleting project...", - // success: "Project deleted", - // error: "Failed to delete project", - // }); - // await p; - // }, }), ); diff --git a/apps/dashboard/lib/trpc/routers/project/create.ts b/apps/dashboard/lib/trpc/routers/project/create.ts index 5a9050a872..bede4ba739 100644 --- a/apps/dashboard/lib/trpc/routers/project/create.ts +++ b/apps/dashboard/lib/trpc/routers/project/create.ts @@ -1,5 +1,5 @@ -import { createProjectSchema } from "@/app/(app)/projects/_components/create-project/create-project.schema"; import { insertAuditLogs } from "@/lib/audit"; +import { createProjectRequestSchema } from "@/lib/collections/projects"; import { db, schema } from "@/lib/db"; import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc"; import { TRPCError } from "@trpc/server"; @@ -8,7 +8,7 @@ import { newId } from "@unkey/id"; export const createProject = t.procedure .use(requireUser) .use(requireWorkspace) - .input(createProjectSchema) + .input(createProjectRequestSchema) .use(withRatelimit(ratelimit.create)) .mutation(async ({ ctx, input }) => { const userId = ctx.user.id; @@ -105,8 +105,11 @@ export const createProject = t.procedure id: environmentId, workspaceId, projectId, - slug: slug, + createdAt: now, + updatedAt: now, + slug, }); + await insertAuditLogs(tx, { workspaceId, actor: { diff --git a/go/pkg/db/schema.sql b/go/pkg/db/schema.sql index ab014422a7..296b70cc35 100644 --- a/go/pkg/db/schema.sql +++ b/go/pkg/db/schema.sql @@ -304,7 +304,7 @@ CREATE TABLE `environments` ( `created_at` bigint NOT NULL, `updated_at` bigint, CONSTRAINT `environments_id` PRIMARY KEY(`id`), - CONSTRAINT `environments_workspace_id_slug_idx` UNIQUE(`workspace_id`,`slug`) + CONSTRAINT `environments_project_id_slug_idx` UNIQUE(`project_id`,`slug`) ); CREATE TABLE `projects` ( diff --git a/internal/db/src/schema/environments.ts b/internal/db/src/schema/environments.ts index 8a5766be05..e109613f1c 100644 --- a/internal/db/src/schema/environments.ts +++ b/internal/db/src/schema/environments.ts @@ -19,7 +19,7 @@ export const environments = mysqlTable( ...lifecycleDates, }, (table) => ({ - uniqueSlug: uniqueIndex("environments_workspace_id_slug_idx").on(table.workspaceId, table.slug), + uniqueSlug: uniqueIndex("environments_project_id_slug_idx").on(table.projectId, table.slug), }), ); From a91f3e68091fea348d20bd2360cec163c381e177 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Mon, 15 Sep 2025 18:41:15 +0300 Subject: [PATCH 02/12] fix: search --- .../app/(app)/projects/_components/list/index.tsx | 14 +++++++++++--- apps/dashboard/components/list-search-input.tsx | 7 +------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/dashboard/app/(app)/projects/_components/list/index.tsx b/apps/dashboard/app/(app)/projects/_components/list/index.tsx index 1ed50c638e..b40f73e814 100644 --- a/apps/dashboard/app/(app)/projects/_components/list/index.tsx +++ b/apps/dashboard/app/(app)/projects/_components/list/index.tsx @@ -1,7 +1,8 @@ import { collection } from "@/lib/collections"; -import { useLiveQuery } from "@tanstack/react-db"; +import { ilike, useLiveQuery } from "@tanstack/react-db"; import { BookBookmark, Dots } from "@unkey/icons"; import { Button, Empty } from "@unkey/ui"; +import { useProjectsFilters } from "../hooks/use-projects-filters"; import { ProjectActions } from "./project-actions"; import { ProjectCard } from "./projects-card"; import { ProjectCardSkeleton } from "./projects-card-skeleton"; @@ -9,8 +10,15 @@ import { ProjectCardSkeleton } from "./projects-card-skeleton"; const MAX_SKELETON_COUNT = 8; export const ProjectsList = () => { - const projects = useLiveQuery((q) => - q.from({ project: collection.projects }).orderBy(({ project }) => project.updatedAt, "desc"), + const { filters } = useProjectsFilters(); + const projectName = filters.find((f) => f.field === "query")?.value ?? ""; + const projects = useLiveQuery( + (q) => + q + .from({ project: collection.projects }) + .orderBy(({ project }) => project.updatedAt, "desc") + .where(({ project }) => ilike(project.name, `%${projectName}%`)), + [projectName], ); if (projects.isLoading) { diff --git a/apps/dashboard/components/list-search-input.tsx b/apps/dashboard/components/list-search-input.tsx index 87368e8528..5a34b3d5f9 100644 --- a/apps/dashboard/components/list-search-input.tsx +++ b/apps/dashboard/components/list-search-input.tsx @@ -26,13 +26,11 @@ type Props = { }; const MAX_QUERY_LENGTH = 120; -const DEFAULT_DEBOUNCE = 300; const DEFAULT_PLACEHOLDER = "Search..."; export const ListSearchInput = ({ useFiltersHook, placeholder = DEFAULT_PLACEHOLDER, - debounceTime = DEFAULT_DEBOUNCE, className, }: Props) => { const { filters, updateFilters } = useFiltersHook(); @@ -98,10 +96,7 @@ export const ListSearchInput = ({ clearTimeout(debounceRef.current); } - // Set new debounce - debounceRef.current = setTimeout(() => { - updateQuery(value); - }, debounceTime); + updateQuery(value); }; const handleClear = () => { From 5d249b9697131423f2dc804545fde784734c53d5 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Mon, 15 Sep 2025 19:00:26 +0300 Subject: [PATCH 03/12] fix: dir structrue --- .../deployment-list-search/index.tsx | 2 +- .../hooks/use-deployment-logs.tsx | 2 +- .../hooks/use-env-var-manager.tsx | 2 +- .../[projectId]/diff/[...compare]/page.tsx | 2 +- apps/dashboard/lib/collections/deployments.ts | 2 +- apps/dashboard/lib/collections/domains.ts | 2 +- .../dashboard/lib/collections/environments.ts | 3 +- apps/dashboard/lib/collections/projects.ts | 4 +- .../{ => deploy}/deployment/buildLogs.ts | 0 .../{ => deploy}/deployment/getById.ts | 2 +- .../{ => deploy}/deployment/getOpenApiDiff.ts | 2 +- .../routers/{ => deploy}/deployment/index.ts | 6 +- .../routers/{ => deploy}/deployment/list.ts | 2 +- .../deployment/llm-search/index.ts | 0 .../deployment/llm-search/utils.ts | 0 .../trpc/routers/{ => deploy}/domains/list.ts | 0 .../routers/{project => deploy}/envs/list.ts | 0 .../routers/{ => deploy}/project/create.ts | 0 .../trpc/routers/{ => deploy}/project/list.ts | 0 .../routers/deployment/listByEnvironment.ts | 57 -------------- .../trpc/routers/deployment/listByProject.ts | 75 ------------------- apps/dashboard/lib/trpc/routers/index.ts | 52 ++++++------- 22 files changed, 39 insertions(+), 176 deletions(-) rename apps/dashboard/lib/trpc/routers/{ => deploy}/deployment/buildLogs.ts (100%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/deployment/getById.ts (95%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/deployment/getOpenApiDiff.ts (98%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/deployment/index.ts (55%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/deployment/list.ts (93%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/deployment/llm-search/index.ts (100%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/deployment/llm-search/utils.ts (100%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/domains/list.ts (100%) rename apps/dashboard/lib/trpc/routers/{project => deploy}/envs/list.ts (100%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/project/create.ts (100%) rename apps/dashboard/lib/trpc/routers/{ => deploy}/project/list.ts (100%) delete mode 100644 apps/dashboard/lib/trpc/routers/deployment/listByEnvironment.ts delete mode 100644 apps/dashboard/lib/trpc/routers/deployment/listByProject.ts diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx index 0ad382118a..06d5b34780 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx @@ -6,7 +6,7 @@ import { useFilters } from "../../../../hooks/use-filters"; export const DeploymentListSearch = () => { const { filters, updateFilters } = useFilters(); - const queryLLMForStructuredOutput = trpc.deployment.search.useMutation({ + const queryLLMForStructuredOutput = trpc.deploy.deployment.search.useMutation({ onSuccess(data) { if (data?.filters.length === 0 || !data) { toast.error( diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx index 0edf0e2806..9a51131362 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx @@ -50,7 +50,7 @@ export function useDeploymentLogs({ const scrollRef = useRef(null); // Fetch logs via tRPC - const { data: logsData, isLoading } = trpc.deployment.buildLogs.useQuery({ + const { data: logsData, isLoading } = trpc.deploy.deployment.buildLogs.useQuery({ deploymentId, }); diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx index 4e84dc7d99..145bb83545 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx @@ -7,7 +7,7 @@ type UseEnvVarsManagerProps = { }; export function useEnvVarsManager({ projectId, environment }: UseEnvVarsManagerProps) { - const { data } = trpc.environmentVariables.list.useQuery({ projectId }); + const { data } = trpc.deploy.environment.list_dummy.useQuery({ projectId }); const envVars = data?.[environment] ?? []; diff --git a/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx b/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx index f36852e6b2..b3f60a4b75 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx @@ -47,7 +47,7 @@ export default function DiffPage({ params }: Props) { data: diffData, isLoading: diffLoading, error: diffError, - } = trpc.deployment.getOpenApiDiff.useQuery( + } = trpc.deploy.deployment.getOpenApiDiff.useQuery( { oldDeploymentId: selectedFromDeployment, newDeploymentId: selectedToDeployment, diff --git a/apps/dashboard/lib/collections/deployments.ts b/apps/dashboard/lib/collections/deployments.ts index 2ea806253f..95f646bb10 100644 --- a/apps/dashboard/lib/collections/deployments.ts +++ b/apps/dashboard/lib/collections/deployments.ts @@ -42,7 +42,7 @@ export const deployments = createCollection( queryClient, queryKey: ["deployments"], retry: 3, - queryFn: () => trpcClient.deployment.list.query(), + queryFn: () => trpcClient.deploy.deployment.list.query(), getKey: (item) => item.id, onInsert: async () => { diff --git a/apps/dashboard/lib/collections/domains.ts b/apps/dashboard/lib/collections/domains.ts index 7cc212c41b..3abae98813 100644 --- a/apps/dashboard/lib/collections/domains.ts +++ b/apps/dashboard/lib/collections/domains.ts @@ -18,7 +18,7 @@ export const domains = createCollection( queryClient, queryKey: ["domains"], retry: 3, - queryFn: () => trpcClient.domain.list.query(), + queryFn: () => trpcClient.deploy.domain.list.query(), getKey: (item) => item.id, onInsert: async () => { diff --git a/apps/dashboard/lib/collections/environments.ts b/apps/dashboard/lib/collections/environments.ts index cd13229d11..c5b310cc8c 100644 --- a/apps/dashboard/lib/collections/environments.ts +++ b/apps/dashboard/lib/collections/environments.ts @@ -17,8 +17,7 @@ export const environments = createCollection( queryClient, queryKey: ["environments"], retry: 3, - queryFn: () => trpcClient.environment.list.query(), - + queryFn: () => trpcClient.deploy.environment.list.query(), getKey: (item) => item.id, onInsert: async () => { throw new Error("Not implemented"); diff --git a/apps/dashboard/lib/collections/projects.ts b/apps/dashboard/lib/collections/projects.ts index 0e1733b153..16d848cc4f 100644 --- a/apps/dashboard/lib/collections/projects.ts +++ b/apps/dashboard/lib/collections/projects.ts @@ -36,7 +36,7 @@ export const projects = createCollection( queryKey: ["projects"], retry: 3, queryFn: async () => { - return await trpcClient.project.list.query(); + return await trpcClient.deploy.project.list.query(); }, getKey: (item) => item.id, onInsert: async ({ transaction }) => { @@ -47,7 +47,7 @@ export const projects = createCollection( slug: changes.slug, gitRepositoryUrl: changes.gitRepositoryUrl, }); - const mutation = trpcClient.project.create.mutate(createInput); + const mutation = trpcClient.deploy.project.create.mutate(createInput); toast.promise(mutation, { loading: "Creating project...", diff --git a/apps/dashboard/lib/trpc/routers/deployment/buildLogs.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/buildLogs.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/deployment/buildLogs.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/buildLogs.ts diff --git a/apps/dashboard/lib/trpc/routers/deployment/getById.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/getById.ts similarity index 95% rename from apps/dashboard/lib/trpc/routers/deployment/getById.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/getById.ts index 160a5ff36e..ee7b18bfd1 100644 --- a/apps/dashboard/lib/trpc/routers/deployment/getById.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/getById.ts @@ -1,7 +1,7 @@ import { db } from "@/lib/db"; +import { requireUser, requireWorkspace, t } from "@/lib/trpc/trpc"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { requireUser, requireWorkspace, t } from "../../trpc"; export const getById = t.procedure .use(requireUser) diff --git a/apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts similarity index 98% rename from apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts index 1ae22b8344..d8986fb26b 100644 --- a/apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts @@ -1,8 +1,8 @@ // trpc/routers/deployments/getOpenApiDiff.ts import { db } from "@/lib/db"; +import { requireUser, requireWorkspace, t } from "@/lib/trpc/trpc"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { requireUser, requireWorkspace, t } from "../../trpc"; export const getOpenApiDiff = t.procedure .use(requireUser) diff --git a/apps/dashboard/lib/trpc/routers/deployment/index.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts similarity index 55% rename from apps/dashboard/lib/trpc/routers/deployment/index.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts index 71cdaf65eb..d38d8251d0 100644 --- a/apps/dashboard/lib/trpc/routers/deployment/index.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts @@ -1,14 +1,10 @@ -import { t } from "../../trpc"; +import { t } from "@/lib/trpc/trpc"; import { getById } from "./getById"; import { getOpenApiDiff } from "./getOpenApiDiff"; import { listDeployments } from "./list"; -import { listByEnvironment } from "./listByEnvironment"; -import { listByProject } from "./listByProject"; export const deploymentRouter = t.router({ list: listDeployments, - listByEnvironment: listByEnvironment, - listByProject: listByProject, getById: getById, getOpenApiDiff: getOpenApiDiff, }); diff --git a/apps/dashboard/lib/trpc/routers/deployment/list.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts similarity index 93% rename from apps/dashboard/lib/trpc/routers/deployment/list.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts index 499d5f2f1b..59d13d72d4 100644 --- a/apps/dashboard/lib/trpc/routers/deployment/list.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts @@ -1,6 +1,6 @@ import { db } from "@/lib/db"; +import { requireUser, requireWorkspace, t } from "@/lib/trpc/trpc"; import { TRPCError } from "@trpc/server"; -import { requireUser, requireWorkspace, t } from "../../trpc"; export const listDeployments = t.procedure .use(requireUser) diff --git a/apps/dashboard/lib/trpc/routers/deployment/llm-search/index.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/llm-search/index.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/deployment/llm-search/index.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/llm-search/index.ts diff --git a/apps/dashboard/lib/trpc/routers/deployment/llm-search/utils.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/llm-search/utils.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/deployment/llm-search/utils.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/llm-search/utils.ts diff --git a/apps/dashboard/lib/trpc/routers/domains/list.ts b/apps/dashboard/lib/trpc/routers/deploy/domains/list.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/domains/list.ts rename to apps/dashboard/lib/trpc/routers/deploy/domains/list.ts diff --git a/apps/dashboard/lib/trpc/routers/project/envs/list.ts b/apps/dashboard/lib/trpc/routers/deploy/envs/list.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/project/envs/list.ts rename to apps/dashboard/lib/trpc/routers/deploy/envs/list.ts diff --git a/apps/dashboard/lib/trpc/routers/project/create.ts b/apps/dashboard/lib/trpc/routers/deploy/project/create.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/project/create.ts rename to apps/dashboard/lib/trpc/routers/deploy/project/create.ts diff --git a/apps/dashboard/lib/trpc/routers/project/list.ts b/apps/dashboard/lib/trpc/routers/deploy/project/list.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/project/list.ts rename to apps/dashboard/lib/trpc/routers/deploy/project/list.ts diff --git a/apps/dashboard/lib/trpc/routers/deployment/listByEnvironment.ts b/apps/dashboard/lib/trpc/routers/deployment/listByEnvironment.ts deleted file mode 100644 index 863d035e78..0000000000 --- a/apps/dashboard/lib/trpc/routers/deployment/listByEnvironment.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { db } from "@/lib/db"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { requireUser, requireWorkspace, t } from "../../trpc"; - -export const listByEnvironment = t.procedure - .use(requireUser) - .use(requireWorkspace) - .input( - z.object({ - projectId: z.string(), - environmentId: z.string(), - }), - ) - .query(async ({ input, ctx }) => { - try { - // First verify the project exists and belongs to this workspace - const project = await db.query.projects.findFirst({ - where: (table, { eq, and }) => - and(eq(table.id, input.projectId), eq(table.workspaceId, ctx.workspace.id)), - }); - - if (!project) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Project not found", - }); - } - - // Get all deployments for this project and environment - const deployments = await db.query.deployments.findMany({ - where: (table, { eq, and }) => - and(eq(table.projectId, input.projectId), eq(table.environmentId, input.environmentId)), - orderBy: (table, { desc }) => [desc(table.createdAt)], - }); - - return { - deployments: deployments.map((deployment) => ({ - id: deployment.id, - status: deployment.status, - gitCommitSha: deployment.gitCommitSha, - gitBranch: deployment.gitBranch, - createdAt: deployment.createdAt, - updatedAt: deployment.updatedAt, - })), - }; - } catch (error) { - if (error instanceof TRPCError) { - throw error; - } - - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to fetch deployments for environment", - }); - } - }); diff --git a/apps/dashboard/lib/trpc/routers/deployment/listByProject.ts b/apps/dashboard/lib/trpc/routers/deployment/listByProject.ts deleted file mode 100644 index 6cabf2a857..0000000000 --- a/apps/dashboard/lib/trpc/routers/deployment/listByProject.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { db } from "@/lib/db"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; -import { requireUser, requireWorkspace, t } from "../../trpc"; - -export const listByProject = t.procedure - .use(requireUser) - .use(requireWorkspace) - .input( - z.object({ - projectId: z.string(), - }), - ) - .query(async ({ input, ctx }) => { - try { - // First verify the project exists and belongs to this workspace - const project = await db.query.projects.findFirst({ - where: (table, { eq, and }) => - and(eq(table.id, input.projectId), eq(table.workspaceId, ctx.workspace.id)), - }); - - if (!project) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Project not found", - }); - } - - // Get all deployments for this project - const deployments = await db.query.deployments.findMany({ - where: (table, { eq }) => eq(table.projectId, input.projectId), - orderBy: (table, { desc }) => [desc(table.createdAt)], - with: { - environment: { columns: { slug: true } }, - project: { columns: { id: true, name: true, slug: true } }, - }, - }); - - return { - project: { - id: project.id, - name: project.name, - slug: project.slug, - gitRepositoryUrl: project.gitRepositoryUrl, - createdAt: project.createdAt, - updatedAt: project.updatedAt, - }, - deployments: deployments.map((deployment) => ({ - id: deployment.id, - status: deployment.status, - gitCommitSha: deployment.gitCommitSha, - gitBranch: deployment.gitBranch, - environment: deployment.environment.slug, - createdAt: deployment.createdAt, - updatedAt: deployment.updatedAt, - project: deployment.project - ? { - id: deployment.project.id, - name: deployment.project.name, - slug: deployment.project.slug, - } - : null, - })), - }; - } catch (error) { - if (error instanceof TRPCError) { - throw error; - } - - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to fetch deployments for project", - }); - } - }); diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts index 2f4ab8f170..8abf96983c 100644 --- a/apps/dashboard/lib/trpc/routers/index.ts +++ b/apps/dashboard/lib/trpc/routers/index.ts @@ -37,11 +37,14 @@ import { searchRolesPermissions } from "./authorization/roles/permissions/search import { queryRoles } from "./authorization/roles/query"; import { upsertRole } from "./authorization/roles/upsert"; import { queryUsage } from "./billing/query-usage"; -import { getDeploymentBuildLogs } from "./deployment/buildLogs"; -import { getOpenApiDiff } from "./deployment/getOpenApiDiff"; -import { listDeployments } from "./deployment/list"; -import { searchDeployments } from "./deployment/llm-search"; -import { listDomains } from "./domains/list"; +import { getDeploymentBuildLogs } from "./deploy/deployment/buildLogs"; +import { getOpenApiDiff } from "./deploy/deployment/getOpenApiDiff"; +import { listDeployments } from "./deploy/deployment/list"; +import { searchDeployments } from "./deploy/deployment/llm-search"; +import { listDomains } from "./deploy/domains/list"; +import { getEnvs } from "./deploy/envs/list"; +import { createProject } from "./deploy/project/create"; +import { listProjects } from "./deploy/project/list"; import { listEnvironments } from "./environment/list"; import { createIdentity } from "./identity/create"; import { queryIdentities } from "./identity/query"; @@ -80,9 +83,6 @@ import { updateMembership, } from "./org"; import { createPlainIssue } from "./plain"; -import { createProject } from "./project/create"; -import { getEnvs } from "./project/envs/list"; -import { listProjects } from "./project/list"; import { createNamespace } from "./ratelimit/createNamespace"; import { createOverride } from "./ratelimit/createOverride"; import { deleteNamespace } from "./ratelimit/deleteNamespace"; @@ -311,24 +311,24 @@ export const router = t.router({ query: queryIdentities, search: searchIdentities, }), - project: t.router({ - list: listProjects, - create: createProject, - }), - domain: t.router({ - list: listDomains, - }), - deployment: t.router({ - list: listDeployments, - search: searchDeployments, - getOpenApiDiff: getOpenApiDiff, - buildLogs: getDeploymentBuildLogs, - }), - environment: t.router({ - list: listEnvironments, - }), - environmentVariables: t.router({ - list: getEnvs, + deploy: t.router({ + project: t.router({ + list: listProjects, + create: createProject, + }), + environment: t.router({ + list_dummy: getEnvs, + list: listEnvironments, + }), + domain: t.router({ + list: listDomains, + }), + deployment: t.router({ + list: listDeployments, + search: searchDeployments, + getOpenApiDiff: getOpenApiDiff, + buildLogs: getDeploymentBuildLogs, + }), }), }); From 1c71597f756674e877f36791d87ebcfa523eaa86 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Mon, 15 Sep 2025 20:41:10 +0300 Subject: [PATCH 04/12] fix: coderabbit --- .../_components/create-project/create-project-dialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx index 2a9b148ab5..cf9a70e538 100644 --- a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx +++ b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx @@ -35,7 +35,7 @@ export const CreateProjectDialog = () => { const onSubmitForm = async (values: CreateProjectRequestSchema) => { try { - collection.projects.insert({ + const tx = collection.projects.insert({ name: values.name, slug: values.slug, gitRepositoryUrl: values.gitRepositoryUrl || null, @@ -43,6 +43,7 @@ export const CreateProjectDialog = () => { updatedAt: null, id: "will-be-replace-by-server", }); + await tx.isPersisted.promise; reset(); setIsModalOpen(false); From 115b3cf83700613f1ff065ae8a929854f671b7cc Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 15:24:10 +0300 Subject: [PATCH 05/12] fix: ui issues --- .../components/rollback-dialog.tsx | 2 +- .../active-deployment-card/filter-button.tsx | 2 +- .../active-deployment-card/git-avatar.tsx | 29 +++++++++++ .../details/active-deployment-card/index.tsx | 13 +++-- .../{active-deployment-card => }/card.tsx | 0 .../project-details-expandables/index.tsx | 1 - .../project-details-expandables/sections.tsx | 22 +++++--- .../(app)/projects/_components/list/index.tsx | 33 +++++++++--- .../_components/list/projects-card.tsx | 14 ++++-- apps/dashboard/lib/collections/deployments.ts | 50 ++++--------------- apps/dashboard/lib/collections/domains.ts | 29 ----------- .../trpc/routers/deploy/deployment/list.ts | 16 +++++- .../{ => deploy/deployment}/rollback.ts | 0 apps/dashboard/lib/trpc/routers/index.ts | 6 +-- 14 files changed, 117 insertions(+), 100 deletions(-) create mode 100644 apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/git-avatar.tsx rename apps/dashboard/app/(app)/projects/[projectId]/details/{active-deployment-card => }/card.tsx (100%) rename apps/dashboard/lib/trpc/routers/{ => deploy/deployment}/rollback.ts (100%) diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx index 52af6548c6..c1d31ad3d7 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx @@ -40,7 +40,7 @@ export const RollbackDialog = ({ hostname, }: RollbackDialogProps) => { const utils = trpc.useUtils(); - const rollback = trpc.deploy.rollback.useMutation({ + const rollback = trpc.deploy.deployment.rollback.useMutation({ onSuccess: () => { utils.invalidate(); toast.success("Rollback completed", { diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/filter-button.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/filter-button.tsx index 3810701fa5..0f48027318 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/filter-button.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/filter-button.tsx @@ -27,7 +27,7 @@ export const FilterButton = ({ > {label} -
+
{count}
diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/git-avatar.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/git-avatar.tsx new file mode 100644 index 0000000000..72c9d0ed49 --- /dev/null +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/git-avatar.tsx @@ -0,0 +1,29 @@ +import { User } from "@unkey/icons"; +import { useState } from "react"; + +type AvatarProps = { + src: string | null | undefined; + alt: string; + className?: string; +}; + +export function Avatar({ src, alt, className = "size-5" }: AvatarProps) { + const [hasError, setHasError] = useState(false); + + if (!src || hasError) { + return ( +
+ +
+ ); + } + + return ( + {alt} setHasError(true)} + /> + ); +} diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx index 8104ee0c38..48d538bccd 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx @@ -16,6 +16,7 @@ import { import { Badge, Button, Card, CopyButton, Input, TimestampInfo } from "@unkey/ui"; import { cn } from "@unkey/ui/src/lib/utils"; import { FilterButton } from "./filter-button"; +import { Avatar } from "./git-avatar"; import { useDeploymentLogs } from "./hooks/use-deployment-logs"; import { InfoChip } from "./info-chip"; import { ActiveDeploymentCardSkeleton } from "./skeleton"; @@ -103,7 +104,7 @@ export const ActiveDeploymentCard: React.FC = ({ deploymentId }) => {
{deployment.id}
-
TODO
+
{deployment.gitCommitMessage}
@@ -116,7 +117,7 @@ export const ActiveDeploymentCard: React.FC = ({ deploymentId }) => {
Created by - TODO + {deployment.gitCommitAuthorName} @@ -139,10 +140,14 @@ export const ActiveDeploymentCard: React.FC = ({ deploymentId }) => { />
- {deployment.gitBranch} + + {deployment.gitBranch} + - {deployment.gitCommitSha} + + {(deployment.gitCommitSha ?? "").slice(0, 7)} +
diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/card.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/card.tsx similarity index 100% rename from apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/card.tsx rename to apps/dashboard/app/(app)/projects/[projectId]/details/card.tsx diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx index 1e21004a46..e5304ee110 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx @@ -23,7 +23,6 @@ export const ProjectDetailsExpandable = ({ q .from({ project: collection.projects }) .where(({ project }) => eq(project.id, projectId)) - .join({ deployment: collection.deployments }, ({ deployment, project }) => eq(deployment.id, project.activeDeploymentId), ) diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/sections.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/sections.tsx index 6d999415e7..8a497e61d7 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/sections.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/sections.tsx @@ -16,6 +16,7 @@ import { } from "@unkey/icons"; import { Badge, TimestampInfo } from "@unkey/ui"; import type { ReactNode } from "react"; +import { Avatar } from "../active-deployment-card/git-avatar"; export type DetailItem = { icon: ReactNode; @@ -45,12 +46,18 @@ export const createDetailSections = (details: Deployment): DetailSection[] => [ { icon: , label: "Branch", - content: {details.gitBranch}, + content: ( + {details.gitBranch} + ), }, { icon: , label: "Commit", - content: {details.gitCommitSha}, + content: ( + + {(details.gitCommitSha ?? "").slice(0, 7)} + + ), }, { icon: , @@ -66,10 +73,9 @@ export const createDetailSections = (details: Deployment): DetailSection[] => [ label: "Author", content: (
- {details.gitCommitAuthorUsername {details.gitCommitAuthorUsername}
@@ -124,7 +130,8 @@ export const createDetailSections = (details: Deployment): DetailSection[] => [ label: "CPU", content: (
- {details.runtimeConfig.cpus}vCPUs + {details.runtimeConfig.cpus} + vCPUs
), }, @@ -133,7 +140,8 @@ export const createDetailSections = (details: Deployment): DetailSection[] => [ label: "Memory", content: (
- {details.runtimeConfig.memory}mb + {details.runtimeConfig.memory} + mb
), }, diff --git a/apps/dashboard/app/(app)/projects/_components/list/index.tsx b/apps/dashboard/app/(app)/projects/_components/list/index.tsx index b40f73e814..08973aeace 100644 --- a/apps/dashboard/app/(app)/projects/_components/list/index.tsx +++ b/apps/dashboard/app/(app)/projects/_components/list/index.tsx @@ -12,6 +12,7 @@ const MAX_SKELETON_COUNT = 8; export const ProjectsList = () => { const { filters } = useProjectsFilters(); const projectName = filters.find((f) => f.field === "query")?.value ?? ""; + const projects = useLiveQuery( (q) => q @@ -21,7 +22,12 @@ export const ProjectsList = () => { [projectName], ); - if (projects.isLoading) { + // Get all deployments and domains to lookup active deployment details + const deployments = useLiveQuery((q) => q.from({ deployment: collection.deployments }), []); + + const domains = useLiveQuery((q) => q.from({ domain: collection.domains }), []); + + if (projects.isLoading || deployments.isLoading || domains.isLoading) { return (
{
{projects.data.map((project) => { + // Find active deployment and associated domain for this project + const activeDeployment = project.activeDeploymentId + ? deployments.data.find((d) => d.id === project.activeDeploymentId) + : null; + + // Find domain for this project + const projectDomain = domains.data.find((d) => d.projectId === project.id); + + // Extract deployment regions for display + const regions = activeDeployment?.runtimeConfig.regions.map((r) => r.region) ?? []; + return ( 0 ? regions : ["No deployments"]} repository={project.gitRepositoryUrl || undefined} actions={ diff --git a/apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx b/apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx index c097b496db..9cb0edb8c0 100644 --- a/apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx +++ b/apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx @@ -1,5 +1,5 @@ import { CodeBranch, Cube, User } from "@unkey/icons"; -import { InfoTooltip, Loading } from "@unkey/ui"; +import { InfoTooltip, Loading, TimestampInfo } from "@unkey/ui"; import Link from "next/link"; import type { ReactNode } from "react"; import { useCallback, useState } from "react"; @@ -9,7 +9,7 @@ type ProjectCardProps = { name: string; domain: string; commitTitle: string; - commitDate: string; + commitTimestamp?: number; branch: string; author: string; regions: string[]; @@ -22,7 +22,7 @@ export const ProjectCard = ({ name, domain, commitTitle, - commitDate, + commitTimestamp, branch, author, regions, @@ -58,7 +58,7 @@ export const ProjectCard = ({ {/*Top Section > Project Name*/} {name} @@ -90,7 +90,11 @@ export const ProjectCard = ({
- {commitDate} on + {commitTimestamp ? ( + + ) : ( + No deployments + )} {branch} diff --git a/apps/dashboard/lib/collections/deployments.ts b/apps/dashboard/lib/collections/deployments.ts index 95f646bb10..424249a256 100644 --- a/apps/dashboard/lib/collections/deployments.ts +++ b/apps/dashboard/lib/collections/deployments.ts @@ -9,15 +9,17 @@ const schema = z.object({ projectId: z.string(), environmentId: z.string(), // Git information - gitCommitSha: z.string().nullable(), - gitBranch: z.string().nullable(), - gitCommitMessage: z.string().nullable(), - gitCommitAuthorName: z.string().nullable(), - gitCommitAuthorEmail: z.string().nullable(), - gitCommitAuthorUsername: z.string().nullable(), - gitCommitAuthorAvatarUrl: z.string().nullable(), - gitCommitTimestamp: z.number().int().nullable(), - + // TEMP: Git fields as non-nullable for UI development with mock data + // TODO: Convert to nullable (.nullable()) when real git integration is added + // In production, deployments may not have git metadata if triggered manually + gitCommitSha: z.string(), + gitBranch: z.string(), + gitCommitMessage: z.string(), + gitCommitAuthorName: z.string(), + gitCommitAuthorEmail: z.string(), + gitCommitAuthorUsername: z.string(), + gitCommitAuthorAvatarUrl: z.string(), + gitCommitTimestamp: z.number().int(), // Immutable configuration snapshot runtimeConfig: z.object({ regions: z.array( @@ -43,42 +45,12 @@ export const deployments = createCollection( queryKey: ["deployments"], retry: 3, queryFn: () => trpcClient.deploy.deployment.list.query(), - getKey: (item) => item.id, onInsert: async () => { throw new Error("Not implemented"); - // const { changes: newNamespace } = transaction.mutations[0]; - // - // const p = trpcClient.deploy.project.create.mutate(schema.parse({ - // id: "created", // will be replaced by the actual ID after creation - // name: newNamespace.name, - // slug: newNamespace.slug, - // gitRepositoryUrl: newNamespace.gitRepositoryUrl ?? null, - // updatedAt: null, - // })) - // toast.promise(p, { - // loading: "Creating project...", - // success: "Project created", - // error: (res) => { - // console.error("Failed to create project", res); - // return { - // message: "Failed to create project", - // description: res.message, - // }; - // }, - // }); - // await p; }, onDelete: async () => { throw new Error("Not implemented"); - // const { original } = transaction.mutations[0]; - // const p = trpcClient.deploy.project.delete.mutate({ projectId: original.id }); - // toast.promise(p, { - // loading: "Deleting project...", - // success: "Project deleted", - // error: "Failed to delete project", - // }); - // await p; }, }), ); diff --git a/apps/dashboard/lib/collections/domains.ts b/apps/dashboard/lib/collections/domains.ts index 3abae98813..056317550a 100644 --- a/apps/dashboard/lib/collections/domains.ts +++ b/apps/dashboard/lib/collections/domains.ts @@ -23,38 +23,9 @@ export const domains = createCollection( getKey: (item) => item.id, onInsert: async () => { throw new Error("Not implemented"); - // const { changes: newNamespace } = transaction.mutations[0]; - // - // const p = trpcClient.deploy.project.create.mutate(schema.parse({ - // id: "created", // will be replaced by the actual ID after creation - // name: newNamespace.name, - // slug: newNamespace.slug, - // gitRepositoryUrl: newNamespace.gitRepositoryUrl ?? null, - // updatedAt: null, - // })) - // toast.promise(p, { - // loading: "Creating project...", - // success: "Project created", - // error: (res) => { - // console.error("Failed to create project", res); - // return { - // message: "Failed to create project", - // description: res.message, - // }; - // }, - // }); - // await p; }, onDelete: async () => { throw new Error("Not implemented"); - // const { original } = transaction.mutations[0]; - // const p = trpcClient.deploy.project.delete.mutate({ projectId: original.id }); - // toast.promise(p, { - // loading: "Deleting project...", - // success: "Project deleted", - // error: "Failed to delete project", - // }); - // await p; }, }), ); diff --git a/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts index 59d13d72d4..8ea50eb2ba 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts @@ -8,7 +8,7 @@ export const listDeployments = t.procedure .query(async ({ ctx }) => { try { // Get all deployments for this workspace with project info - return await db.query.deployments.findMany({ + const deployments = await db.query.deployments.findMany({ where: (table, { eq }) => eq(table.workspaceId, ctx.workspace.id), columns: { id: true, @@ -28,6 +28,20 @@ export const listDeployments = t.procedure }, limit: 500, }); + + return deployments.map((deployment) => ({ + ...deployment, + // Replace NULL git fields with dummy data that clearly indicates it's fake + gitCommitSha: deployment.gitCommitSha ?? "abc123ef456789012345678901234567890abcdef", + gitBranch: deployment.gitBranch ?? "main", + gitCommitMessage: deployment.gitCommitMessage ?? "[DUMMY] Initial commit", + gitCommitAuthorName: deployment.gitCommitAuthorName ?? "[DUMMY] Unknown Author", + gitCommitAuthorEmail: deployment.gitCommitAuthorEmail ?? "dummy@example.com", + gitCommitAuthorUsername: deployment.gitCommitAuthorUsername ?? "dummy-user", + gitCommitAuthorAvatarUrl: + deployment.gitCommitAuthorAvatarUrl ?? "https://github.com/identicons/dummy-user.png", + gitCommitTimestamp: deployment.gitCommitTimestamp ?? Date.now() - 86400000, + })); } catch (_error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", diff --git a/apps/dashboard/lib/trpc/routers/rollback.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts similarity index 100% rename from apps/dashboard/lib/trpc/routers/rollback.ts rename to apps/dashboard/lib/trpc/routers/deploy/deployment/rollback.ts diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts index 2dee029737..2130b4254b 100644 --- a/apps/dashboard/lib/trpc/routers/index.ts +++ b/apps/dashboard/lib/trpc/routers/index.ts @@ -41,6 +41,7 @@ import { getDeploymentBuildLogs } from "./deploy/deployment/buildLogs"; import { getOpenApiDiff } from "./deploy/deployment/getOpenApiDiff"; import { listDeployments } from "./deploy/deployment/list"; import { searchDeployments } from "./deploy/deployment/llm-search"; +import { rollback } from "./deploy/deployment/rollback"; import { listDomains } from "./deploy/domains/list"; import { getEnvs } from "./deploy/envs/list"; import { createProject } from "./deploy/project/create"; @@ -107,7 +108,6 @@ import { disconnectPermissionFromRole } from "./rbac/disconnectPermissionFromRol import { disconnectRoleFromKey } from "./rbac/disconnectRoleFromKey"; import { updatePermission } from "./rbac/updatePermission"; import { updateRole } from "./rbac/updateRole"; -import { rollback } from "./rollback"; import { deleteRootKeys } from "./settings/root-keys/delete"; import { rootKeysLlmSearch } from "./settings/root-keys/llm-search"; import { queryRootKeys } from "./settings/root-keys/query"; @@ -329,11 +329,9 @@ export const router = t.router({ search: searchDeployments, getOpenApiDiff: getOpenApiDiff, buildLogs: getDeploymentBuildLogs, + rollback, }), }), - deploy: t.router({ - rollback: rollback, - }), }); // export type definition of API From 02bfa838bf8d45856030430f4e58ffdf2c01fe36 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 15:32:45 +0300 Subject: [PATCH 06/12] fix: broken avatars and gitsha --- .../components/table/deployments-list.tsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx index 535e97e6cf..13af56eb9e 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx @@ -11,6 +11,7 @@ import { cn } from "@unkey/ui/src/lib/utils"; import ms from "ms"; import dynamic from "next/dynamic"; import { useMemo, useState } from "react"; +import { Avatar } from "../../../details/active-deployment-card/git-avatar"; import type { DeploymentListFilterField } from "../../filters.schema"; import { useFilters } from "../../hooks/use-filters"; import { DeploymentStatusBadge } from "./components/deployment-status-badge"; @@ -185,7 +186,7 @@ export const DeploymentsList = ({ projectId }: Props) => { ); return (
-
+
{iconContainer}
@@ -291,7 +292,7 @@ export const DeploymentsList = ({ projectId }: Props) => { ); return (
-
+
{iconContainer}
@@ -305,7 +306,7 @@ export const DeploymentsList = ({ projectId }: Props) => {
- {deployment.gitCommitSha} + {deployment.gitCommitSha?.slice(0, 7)}
@@ -322,12 +323,12 @@ export const DeploymentsList = ({ projectId }: Props) => { render: ({ deployment }: { deployment: Deployment }) => { return (
-
- Author + +
@@ -369,10 +370,9 @@ export const DeploymentsList = ({ projectId }: Props) => { render: ({ deployment }: { deployment: Deployment }) => { return (
- Author {deployment.gitCommitAuthorName} From ff1b5f1097b39e4a91ad213f9cfdce5ce5385e91 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 15:39:29 +0300 Subject: [PATCH 07/12] fix: shrink table when details are active --- .../(app)/projects/[projectId]/deployments/page.tsx | 12 ++++++++++-- .../(app)/projects/[projectId]/layout-provider.tsx | 1 - 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx index 6e8cfb947e..257f8b7fcb 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx @@ -1,6 +1,7 @@ "use client"; - +import { cn } from "@unkey/ui/src/lib/utils"; import { useParams } from "next/navigation"; +import { useProjectLayout } from "../layout-provider"; import { DeploymentsListControlCloud } from "./components/control-cloud"; import { DeploymentsListControls } from "./components/controls"; import { DeploymentsList } from "./components/table/deployments-list"; @@ -8,8 +9,15 @@ import { DeploymentsList } from "./components/table/deployments-list"; export default function Deployments() { // biome-ignore lint/style/noNonNullAssertion: shut up nextjs const { projectId } = useParams<{ projectId: string }>()!; + const { isDetailsOpen } = useProjectLayout(); + return ( -
+
diff --git a/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx b/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx index 3a0ce064f7..3914c6f170 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx @@ -3,7 +3,6 @@ import { createContext, useContext } from "react"; type ProjectLayoutContextType = { isDetailsOpen: boolean; setIsDetailsOpen: (open: boolean) => void; - projectId: string; }; From 5391b7582955c162327c3e782c77f496c6bda354 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 15:46:03 +0300 Subject: [PATCH 08/12] refactor: organize and cleanup --- .../components/table/deployments-list.tsx | 127 +----------------- .../deployments/hooks/use-deployments.ts | 116 ++++++++++++++++ .../projects/[projectId]/deployments/page.tsx | 5 +- 3 files changed, 124 insertions(+), 124 deletions(-) create mode 100644 apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx index 13af56eb9e..78daec7272 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/table/deployments-list.tsx @@ -2,18 +2,15 @@ import { VirtualTable } from "@/components/virtual-table/index"; import type { Column } from "@/components/virtual-table/types"; import { useIsMobile } from "@/hooks/use-mobile"; -import { type Deployment, type Environment, collection } from "@/lib/collections"; +import type { Deployment, Environment } from "@/lib/collections"; import { shortenId } from "@/lib/shorten-id"; -import { eq, gt, gte, lte, or, useLiveQuery } from "@tanstack/react-db"; import { BookBookmark, Cloud, CodeBranch, Cube } from "@unkey/icons"; import { Button, Empty, TimestampInfo } from "@unkey/ui"; import { cn } from "@unkey/ui/src/lib/utils"; -import ms from "ms"; import dynamic from "next/dynamic"; import { useMemo, useState } from "react"; import { Avatar } from "../../../details/active-deployment-card/git-avatar"; -import type { DeploymentListFilterField } from "../../filters.schema"; -import { useFilters } from "../../hooks/use-filters"; +import { useDeployments } from "../../hooks/use-deployments"; import { DeploymentStatusBadge } from "./components/deployment-status-badge"; import { EnvStatusBadge } from "./components/env-status-badge"; import { @@ -42,125 +39,15 @@ const DeploymentListTableActions = dynamic( const COMPACT_BREAKPOINT = 1200; -type Props = { - projectId: string; -}; - -export const DeploymentsList = ({ projectId }: Props) => { - const { filters } = useFilters(); - - const project = useLiveQuery((q) => { - return q - .from({ project: collection.projects }) - .where(({ project }) => eq(project.id, projectId)) - .orderBy(({ project }) => project.id, "asc") - .limit(1); - }); - - const activeDeploymentId = project.data.at(0)?.activeDeploymentId; - - const activeDeployment = useLiveQuery( - (q) => - q - .from({ deployment: collection.deployments }) - .where(({ deployment }) => eq(deployment.id, activeDeploymentId)) - .orderBy(({ deployment }) => deployment.createdAt, "desc") - .limit(1), - [activeDeploymentId], - ); - - const deployments = useLiveQuery( - (q) => { - // Query filtered environments - // further down below we use this to rightJoin with deployments to filter deployments by environment - let environments = q.from({ environment: collection.environments }); - - for (const filter of filters) { - if (filter.field === "environment") { - environments = environments.where(({ environment }) => - eq(environment.slug, filter.value), - ); - } - } - - let query = q - .from({ deployment: collection.deployments }) - - .where(({ deployment }) => eq(deployment.projectId, projectId)); - - // add additional where clauses based on filters. - // All of these are a locical AND - - const groupedFilters = filters.reduce( - (acc, f) => { - if (!acc[f.field]) { - acc[f.field] = []; - } - acc[f.field].push(f.value); - return acc; - }, - {} as Record, - ); - for (const [field, values] of Object.entries(groupedFilters)) { - // this is kind of dumb, but `or`s type doesn't allow spreaded args without - // specifying the first two - const [v1, v2, ...rest] = values; - const f = field as DeploymentListFilterField; // I want some typesafety - switch (f) { - case "status": - query = query.where(({ deployment }) => - or( - eq(deployment.status, v1), - eq(deployment.status, v2), - ...rest.map((value) => eq(deployment.status, value)), - ), - ); - break; - case "branch": - query = query.where(({ deployment }) => - or( - eq(deployment.gitBranch, v1), - eq(deployment.gitBranch, v2), - ...rest.map((value) => eq(deployment.gitBranch, value)), - ), - ); - break; - case "environment": - // We already filtered - break; - case "since": - query = query.where(({ deployment }) => - gt(deployment.createdAt, Date.now() - ms(values.at(0) as string)), - ); - - break; - case "startTime": - query = query.where(({ deployment }) => gte(deployment.createdAt, values.at(0))); - break; - case "endTime": - query = query.where(({ deployment }) => lte(deployment.createdAt, values.at(0))); - break; - default: - break; - } - } - - return query - .rightJoin({ environment: environments }, ({ environment, deployment }) => - eq(environment.id, deployment.environmentId), - ) - .orderBy(({ deployment }) => deployment.createdAt, "desc") - .limit(100); - }, - [projectId, filters], - ); - +export const DeploymentsList = () => { const [selectedDeployment, setSelectedDeployment] = useState<{ deployment: Deployment; environment?: Environment; } | null>(null); const isCompactView = useIsMobile({ breakpoint: COMPACT_BREAKPOINT }); + const { activeDeployment, deployments } = useDeployments(); + const columns: Column<{ deployment: Deployment; environment?: Environment; @@ -198,7 +85,7 @@ export const DeploymentsList = ({ projectId }: Props) => { > {shortenId(deployment.id)}
- {deployment.id === activeDeploymentId ? ( + {deployment.id === activeDeployment.data.at(0)?.id ? ( ) : null}
@@ -403,7 +290,7 @@ export const DeploymentsList = ({ projectId }: Props) => { }, }, ]; - }, [selectedDeployment?.deployment.id, isCompactView, activeDeployment, activeDeploymentId]); + }, [selectedDeployment?.deployment.id, isCompactView, activeDeployment]); return ( { + const { projectId } = useProjectLayout(); + const { filters } = useFilters(); + + const project = useLiveQuery((q) => { + return q + .from({ project: collection.projects }) + .where(({ project }) => eq(project.id, projectId)) + .orderBy(({ project }) => project.id, "asc") + .limit(1); + }); + const activeDeploymentId = project.data.at(0)?.activeDeploymentId; + const activeDeployment = useLiveQuery( + (q) => + q + .from({ deployment: collection.deployments }) + .where(({ deployment }) => eq(deployment.id, activeDeploymentId)) + .orderBy(({ deployment }) => deployment.createdAt, "desc") + .limit(1), + [activeDeploymentId], + ); + const deployments = useLiveQuery( + (q) => { + // Query filtered environments + // further down below we use this to rightJoin with deployments to filter deployments by environment + let environments = q.from({ environment: collection.environments }); + + for (const filter of filters) { + if (filter.field === "environment") { + environments = environments.where(({ environment }) => + eq(environment.slug, filter.value), + ); + } + } + + let query = q + .from({ deployment: collection.deployments }) + + .where(({ deployment }) => eq(deployment.projectId, projectId)); + + // add additional where clauses based on filters. + // All of these are a locical AND + + const groupedFilters = filters.reduce( + (acc, f) => { + if (!acc[f.field]) { + acc[f.field] = []; + } + acc[f.field].push(f.value); + return acc; + }, + {} as Record, + ); + for (const [field, values] of Object.entries(groupedFilters)) { + // this is kind of dumb, but `or`s type doesn't allow spreaded args without + // specifying the first two + const [v1, v2, ...rest] = values; + const f = field as DeploymentListFilterField; // I want some typesafety + switch (f) { + case "status": + query = query.where(({ deployment }) => + or( + eq(deployment.status, v1), + eq(deployment.status, v2), + ...rest.map((value) => eq(deployment.status, value)), + ), + ); + break; + case "branch": + query = query.where(({ deployment }) => + or( + eq(deployment.gitBranch, v1), + eq(deployment.gitBranch, v2), + ...rest.map((value) => eq(deployment.gitBranch, value)), + ), + ); + break; + case "environment": + // We already filtered + break; + case "since": + query = query.where(({ deployment }) => + gt(deployment.createdAt, Date.now() - ms(values.at(0) as string)), + ); + + break; + case "startTime": + query = query.where(({ deployment }) => gte(deployment.createdAt, values.at(0))); + break; + case "endTime": + query = query.where(({ deployment }) => lte(deployment.createdAt, values.at(0))); + break; + default: + break; + } + } + + return query + .rightJoin({ environment: environments }, ({ environment, deployment }) => + eq(environment.id, deployment.environmentId), + ) + .orderBy(({ deployment }) => deployment.createdAt, "desc") + .limit(100); + }, + [projectId, filters], + ); + + return { deployments, activeDeployment, activeDeploymentId }; +}; diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx index 257f8b7fcb..d7b30eeb33 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/page.tsx @@ -1,14 +1,11 @@ "use client"; import { cn } from "@unkey/ui/src/lib/utils"; -import { useParams } from "next/navigation"; import { useProjectLayout } from "../layout-provider"; import { DeploymentsListControlCloud } from "./components/control-cloud"; import { DeploymentsListControls } from "./components/controls"; import { DeploymentsList } from "./components/table/deployments-list"; export default function Deployments() { - // biome-ignore lint/style/noNonNullAssertion: shut up nextjs - const { projectId } = useParams<{ projectId: string }>()!; const { isDetailsOpen } = useProjectLayout(); return ( @@ -20,7 +17,7 @@ export default function Deployments() { > - +
); } From 2f8691deef1a86c650854e5a2936c69e026c6651 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 16:16:53 +0300 Subject: [PATCH 09/12] refactor: promote new deploy to production for testing --- go/apps/ctrl/services/deployment/deploy_workflow.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/go/apps/ctrl/services/deployment/deploy_workflow.go b/go/apps/ctrl/services/deployment/deploy_workflow.go index 864deb36f8..1b22a38c8e 100644 --- a/go/apps/ctrl/services/deployment/deploy_workflow.go +++ b/go/apps/ctrl/services/deployment/deploy_workflow.go @@ -281,7 +281,6 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro // Create database entries for all domains err = hydra.StepVoid(ctx, "create-domain-entries", func(stepCtx context.Context) error { - // Prepare bulk insert parameters domainParams := make([]db.InsertDomainParams, 0, len(allDomains)) currentTime := time.Now().UnixMilli() @@ -382,6 +381,16 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro } w.logger.Info("deployment status updated to ready", "deployment_id", req.DeploymentID) + // TODO: This section will be removed in the future in favor of "Promote to Production" + err = db.Query.UpdateProjectActiveDeploymentId(stepCtx, w.db.RW(), db.UpdateProjectActiveDeploymentIdParams{ + ID: req.ProjectID, + ActiveDeploymentID: sql.NullString{Valid: true, String: req.DeploymentID}, + UpdatedAt: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()}, + }) + if err != nil { + return nil, fmt.Errorf("failed to update project %s active deployment ID to %s: %w", req.ProjectID, req.DeploymentID, err) + } + return &DeploymentResult{ DeploymentID: req.DeploymentID, Status: "ready", From 4901dab7a81fc509a1c6a6311fd2e4217763a179 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 17:42:27 +0300 Subject: [PATCH 10/12] feat: add scoping to tanstack collections --- .../deployment-list-datetime/index.tsx | 16 +--- .../components/environment-filter.tsx | 5 +- .../deployments/hooks/use-deployments.ts | 8 +- .../details/active-deployment-card/index.tsx | 5 +- .../project-details-expandables/index.tsx | 4 +- .../[projectId]/diff/[...compare]/page.tsx | 7 +- .../(app)/projects/[projectId]/diff/page.tsx | 7 +- .../projects/[projectId]/layout-provider.tsx | 2 + .../app/(app)/projects/[projectId]/layout.tsx | 6 +- .../app/(app)/projects/[projectId]/page.tsx | 7 +- .../create-project/create-project-dialog.tsx | 2 +- .../(app)/projects/_components/list/index.tsx | 31 +++++-- .../collections/{ => deploy}/deployments.ts | 34 +++---- .../lib/collections/deploy/domains.ts | 31 +++++++ .../lib/collections/deploy/environments.ts | 30 +++++++ .../lib/collections/{ => deploy}/projects.ts | 2 +- apps/dashboard/lib/collections/domains.ts | 31 ------- .../dashboard/lib/collections/environments.ts | 76 ---------------- apps/dashboard/lib/collections/index.ts | 90 +++++++++++++++---- .../namespaces.ts} | 10 ++- .../overrides.ts} | 6 +- .../trpc/routers/deploy/deployment/list.ts | 9 +- .../lib/trpc/routers/deploy/domains/list.ts | 7 +- .../lib/trpc/routers/deploy/project/create.ts | 2 +- .../lib/trpc/routers/environment/list.ts | 22 ++++- 25 files changed, 249 insertions(+), 201 deletions(-) rename apps/dashboard/lib/collections/{ => deploy}/deployments.ts (70%) create mode 100644 apps/dashboard/lib/collections/deploy/domains.ts create mode 100644 apps/dashboard/lib/collections/deploy/environments.ts rename apps/dashboard/lib/collections/{ => deploy}/projects.ts (98%) delete mode 100644 apps/dashboard/lib/collections/domains.ts delete mode 100644 apps/dashboard/lib/collections/environments.ts rename apps/dashboard/lib/collections/{ratelimit_namespaces.ts => ratelimit/namespaces.ts} (93%) rename apps/dashboard/lib/collections/{ratelimit_overrides.ts => ratelimit/overrides.ts} (96%) diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-datetime/index.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-datetime/index.tsx index 7ebaec0d38..4eb0dcfdb9 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-datetime/index.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-datetime/index.tsx @@ -5,23 +5,15 @@ import { Button } from "@unkey/ui"; import { useEffect, useState } from "react"; import { useFilters } from "../../../../hooks/use-filters"; -const TITLE_EMPTY_DEFAULT = "Select Time Range"; - export const DeploymentListDatetime = () => { - const [title, setTitle] = useState(TITLE_EMPTY_DEFAULT); + const [title, setTitle] = useState(null); const { filters, updateFilters } = useFilters(); - // If none of the filters are set anymore we should reset the title - // This can happen when the user manually clears a filter in the url - // or in the filter cloud useEffect(() => { - for (const filter of filters) { - if (["startTime", "endTime", "since"].includes(filter.field)) { - return; - } + if (!title) { + setTitle("Last 12 hours"); } - setTitle(TITLE_EMPTY_DEFAULT); - }, [filters]); + }, [title]); const timeValues = filters .filter((f) => ["startTime", "endTime", "since"].includes(f.field)) diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-filters/components/environment-filter.tsx b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-filters/components/environment-filter.tsx index 145284e0b9..19a48a393d 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-filters/components/environment-filter.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-filters/components/environment-filter.tsx @@ -1,12 +1,13 @@ +import { useProjectLayout } from "@/app/(app)/projects/[projectId]/layout-provider"; import { FilterCheckbox } from "@/components/logs/checkbox/filter-checkbox"; -import { collection } from "@/lib/collections"; import { useLiveQuery } from "@tanstack/react-db"; import { useFilters } from "../../../../../hooks/use-filters"; export const EnvironmentFilter = () => { const { filters, updateFilters } = useFilters(); + const { collections } = useProjectLayout(); - const environments = useLiveQuery((q) => q.from({ environment: collection.environments })); + const environments = useLiveQuery((q) => q.from({ environment: collections.environments })); return ( { - const { projectId } = useProjectLayout(); + const { projectId, collections } = useProjectLayout(); const { filters } = useFilters(); const project = useLiveQuery((q) => { @@ -20,7 +20,7 @@ export const useDeployments = () => { const activeDeployment = useLiveQuery( (q) => q - .from({ deployment: collection.deployments }) + .from({ deployment: collections.deployments }) .where(({ deployment }) => eq(deployment.id, activeDeploymentId)) .orderBy(({ deployment }) => deployment.createdAt, "desc") .limit(1), @@ -30,7 +30,7 @@ export const useDeployments = () => { (q) => { // Query filtered environments // further down below we use this to rightJoin with deployments to filter deployments by environment - let environments = q.from({ environment: collection.environments }); + let environments = q.from({ environment: collections.environments }); for (const filter of filters) { if (filter.field === "environment") { @@ -41,7 +41,7 @@ export const useDeployments = () => { } let query = q - .from({ deployment: collection.deployments }) + .from({ deployment: collections.deployments }) .where(({ deployment }) => eq(deployment.projectId, projectId)); diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx index 48d538bccd..ff86f6640d 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx @@ -1,6 +1,5 @@ "use client"; -import { collection } from "@/lib/collections"; import { eq, useLiveQuery } from "@tanstack/react-db"; import { ChevronDown, @@ -15,6 +14,7 @@ import { } from "@unkey/icons"; import { Badge, Button, Card, CopyButton, Input, TimestampInfo } from "@unkey/ui"; import { cn } from "@unkey/ui/src/lib/utils"; +import { useProjectLayout } from "../../layout-provider"; import { FilterButton } from "./filter-button"; import { Avatar } from "./git-avatar"; import { useDeploymentLogs } from "./hooks/use-deployment-logs"; @@ -69,9 +69,10 @@ type Props = { }; export const ActiveDeploymentCard: React.FC = ({ deploymentId }) => { + const { collections } = useProjectLayout(); const { data } = useLiveQuery((q) => q - .from({ deployment: collection.deployments }) + .from({ deployment: collections.deployments }) .where(({ deployment }) => eq(deployment.id, deploymentId)), ); diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx index e5304ee110..df58b30f67 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx @@ -3,6 +3,7 @@ import { eq, useLiveQuery } from "@tanstack/react-db"; import { Book2, Cube, DoubleChevronRight } from "@unkey/icons"; import { Button, InfoTooltip } from "@unkey/ui"; import { cn } from "@unkey/ui/src/lib/utils"; +import { useProjectLayout } from "../../layout-provider"; import { DetailSection } from "./detail-section"; import { createDetailSections } from "./sections"; @@ -19,11 +20,12 @@ export const ProjectDetailsExpandable = ({ onClose, projectId, }: ProjectDetailsExpandableProps) => { + const { collections } = useProjectLayout(); const query = useLiveQuery((q) => q .from({ project: collection.projects }) .where(({ project }) => eq(project.id, projectId)) - .join({ deployment: collection.deployments }, ({ deployment, project }) => + .join({ deployment: collections.deployments }, ({ deployment, project }) => eq(deployment.id, project.activeDeploymentId), ) .orderBy(({ project }) => project.id, "asc") diff --git a/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx b/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx index b3f60a4b75..26ff6cdbc4 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx @@ -1,12 +1,12 @@ "use client"; -import { collection } from "@/lib/collections"; import { trpc } from "@/lib/trpc/client"; import { eq, useLiveQuery } from "@tanstack/react-db"; import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@unkey/ui"; import { AlertCircle, ArrowLeft, GitCompare, Loader } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; +import { useProjectLayout } from "../../layout-provider"; import { DiffViewer } from "./components/client"; interface Props { @@ -20,6 +20,7 @@ interface Props { } export default function DiffPage({ params }: Props) { + const { collections } = useProjectLayout(); const router = useRouter(); const [fromDeploymentId, toDeploymentId] = params.compare; const [selectedFromDeployment, setSelectedFromDeployment] = useState( @@ -33,9 +34,9 @@ export default function DiffPage({ params }: Props) { const deployments = useLiveQuery((q) => q - .from({ deployment: collection.deployments }) + .from({ deployment: collections.deployments }) .where(({ deployment }) => eq(deployment.projectId, params.projectId)) - .join({ environment: collection.environments }, ({ environment, deployment }) => + .join({ environment: collections.environments }, ({ environment, deployment }) => eq(environment.id, deployment.environmentId), ) .orderBy(({ deployment }) => deployment.createdAt, "desc") diff --git a/apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx b/apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx index 1020b9ac66..08a384ddd0 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx @@ -1,13 +1,14 @@ "use client"; -import { collection } from "@/lib/collections"; import { eq, useLiveQuery } from "@tanstack/react-db"; import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@unkey/ui"; import { ArrowLeft, GitBranch, GitCommit, GitCompare, Globe, Tag } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; import { useState } from "react"; +import { useProjectLayout } from "../layout-provider"; export default function DiffSelectionPage(): JSX.Element { + const { collections } = useProjectLayout(); const params = useParams(); const router = useRouter(); const projectId = params?.projectId as string; @@ -18,9 +19,9 @@ export default function DiffSelectionPage(): JSX.Element { // Fetch all deployments for this project const deployments = useLiveQuery((q) => q - .from({ deployment: collection.deployments }) + .from({ deployment: collections.deployments }) .where(({ deployment }) => eq(deployment.projectId, params?.projectId)) - .join({ environment: collection.environments }, ({ environment, deployment }) => + .join({ environment: collections.environments }, ({ environment, deployment }) => eq(environment.id, deployment.environmentId), ) .orderBy(({ deployment }) => deployment.createdAt, "desc") diff --git a/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx b/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx index 3914c6f170..238cdf0d91 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx @@ -1,9 +1,11 @@ +import type { collectionManager } from "@/lib/collections"; import { createContext, useContext } from "react"; type ProjectLayoutContextType = { isDetailsOpen: boolean; setIsDetailsOpen: (open: boolean) => void; projectId: string; + collections: ReturnType; }; export const ProjectLayoutContext = createContext(null); diff --git a/apps/dashboard/app/(app)/projects/[projectId]/layout.tsx b/apps/dashboard/app/(app)/projects/[projectId]/layout.tsx index 6a1756bf94..f36b35cc83 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/layout.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/layout.tsx @@ -1,4 +1,5 @@ "use client"; +import { collectionManager } from "@/lib/collections"; import { DoubleChevronLeft } from "@unkey/icons"; import { Button, InfoTooltip } from "@unkey/ui"; import { useState } from "react"; @@ -24,7 +25,9 @@ type ProjectLayoutProps = { const ProjectLayout = ({ projectId, children }: ProjectLayoutProps) => { const [tableDistanceToTop, setTableDistanceToTop] = useState(0); - const [isDetailsOpen, setIsDetailsOpen] = useState(false); + const [isDetailsOpen, setIsDetailsOpen] = useState(true); + + const collections = collectionManager.getProjectCollections(projectId); return ( { isDetailsOpen, setIsDetailsOpen, projectId, + collections, }} >
diff --git a/apps/dashboard/app/(app)/projects/[projectId]/page.tsx b/apps/dashboard/app/(app)/projects/[projectId]/page.tsx index 062e694866..c4b3f7d12b 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/page.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/page.tsx @@ -11,12 +11,9 @@ import { EnvironmentVariablesSection } from "./details/env-variables-section"; import { useProjectLayout } from "./layout-provider"; export default function ProjectDetails() { - const { isDetailsOpen, projectId } = useProjectLayout(); - - const domains = useLiveQuery((q) => - q.from({ domain: collection.domains }).where(({ domain }) => eq(domain.projectId, projectId)), - ); + const { isDetailsOpen, projectId, collections } = useProjectLayout(); + const domains = useLiveQuery((q) => q.from({ domain: collections.domains })); const projects = useLiveQuery((q) => q.from({ project: collection.projects }).where(({ project }) => eq(project.id, projectId)), ); diff --git a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx index cf9a70e538..3227c1657c 100644 --- a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx +++ b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx @@ -5,7 +5,7 @@ import { collection } from "@/lib/collections"; import { type CreateProjectRequestSchema, createProjectRequestSchema, -} from "@/lib/collections/projects"; +} from "@/lib/collections/deploy/projects"; import { zodResolver } from "@hookform/resolvers/zod"; import { DuplicateKeyError } from "@tanstack/react-db"; import { Plus } from "@unkey/icons"; diff --git a/apps/dashboard/app/(app)/projects/_components/list/index.tsx b/apps/dashboard/app/(app)/projects/_components/list/index.tsx index 08973aeace..7cec2d9e6d 100644 --- a/apps/dashboard/app/(app)/projects/_components/list/index.tsx +++ b/apps/dashboard/app/(app)/projects/_components/list/index.tsx @@ -1,4 +1,4 @@ -import { collection } from "@/lib/collections"; +import { collection, collectionManager } from "@/lib/collections"; import { ilike, useLiveQuery } from "@tanstack/react-db"; import { BookBookmark, Dots } from "@unkey/icons"; import { Button, Empty } from "@unkey/ui"; @@ -22,12 +22,27 @@ export const ProjectsList = () => { [projectName], ); - // Get all deployments and domains to lookup active deployment details - const deployments = useLiveQuery((q) => q.from({ deployment: collection.deployments }), []); + // Get deployments and domains for each project + const deploymentQueries = projects.data.map((project) => { + const collections = collectionManager.getProjectCollections(project.id); + return useLiveQuery((q) => q.from({ deployment: collections.deployments }), [project.id]); + }); - const domains = useLiveQuery((q) => q.from({ domain: collection.domains }), []); + const domainQueries = projects.data.map((project) => { + const collections = collectionManager.getProjectCollections(project.id); + return useLiveQuery((q) => q.from({ domain: collections.domains }), [project.id]); + }); - if (projects.isLoading || deployments.isLoading || domains.isLoading) { + // Flatten the results + const allDeployments = deploymentQueries.flatMap((query) => query.data || []); + const allDomains = domainQueries.flatMap((query) => query.data || []); + + const isLoading = + projects.isLoading || + deploymentQueries.some((q) => q.isLoading) || + domainQueries.some((q) => q.isLoading); + + if (isLoading) { return (
{ {projects.data.map((project) => { // Find active deployment and associated domain for this project const activeDeployment = project.activeDeploymentId - ? deployments.data.find((d) => d.id === project.activeDeploymentId) + ? allDeployments.find((d) => d.id === project.activeDeploymentId) : null; // Find domain for this project - const projectDomain = domains.data.find((d) => d.projectId === project.id); + const projectDomain = allDomains.find((d) => d.projectId === project.id); // Extract deployment regions for display - const regions = activeDeployment?.runtimeConfig.regions.map((r) => r.region) ?? []; + const regions = activeDeployment?.runtimeConfig?.regions?.map((r) => r.region) ?? []; return ( ; -export const deployments = createCollection( - queryCollectionOptions({ - queryClient, - queryKey: ["deployments"], - retry: 3, - queryFn: () => trpcClient.deploy.deployment.list.query(), - getKey: (item) => item.id, - onInsert: async () => { - throw new Error("Not implemented"); - }, - onDelete: async () => { - throw new Error("Not implemented"); - }, - }), -); +export function createDeploymentsCollection(projectId: string) { + if (!projectId) { + throw new Error("projectId is required to create deployments collection"); + } + + return createCollection( + queryCollectionOptions({ + queryClient, + queryKey: [projectId, "deployments"], + retry: 3, + queryFn: () => trpcClient.deploy.deployment.list.query({ projectId }), + getKey: (item) => item.id, + id: `${projectId}-deployments`, + }), + ); +} diff --git a/apps/dashboard/lib/collections/deploy/domains.ts b/apps/dashboard/lib/collections/deploy/domains.ts new file mode 100644 index 0000000000..a2e1f1e897 --- /dev/null +++ b/apps/dashboard/lib/collections/deploy/domains.ts @@ -0,0 +1,31 @@ +"use client"; +import { queryCollectionOptions } from "@tanstack/query-db-collection"; +import { createCollection } from "@tanstack/react-db"; +import { z } from "zod"; +import { queryClient, trpcClient } from "../client"; + +const schema = z.object({ + id: z.string(), + domain: z.string(), + type: z.enum(["custom", "wildcard"]), + projectId: z.string().nullable(), +}); + +export type Domain = z.infer; + +export function createDomainsCollection(projectId: string) { + if (!projectId) { + throw new Error("projectId is required to create domains collection"); + } + + return createCollection( + queryCollectionOptions({ + queryClient, + queryKey: [projectId, "domains"], + retry: 3, + queryFn: () => trpcClient.deploy.domain.list.query({ projectId }), + getKey: (item) => item.id, + id: `${projectId}-domains`, + }), + ); +} diff --git a/apps/dashboard/lib/collections/deploy/environments.ts b/apps/dashboard/lib/collections/deploy/environments.ts new file mode 100644 index 0000000000..ed6d467d51 --- /dev/null +++ b/apps/dashboard/lib/collections/deploy/environments.ts @@ -0,0 +1,30 @@ +"use client"; +import { queryCollectionOptions } from "@tanstack/query-db-collection"; +import { createCollection } from "@tanstack/react-db"; +import { z } from "zod"; +import { queryClient, trpcClient } from "../client"; + +const schema = z.object({ + id: z.string(), + projectId: z.string(), + slug: z.string(), +}); + +export type Environment = z.infer; + +export function createEnvironmentsCollection(projectId: string) { + if (!projectId) { + throw new Error("projectId is required to create environments collection"); + } + + return createCollection( + queryCollectionOptions({ + queryClient, + queryKey: [projectId, "environments"], + retry: 3, + queryFn: () => trpcClient.deploy.environment.list.query({ projectId }), + getKey: (item) => item.id, + id: `${projectId}-environments`, + }), + ); +} diff --git a/apps/dashboard/lib/collections/projects.ts b/apps/dashboard/lib/collections/deploy/projects.ts similarity index 98% rename from apps/dashboard/lib/collections/projects.ts rename to apps/dashboard/lib/collections/deploy/projects.ts index 16d848cc4f..16224cbe36 100644 --- a/apps/dashboard/lib/collections/projects.ts +++ b/apps/dashboard/lib/collections/deploy/projects.ts @@ -2,7 +2,7 @@ import { queryCollectionOptions } from "@tanstack/query-db-collection"; import { createCollection } from "@tanstack/react-db"; import { toast } from "@unkey/ui"; import { z } from "zod"; -import { queryClient, trpcClient } from "./client"; +import { queryClient, trpcClient } from "../client"; const schema = z.object({ id: z.string(), diff --git a/apps/dashboard/lib/collections/domains.ts b/apps/dashboard/lib/collections/domains.ts deleted file mode 100644 index 056317550a..0000000000 --- a/apps/dashboard/lib/collections/domains.ts +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; -import { queryCollectionOptions } from "@tanstack/query-db-collection"; -import { createCollection } from "@tanstack/react-db"; -import { z } from "zod"; -import { queryClient, trpcClient } from "./client"; - -const schema = z.object({ - id: z.string(), - domain: z.string(), - type: z.enum(["custom", "wildcard"]), - projectId: z.string().nullable(), -}); - -export type Domain = z.infer; - -export const domains = createCollection( - queryCollectionOptions({ - queryClient, - queryKey: ["domains"], - retry: 3, - queryFn: () => trpcClient.deploy.domain.list.query(), - - getKey: (item) => item.id, - onInsert: async () => { - throw new Error("Not implemented"); - }, - onDelete: async () => { - throw new Error("Not implemented"); - }, - }), -); diff --git a/apps/dashboard/lib/collections/environments.ts b/apps/dashboard/lib/collections/environments.ts deleted file mode 100644 index 97471490f0..0000000000 --- a/apps/dashboard/lib/collections/environments.ts +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; -import { queryCollectionOptions } from "@tanstack/query-db-collection"; -import { createCollection } from "@tanstack/react-db"; -import { z } from "zod"; -import { queryClient, trpcClient } from "./client"; - -const schema = z.object({ - id: z.string(), - projectId: z.string(), - slug: z.string(), -}); - -export type Environment = z.infer; - -export const environments = createCollection( - queryCollectionOptions({ - queryClient, - queryKey: ["environments"], - retry: 3, - queryFn: () => trpcClient.deploy.environment.list.query(), - getKey: (item) => item.id, - onInsert: async () => { - throw new Error("Not implemented"); - // const { changes: newNamespace } = transaction.mutations[0]; - // - // const p = trpcClient.deploy.project.create.mutate(schema.parse({ - // id: "created", // will be replaced by the actual ID after creation - // name: newNamespace.name, - // slug: newNamespace.slug, - // gitRepositoryUrl: newNamespace.gitRepositoryUrl ?? null, - // updatedAt: null, - // })) - // toast.promise(p, { - // loading: "Creating project...", - // success: "Project created", - // error: (res) => { - // console.error("Failed to create project", res); - // return { - // message: "Failed to create project", - // description: res.message, - // }; - // }, - // }); - // await p; - }, - onUpdate: async () => { - throw new Error("Not implemented"); - // const { changes: updatedNamespace } = transaction.mutations[0]; - // - // const p = trpcClient.deploy.project.update.mutate(schema.parse({ - // id: updatedNamespace.id, - // name: updatedNamespace.name, - // slug: updatedNamespace.slug, - // gitRepositoryUrl: updatedNamespace.gitRepositoryUrl ?? null, - // updatedAt: new Date(), - // })); - // toast.promise(p, { - // loading: "Updating project...", - // success: "Project updated", - // error: "Failed to update project", - // }); - // await p; - }, - onDelete: async () => { - throw new Error("Not implemented"); - // const { original } = transaction.mutations[0]; - // const p = trpcClient.deploy.project.delete.mutate({ projectId: original.id }); - // toast.promise(p, { - // loading: "Deleting project...", - // success: "Project deleted", - // error: "Failed to delete project", - // }); - // await p; - }, - }), -); diff --git a/apps/dashboard/lib/collections/index.ts b/apps/dashboard/lib/collections/index.ts index 5fcd6ab171..dd323fa98e 100644 --- a/apps/dashboard/lib/collections/index.ts +++ b/apps/dashboard/lib/collections/index.ts @@ -1,33 +1,85 @@ "use client"; +import { createDeploymentsCollection } from "./deploy/deployments"; +import { createDomainsCollection } from "./deploy/domains"; +import { createEnvironmentsCollection } from "./deploy/environments"; +import { projects } from "./deploy/projects"; +import { ratelimitNamespaces } from "./ratelimit/namespaces"; +import { ratelimitOverrides } from "./ratelimit/overrides"; -import { deployments } from "./deployments"; -import { domains } from "./domains"; -import { environments } from "./environments"; -import { projects } from "./projects"; -import { ratelimitNamespaces } from "./ratelimit_namespaces"; -import { ratelimitOverrides } from "./ratelimit_overrides"; +// Export types +export type { Deployment } from "./deploy/deployments"; +export type { Domain } from "./deploy/domains"; +export type { Project } from "./deploy/projects"; +export type { RatelimitNamespace } from "./ratelimit/namespaces"; +export type { RatelimitOverride } from "./ratelimit/overrides"; +export type { Environment } from "./deploy/environments"; -export type { Deployment } from "./deployments"; -export type { Domain } from "./domains"; -export type { Project } from "./projects"; -export type { RatelimitNamespace } from "./ratelimit_namespaces"; -export type { RatelimitOverride } from "./ratelimit_overrides"; -export type { Environment } from "./environments"; +type ProjectCollections = { + environments: ReturnType; + domains: ReturnType; + deployments: ReturnType; + projects: typeof projects; +}; + +class CollectionManager { + private projectCollections = new Map(); + + getProjectCollections(projectId: string): ProjectCollections { + if (!projectId) { + throw new Error("projectId is required"); + } + if (!this.projectCollections.has(projectId)) { + this.projectCollections.set(projectId, { + environments: createEnvironmentsCollection(projectId), + domains: createDomainsCollection(projectId), + deployments: createDeploymentsCollection(projectId), + projects, + }); + } + // biome-ignore lint/style/noNonNullAssertion: Its okay + return this.projectCollections.get(projectId)!; + } + + async cleanup(projectId: string) { + const collections = this.projectCollections.get(projectId); + if (collections) { + await Promise.all([ + collections.environments.cleanup(), + collections.domains.cleanup(), + collections.deployments.cleanup(), + // Note: projects is shared, don't clean it up per project + ]); + this.projectCollections.delete(projectId); + } + } + + async cleanupAll() { + // Clean up all project collections + const projectCleanupPromises = Array.from(this.projectCollections.keys()).map((projectId) => + this.cleanup(projectId), + ); + + // Clean up global collections + const globalCleanupPromises = Object.values(collection).map((c) => c.cleanup()); + await Promise.all([...projectCleanupPromises, ...globalCleanupPromises]); + } +} + +export const collectionManager = new CollectionManager(); + +// Global collections export const collection = { + projects, ratelimitNamespaces, ratelimitOverrides, - projects, - domains, - deployments, - environments, -}; +} as const; -// resets all collections data and preloads new export async function reset() { + await collectionManager.cleanupAll(); + // Preload global collections after cleanup await Promise.all( Object.values(collection).map(async (c) => { - await c.cleanup(); await c.preload(); }), ); diff --git a/apps/dashboard/lib/collections/ratelimit_namespaces.ts b/apps/dashboard/lib/collections/ratelimit/namespaces.ts similarity index 93% rename from apps/dashboard/lib/collections/ratelimit_namespaces.ts rename to apps/dashboard/lib/collections/ratelimit/namespaces.ts index 15f150095f..ee37bbd9fe 100644 --- a/apps/dashboard/lib/collections/ratelimit_namespaces.ts +++ b/apps/dashboard/lib/collections/ratelimit/namespaces.ts @@ -3,7 +3,7 @@ import { queryCollectionOptions } from "@tanstack/query-db-collection"; import { createCollection } from "@tanstack/react-db"; import { toast } from "@unkey/ui"; import { z } from "zod"; -import { queryClient, trpcClient } from "./client"; +import { queryClient, trpcClient } from "../client"; const schema = z.object({ id: z.string(), @@ -27,7 +27,9 @@ export const ratelimitNamespaces = createCollection( throw new Error("Namespace name is required"); } - const mutation = trpcClient.ratelimit.namespace.create.mutate({ name: newNamespace.name }); + const mutation = trpcClient.ratelimit.namespace.create.mutate({ + name: newNamespace.name, + }); toast.promise(mutation, { loading: "Creating namespace...", success: "Namespace created", @@ -57,7 +59,9 @@ export const ratelimitNamespaces = createCollection( }, onDelete: async ({ transaction }) => { const { original } = transaction.mutations[0]; - const mutation = trpcClient.ratelimit.namespace.delete.mutate({ namespaceId: original.id }); + const mutation = trpcClient.ratelimit.namespace.delete.mutate({ + namespaceId: original.id, + }); toast.promise(mutation, { loading: "Deleting namespace...", success: "Namespace deleted", diff --git a/apps/dashboard/lib/collections/ratelimit_overrides.ts b/apps/dashboard/lib/collections/ratelimit/overrides.ts similarity index 96% rename from apps/dashboard/lib/collections/ratelimit_overrides.ts rename to apps/dashboard/lib/collections/ratelimit/overrides.ts index 47698eca60..3ae65677b8 100644 --- a/apps/dashboard/lib/collections/ratelimit_overrides.ts +++ b/apps/dashboard/lib/collections/ratelimit/overrides.ts @@ -3,7 +3,7 @@ import { queryCollectionOptions } from "@tanstack/query-db-collection"; import { createCollection } from "@tanstack/react-db"; import { toast } from "@unkey/ui"; import { z } from "zod"; -import { queryClient, trpcClient } from "./client"; +import { queryClient, trpcClient } from "../client"; const schema = z.object({ id: z.string(), @@ -56,7 +56,9 @@ export const ratelimitOverrides = createCollection( }, onDelete: async ({ transaction }) => { const { original } = transaction.mutations[0]; - const mutation = trpcClient.ratelimit.override.delete.mutate({ id: original.id }); + const mutation = trpcClient.ratelimit.override.delete.mutate({ + id: original.id, + }); toast.promise(mutation, { loading: "Deleting override...", success: "Override deleted", diff --git a/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts b/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts index 8ea50eb2ba..12ffe8657e 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts @@ -1,15 +1,18 @@ import { db } from "@/lib/db"; import { requireUser, requireWorkspace, t } from "@/lib/trpc/trpc"; import { TRPCError } from "@trpc/server"; +import { z } from "zod"; export const listDeployments = t.procedure .use(requireUser) .use(requireWorkspace) - .query(async ({ ctx }) => { + .input(z.object({ projectId: z.string() })) + .query(async ({ ctx, input }) => { try { - // Get all deployments for this workspace with project info + // Get all deployments for this workspace and specific project const deployments = await db.query.deployments.findMany({ - where: (table, { eq }) => eq(table.workspaceId, ctx.workspace.id), + where: (table, { eq, and }) => + and(eq(table.workspaceId, ctx.workspace.id), eq(table.projectId, input.projectId)), columns: { id: true, projectId: true, diff --git a/apps/dashboard/lib/trpc/routers/deploy/domains/list.ts b/apps/dashboard/lib/trpc/routers/deploy/domains/list.ts index 7a280745af..903acb08e0 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/domains/list.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/domains/list.ts @@ -1,15 +1,18 @@ import { db } from "@/lib/db"; import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc"; import { TRPCError } from "@trpc/server"; +import { z } from "zod"; export const listDomains = t.procedure .use(requireUser) .use(requireWorkspace) .use(withRatelimit(ratelimit.read)) - .query(async ({ ctx }) => { + .input(z.object({ projectId: z.string() })) + .query(async ({ ctx, input }) => { return await db.query.domains .findMany({ - where: (table, { eq }) => eq(table.workspaceId, ctx.workspace.id), + where: (table, { eq, and }) => + and(eq(table.workspaceId, ctx.workspace.id), eq(table.projectId, input.projectId)), columns: { id: true, domain: true, diff --git a/apps/dashboard/lib/trpc/routers/deploy/project/create.ts b/apps/dashboard/lib/trpc/routers/deploy/project/create.ts index bede4ba739..dbe80f4292 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/project/create.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/project/create.ts @@ -1,5 +1,5 @@ import { insertAuditLogs } from "@/lib/audit"; -import { createProjectRequestSchema } from "@/lib/collections/projects"; +import { createProjectRequestSchema } from "@/lib/collections/deploy/projects"; import { db, schema } from "@/lib/db"; import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc"; import { TRPCError } from "@trpc/server"; diff --git a/apps/dashboard/lib/trpc/routers/environment/list.ts b/apps/dashboard/lib/trpc/routers/environment/list.ts index b2bdab433a..45325417b3 100644 --- a/apps/dashboard/lib/trpc/routers/environment/list.ts +++ b/apps/dashboard/lib/trpc/routers/environment/list.ts @@ -1,21 +1,35 @@ -import { db } from "@/lib/db"; +import { and, db, eq } from "@/lib/db"; import { TRPCError } from "@trpc/server"; +import { environments } from "@unkey/db/src/schema"; +import { z } from "zod"; import { requireUser, requireWorkspace, t } from "../../trpc"; export const listEnvironments = t.procedure .use(requireUser) .use(requireWorkspace) - .query(async ({ ctx }) => { + .input( + z.object({ + projectId: z.string(), + }), + ) + .query(async ({ ctx, input }) => { try { return await db.query.environments.findMany({ - where: (table, { eq }) => eq(table.workspaceId, ctx.workspace.id), + where: and( + eq(environments.workspaceId, ctx.workspace.id), + eq(environments.projectId, input.projectId), + ), columns: { id: true, projectId: true, slug: true, }, }); - } catch (_error) { + } catch (error) { + if (error instanceof TRPCError) { + throw error; + } + console.error("Failed to fetch environments:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to fetch environments", From 6081ff31d3a86a0d8d57092aa01e55f3460b3f09 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 18:01:51 +0300 Subject: [PATCH 11/12] refactor: rename activeDeploymentId to liveDeploymentId --- .../deployments/hooks/use-deployments.ts | 12 ++++--- .../project-details-expandables/index.tsx | 2 +- .../app/(app)/projects/[projectId]/page.tsx | 4 +-- .../create-project/create-project-dialog.tsx | 2 +- .../(app)/projects/_components/list/index.tsx | 4 +-- .../lib/collections/deploy/projects.ts | 2 +- .../lib/trpc/routers/deploy/project/create.ts | 2 +- .../lib/trpc/routers/deploy/project/list.ts | 2 +- .../services/deployment/deploy_workflow.go | 8 ++--- go/apps/ctrl/services/routing/service.go | 8 ++--- go/pkg/db/models_generated.go | 22 ++++++------- ...date_active_deployment_id.sql_generated.go | 33 ------------------- ...update_live_deployment_id.sql_generated.go | 33 +++++++++++++++++++ go/pkg/db/querier_generated.go | 8 ++--- .../project_update_active_deployment_id.sql | 4 --- .../project_update_live_deployment_id.sql | 4 +++ go/pkg/db/schema.sql | 3 +- internal/db/src/schema/projects.ts | 2 +- 18 files changed, 79 insertions(+), 76 deletions(-) delete mode 100644 go/pkg/db/project_update_active_deployment_id.sql_generated.go create mode 100644 go/pkg/db/project_update_live_deployment_id.sql_generated.go delete mode 100644 go/pkg/db/queries/project_update_active_deployment_id.sql create mode 100644 go/pkg/db/queries/project_update_live_deployment_id.sql diff --git a/apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts b/apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts index a6b74406c8..87623da551 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts +++ b/apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts @@ -16,15 +16,15 @@ export const useDeployments = () => { .orderBy(({ project }) => project.id, "asc") .limit(1); }); - const activeDeploymentId = project.data.at(0)?.activeDeploymentId; + const liveDeploymentId = project.data.at(0)?.liveDeploymentId; const activeDeployment = useLiveQuery( (q) => q .from({ deployment: collections.deployments }) - .where(({ deployment }) => eq(deployment.id, activeDeploymentId)) + .where(({ deployment }) => eq(deployment.id, liveDeploymentId)) .orderBy(({ deployment }) => deployment.createdAt, "desc") .limit(1), - [activeDeploymentId], + [liveDeploymentId], ); const deployments = useLiveQuery( (q) => { @@ -112,5 +112,9 @@ export const useDeployments = () => { [projectId, filters], ); - return { deployments, activeDeployment, activeDeploymentId }; + return { + deployments, + activeDeployment, + activeDeploymentId: liveDeploymentId, + }; }; diff --git a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx index df58b30f67..972ef07cb6 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx @@ -26,7 +26,7 @@ export const ProjectDetailsExpandable = ({ .from({ project: collection.projects }) .where(({ project }) => eq(project.id, projectId)) .join({ deployment: collections.deployments }, ({ deployment, project }) => - eq(deployment.id, project.activeDeploymentId), + eq(deployment.id, project.liveDeploymentId), ) .orderBy(({ project }) => project.id, "asc") .limit(1), diff --git a/apps/dashboard/app/(app)/projects/[projectId]/page.tsx b/apps/dashboard/app/(app)/projects/[projectId]/page.tsx index c4b3f7d12b..86a9ed5aad 100644 --- a/apps/dashboard/app/(app)/projects/[projectId]/page.tsx +++ b/apps/dashboard/app/(app)/projects/[projectId]/page.tsx @@ -38,13 +38,13 @@ export default function ProjectDetails() { )} >
- {project.activeDeploymentId ? ( + {project.liveDeploymentId ? (
} title="Active Deployment" /> - +
) : null} diff --git a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx index 3227c1657c..9f2abffe54 100644 --- a/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx +++ b/apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx @@ -39,7 +39,7 @@ export const CreateProjectDialog = () => { name: values.name, slug: values.slug, gitRepositoryUrl: values.gitRepositoryUrl || null, - activeDeploymentId: null, + liveDeploymentId: null, updatedAt: null, id: "will-be-replace-by-server", }); diff --git a/apps/dashboard/app/(app)/projects/_components/list/index.tsx b/apps/dashboard/app/(app)/projects/_components/list/index.tsx index 7cec2d9e6d..cd6562aff6 100644 --- a/apps/dashboard/app/(app)/projects/_components/list/index.tsx +++ b/apps/dashboard/app/(app)/projects/_components/list/index.tsx @@ -98,8 +98,8 @@ export const ProjectsList = () => { > {projects.data.map((project) => { // Find active deployment and associated domain for this project - const activeDeployment = project.activeDeploymentId - ? allDeployments.find((d) => d.id === project.activeDeploymentId) + const activeDeployment = project.liveDeploymentId + ? allDeployments.find((d) => d.id === project.liveDeploymentId) : null; // Find domain for this project diff --git a/apps/dashboard/lib/collections/deploy/projects.ts b/apps/dashboard/lib/collections/deploy/projects.ts index 16224cbe36..ee5a22ec24 100644 --- a/apps/dashboard/lib/collections/deploy/projects.ts +++ b/apps/dashboard/lib/collections/deploy/projects.ts @@ -10,7 +10,7 @@ const schema = z.object({ slug: z.string(), gitRepositoryUrl: z.string().nullable(), updatedAt: z.number().int().nullable(), - activeDeploymentId: z.string().nullable(), + liveDeploymentId: z.string().nullable(), }); export const createProjectRequestSchema = z.object({ diff --git a/apps/dashboard/lib/trpc/routers/deploy/project/create.ts b/apps/dashboard/lib/trpc/routers/deploy/project/create.ts index dbe80f4292..f30c7d2f48 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/project/create.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/project/create.ts @@ -70,7 +70,7 @@ export const createProject = t.procedure workspaceId, name: input.name, slug: input.slug, - activeDeploymentId: null, + liveDeploymentId: null, gitRepositoryUrl: input.gitRepositoryUrl || null, defaultBranch: "main", deleteProtection: false, diff --git a/apps/dashboard/lib/trpc/routers/deploy/project/list.ts b/apps/dashboard/lib/trpc/routers/deploy/project/list.ts index 71351fc4b8..5c279b5acd 100644 --- a/apps/dashboard/lib/trpc/routers/deploy/project/list.ts +++ b/apps/dashboard/lib/trpc/routers/deploy/project/list.ts @@ -16,7 +16,7 @@ export const listProjects = t.procedure slug: true, updatedAt: true, gitRepositoryUrl: true, - activeDeploymentId: true, + liveDeploymentId: true, }, }) .catch((error) => { diff --git a/go/apps/ctrl/services/deployment/deploy_workflow.go b/go/apps/ctrl/services/deployment/deploy_workflow.go index 1b22a38c8e..5a9a8e721a 100644 --- a/go/apps/ctrl/services/deployment/deploy_workflow.go +++ b/go/apps/ctrl/services/deployment/deploy_workflow.go @@ -382,10 +382,10 @@ func (w *DeployWorkflow) Run(ctx hydra.WorkflowContext, req *DeployRequest) erro w.logger.Info("deployment status updated to ready", "deployment_id", req.DeploymentID) // TODO: This section will be removed in the future in favor of "Promote to Production" - err = db.Query.UpdateProjectActiveDeploymentId(stepCtx, w.db.RW(), db.UpdateProjectActiveDeploymentIdParams{ - ID: req.ProjectID, - ActiveDeploymentID: sql.NullString{Valid: true, String: req.DeploymentID}, - UpdatedAt: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()}, + err = db.Query.UpdateProjectLiveDeploymentId(stepCtx, w.db.RW(), db.UpdateProjectLiveDeploymentIdParams{ + ID: req.ProjectID, + LiveDeploymentID: sql.NullString{Valid: true, String: req.DeploymentID}, + UpdatedAt: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()}, }) if err != nil { return nil, fmt.Errorf("failed to update project %s active deployment ID to %s: %w", req.ProjectID, req.DeploymentID, err) diff --git a/go/apps/ctrl/services/routing/service.go b/go/apps/ctrl/services/routing/service.go index c66cd159a6..2fe351012c 100644 --- a/go/apps/ctrl/services/routing/service.go +++ b/go/apps/ctrl/services/routing/service.go @@ -358,10 +358,10 @@ func (s *Service) Rollback(ctx context.Context, req *connect.Request[ctrlv1.Roll slog.String("new_deployment_id", targetDeploymentID), ) - err = db.Query.UpdateProjectActiveDeploymentId(ctx, s.db.RW(), db.UpdateProjectActiveDeploymentIdParams{ - ID: deployment.ProjectID, - ActiveDeploymentID: sql.NullString{Valid: true, String: targetDeploymentID}, - UpdatedAt: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()}, + err = db.Query.UpdateProjectLiveDeploymentId(ctx, s.db.RW(), db.UpdateProjectLiveDeploymentIdParams{ + ID: deployment.ProjectID, + LiveDeploymentID: sql.NullString{Valid: true, String: targetDeploymentID}, + UpdatedAt: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()}, }) if err != nil { s.logger.ErrorContext(ctx, "failed to update project active deployment ID", diff --git a/go/pkg/db/models_generated.go b/go/pkg/db/models_generated.go index dd048fd82d..98ef72aca3 100644 --- a/go/pkg/db/models_generated.go +++ b/go/pkg/db/models_generated.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.27.0 package db @@ -669,16 +669,16 @@ type Permission struct { } type Project struct { - ID string `db:"id"` - WorkspaceID string `db:"workspace_id"` - Name string `db:"name"` - Slug string `db:"slug"` - GitRepositoryUrl sql.NullString `db:"git_repository_url"` - ActiveDeploymentID sql.NullString `db:"active_deployment_id"` - DefaultBranch sql.NullString `db:"default_branch"` - DeleteProtection sql.NullBool `db:"delete_protection"` - CreatedAt int64 `db:"created_at"` - UpdatedAt sql.NullInt64 `db:"updated_at"` + ID string `db:"id"` + WorkspaceID string `db:"workspace_id"` + Name string `db:"name"` + Slug string `db:"slug"` + GitRepositoryUrl sql.NullString `db:"git_repository_url"` + LiveDeploymentID sql.NullString `db:"live_deployment_id"` + DefaultBranch sql.NullString `db:"default_branch"` + DeleteProtection sql.NullBool `db:"delete_protection"` + CreatedAt int64 `db:"created_at"` + UpdatedAt sql.NullInt64 `db:"updated_at"` } type Quotum struct { diff --git a/go/pkg/db/project_update_active_deployment_id.sql_generated.go b/go/pkg/db/project_update_active_deployment_id.sql_generated.go deleted file mode 100644 index 4b54642eec..0000000000 --- a/go/pkg/db/project_update_active_deployment_id.sql_generated.go +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: project_update_active_deployment_id.sql - -package db - -import ( - "context" - "database/sql" -) - -const updateProjectActiveDeploymentId = `-- name: UpdateProjectActiveDeploymentId :exec -UPDATE projects -SET active_deployment_id = ?, updated_at = ? -WHERE id = ? -` - -type UpdateProjectActiveDeploymentIdParams struct { - ActiveDeploymentID sql.NullString `db:"active_deployment_id"` - UpdatedAt sql.NullInt64 `db:"updated_at"` - ID string `db:"id"` -} - -// UpdateProjectActiveDeploymentId -// -// UPDATE projects -// SET active_deployment_id = ?, updated_at = ? -// WHERE id = ? -func (q *Queries) UpdateProjectActiveDeploymentId(ctx context.Context, db DBTX, arg UpdateProjectActiveDeploymentIdParams) error { - _, err := db.ExecContext(ctx, updateProjectActiveDeploymentId, arg.ActiveDeploymentID, arg.UpdatedAt, arg.ID) - return err -} diff --git a/go/pkg/db/project_update_live_deployment_id.sql_generated.go b/go/pkg/db/project_update_live_deployment_id.sql_generated.go new file mode 100644 index 0000000000..3368c1a7fc --- /dev/null +++ b/go/pkg/db/project_update_live_deployment_id.sql_generated.go @@ -0,0 +1,33 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: project_update_live_deployment_id.sql + +package db + +import ( + "context" + "database/sql" +) + +const updateProjectLiveDeploymentId = `-- name: UpdateProjectLiveDeploymentId :exec +UPDATE projects +SET live_deployment_id = ?, updated_at = ? +WHERE id = ? +` + +type UpdateProjectLiveDeploymentIdParams struct { + LiveDeploymentID sql.NullString `db:"live_deployment_id"` + UpdatedAt sql.NullInt64 `db:"updated_at"` + ID string `db:"id"` +} + +// UpdateProjectLiveDeploymentId +// +// UPDATE projects +// SET live_deployment_id = ?, updated_at = ? +// WHERE id = ? +func (q *Queries) UpdateProjectLiveDeploymentId(ctx context.Context, db DBTX, arg UpdateProjectLiveDeploymentIdParams) error { + _, err := db.ExecContext(ctx, updateProjectLiveDeploymentId, arg.LiveDeploymentID, arg.UpdatedAt, arg.ID) + return err +} diff --git a/go/pkg/db/querier_generated.go b/go/pkg/db/querier_generated.go index 107c9b5618..b63b885ea9 100644 --- a/go/pkg/db/querier_generated.go +++ b/go/pkg/db/querier_generated.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.27.0 package db @@ -1674,12 +1674,12 @@ type Querier interface { // // UPDATE `key_auth` SET store_encrypted_keys = ? WHERE id = ? UpdateKeyringKeyEncryption(ctx context.Context, db DBTX, arg UpdateKeyringKeyEncryptionParams) error - //UpdateProjectActiveDeploymentId + //UpdateProjectLiveDeploymentId // // UPDATE projects - // SET active_deployment_id = ?, updated_at = ? + // SET live_deployment_id = ?, updated_at = ? // WHERE id = ? - UpdateProjectActiveDeploymentId(ctx context.Context, db DBTX, arg UpdateProjectActiveDeploymentIdParams) error + UpdateProjectLiveDeploymentId(ctx context.Context, db DBTX, arg UpdateProjectLiveDeploymentIdParams) error //UpdateRatelimit // // UPDATE `ratelimits` diff --git a/go/pkg/db/queries/project_update_active_deployment_id.sql b/go/pkg/db/queries/project_update_active_deployment_id.sql deleted file mode 100644 index e5e7044c91..0000000000 --- a/go/pkg/db/queries/project_update_active_deployment_id.sql +++ /dev/null @@ -1,4 +0,0 @@ --- name: UpdateProjectActiveDeploymentId :exec -UPDATE projects -SET active_deployment_id = ?, updated_at = ? -WHERE id = ?; diff --git a/go/pkg/db/queries/project_update_live_deployment_id.sql b/go/pkg/db/queries/project_update_live_deployment_id.sql new file mode 100644 index 0000000000..45d8c52dc3 --- /dev/null +++ b/go/pkg/db/queries/project_update_live_deployment_id.sql @@ -0,0 +1,4 @@ +-- name: UpdateProjectLiveDeploymentId :exec +UPDATE projects +SET live_deployment_id = ?, updated_at = ? +WHERE id = ?; diff --git a/go/pkg/db/schema.sql b/go/pkg/db/schema.sql index 296b70cc35..cb7efc4868 100644 --- a/go/pkg/db/schema.sql +++ b/go/pkg/db/schema.sql @@ -313,7 +313,7 @@ CREATE TABLE `projects` ( `name` varchar(256) NOT NULL, `slug` varchar(256) NOT NULL, `git_repository_url` varchar(500), - `active_deployment_id` varchar(256), + `live_deployment_id` varchar(256), `default_branch` varchar(256) DEFAULT 'main', `delete_protection` boolean DEFAULT false, `created_at` bigint NOT NULL, @@ -415,4 +415,3 @@ CREATE INDEX `workspace_idx` ON `domains` (`workspace_id`); CREATE INDEX `project_idx` ON `domains` (`project_id`); CREATE INDEX `workspace_idx` ON `acme_challenges` (`workspace_id`); CREATE INDEX `status_idx` ON `acme_challenges` (`status`); - diff --git a/internal/db/src/schema/projects.ts b/internal/db/src/schema/projects.ts index b9551e5c04..a55fac12d0 100644 --- a/internal/db/src/schema/projects.ts +++ b/internal/db/src/schema/projects.ts @@ -18,7 +18,7 @@ export const projects = mysqlTable( gitRepositoryUrl: varchar("git_repository_url", { length: 500 }), // this is likely temporary but we need a way to point to the current prod deployment. // in the future I think we want to have a special deployment per environment, but for now this is fine - activeDeploymentId: varchar("active_deployment_id", { length: 256 }), + liveDeploymentId: varchar("live_deployment_id", { length: 256 }), defaultBranch: varchar("default_branch", { length: 256 }).default("main"), ...deleteProtection, From 29e4fa0fbee6731c5ddb1b752d6ef49a94112f84 Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 16 Sep 2025 18:21:00 +0300 Subject: [PATCH 12/12] fix: sqlc versions --- go/pkg/db/models_generated.go | 2 +- go/pkg/db/project_update_live_deployment_id.sql_generated.go | 2 +- go/pkg/db/querier_generated.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go/pkg/db/models_generated.go b/go/pkg/db/models_generated.go index 98ef72aca3..832653dbd4 100644 --- a/go/pkg/db/models_generated.go +++ b/go/pkg/db/models_generated.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.29.0 package db diff --git a/go/pkg/db/project_update_live_deployment_id.sql_generated.go b/go/pkg/db/project_update_live_deployment_id.sql_generated.go index 3368c1a7fc..e68c491f75 100644 --- a/go/pkg/db/project_update_live_deployment_id.sql_generated.go +++ b/go/pkg/db/project_update_live_deployment_id.sql_generated.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.29.0 // source: project_update_live_deployment_id.sql package db diff --git a/go/pkg/db/querier_generated.go b/go/pkg/db/querier_generated.go index b63b885ea9..3ba37dc3e4 100644 --- a/go/pkg/db/querier_generated.go +++ b/go/pkg/db/querier_generated.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.29.0 package db