Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"use client";

import { type Deployment, collection } from "@/lib/collections";
import { trpc } from "@/lib/trpc/client";
import { CircleInfo, Cloud, CodeBranch, CodeCommit } from "@unkey/icons";
import { Badge, Button, DialogContainer, toast } from "@unkey/ui";

type RollbackDialogProps = {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
deployment: Deployment;
currentDeployment: Deployment;
hostname?: string;
};

export const RollbackDialog = ({
isOpen,
onOpenChange,
deployment,
currentDeployment,
hostname,
}: RollbackDialogProps) => {
const utils = trpc.useUtils();
const rollback = trpc.deploy.rollback.useMutation({
onSuccess: () => {
utils.invalidate();
toast.success("Rollback completed", {
description: `Successfully rolled back to deployment ${deployment.id}`,
});
// hack to revalidate
try {
// @ts-expect-error Their docs say it's here
collection.projects.utils.refetch();
} catch (error) {
console.error("Refetch error:", error);
}

onOpenChange(false);
},
onError: (error) => {
toast.error("Rollback failed", {
description: error.message,
});
},
});

const handleRollback = async () => {
if (!hostname) {
toast.error("Missing hostname", {
description: "Cannot perform rollback without hostname information",
});
return;
}

try {
await rollback.mutateAsync({
hostname,
targetDeploymentId: deployment.id,
});
} catch (error) {
console.error("Rollback error:", error);
}
};

return (
<DialogContainer
isOpen={isOpen}
onOpenChange={onOpenChange}
title="Rollback to version"
subTitle="Switch the active deployment to a target stable version"
footer={
<div className="flex flex-col items-center w-full gap-2">
<Button
variant="primary"
size="xlg"
onClick={handleRollback}
disabled={rollback.isLoading}
loading={rollback.isLoading}
className="w-full rounded-lg"
>
Rollback to target version
</Button>
<div className="text-xs text-gray-9">Rollbacks usually complete within seconds</div>
</div>
}
>
<div className="space-y-6">
{/* Current active deployment */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium text-gray-12">Current active deployment</h3>
<CircleInfo size="sm-regular" className="text-gray-9" />
</div>

<div className="bg-gray-2 border border-gray-6 rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Cloud size="md-regular" className="text-gray-11 bg-gray-3 rounded" />
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-12">{currentDeployment.id}</span>
<Badge variant="success" className="text-successA-11 font-medium">
<div className="flex items-center gap-2">Active</div>
</Badge>
</div>
<div className="text-xs text-gray-11">
{currentDeployment?.gitCommitMessage || "Current active deployment"}
</div>
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-3 rounded text-xs text-gray-11">
<CodeBranch size="sm-regular" />
<span>{currentDeployment.gitBranch}</span>
</div>
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-3 rounded text-xs text-gray-11">
<CodeCommit size="sm-regular" />
<span>{currentDeployment.gitCommitSha}</span>
</div>
</div>
</div>
</div>
</div>

{/* Target version */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium text-gray-12">Target version</h3>
<CircleInfo size="sm-regular" className="text-gray-9" />
</div>

<div className="bg-gray-2 border border-gray-6 rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Cloud size="md-regular" className="text-gray-11 bg-gray-3 rounded" />
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-12">{deployment.id}</span>
<Badge variant="primary" className="text-primaryA-11 font-medium">
<div className="flex items-center gap-1">Inactive</div>
</Badge>
</div>
<div className="text-xs text-gray-11">
{deployment.gitCommitMessage || "Target deployment"}
</div>
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-3 rounded text-xs text-gray-11">
<CodeBranch size="sm-regular" />
<span>{deployment.gitBranch}</span>
</div>
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-3 rounded text-xs text-gray-11">
<CodeCommit size="sm-regular" />
<span>{deployment.gitCommitSha}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</DialogContainer>
);
};
Original file line number Diff line number Diff line change
@@ -1,24 +1,60 @@
"use client";
import { type MenuItem, TableActionPopover } from "@/components/logs/table-action.popover";
import type { Deployment } from "@/lib/collections";
import { PenWriting3 } from "@unkey/icons";
import type { Deployment, Environment } from "@/lib/collections";
import { ArrowDottedRotateAnticlockwise, PenWriting3 } from "@unkey/icons";
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { RollbackDialog } from "../../../rollback-dialog";

type DeploymentListTableActionsProps = {
deployment: Deployment;
currentActiveDeployment?: Deployment;
environment?: Environment;
};

export const DeploymentListTableActions = ({ deployment }: DeploymentListTableActionsProps) => {
export const DeploymentListTableActions = ({
deployment,
currentActiveDeployment,
environment,
}: DeploymentListTableActionsProps) => {
const router = useRouter();
const menuItems = getDeploymentListTableActionItems(deployment, router);
return <TableActionPopover items={menuItems} />;
const [isRollbackModalOpen, setIsRollbackModalOpen] = useState(false);
const menuItems = getDeploymentListTableActionItems(
deployment,
environment,
router,
setIsRollbackModalOpen,
);

return (
<>
<TableActionPopover items={menuItems} />
{currentActiveDeployment && (
<RollbackDialog
isOpen={isRollbackModalOpen}
onOpenChange={setIsRollbackModalOpen}
deployment={deployment}
currentDeployment={currentActiveDeployment}
hostname="example.com" // TODO: Get actual hostname from deployment/project
/>
)}
</>
);
};

const getDeploymentListTableActionItems = (
deployment: Deployment,
environment: Environment | undefined,
router: AppRouterInstance,
setIsRollbackModalOpen: (open: boolean) => void,
): MenuItem[] => {
// Rollback is only enabled for production deployments that are ready and not currently active
const canRollback =
environment?.slug === "production" &&
deployment.status === "ready" &&
deployment.id !== "current_active_deployment_id"; // TODO: Better way to determine if this is the current active deployment

return [
{
id: "edit-root-key",
Expand All @@ -28,5 +64,16 @@ const getDeploymentListTableActionItems = (
router.push(`/settings/root-keys/${deployment.id}`);
},
},
{
id: "rollback",
label: "Rollback",
icon: <ArrowDottedRotateAnticlockwise size="md-regular" />,
disabled: !canRollback,
onClick: () => {
if (canRollback) {
setIsRollbackModalOpen(true);
}
},
},
];
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ type Props = {
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
Expand Down Expand Up @@ -174,7 +194,7 @@ export const DeploymentsList = ({ projectId }: Props) => {
>
{shortenId(deployment.id)}
</div>
{environment?.slug === "production" ? (
{deployment.id === activeDeploymentId ? (
<EnvStatusBadge variant="current" text="Current" />
) : null}
</div>
Expand Down Expand Up @@ -363,12 +383,21 @@ export const DeploymentsList = ({ projectId }: Props) => {
key: "action",
header: "",
width: "auto",
render: ({ deployment }: { deployment: Deployment }) => {
return <DeploymentListTableActions deployment={deployment} />;
render: ({
deployment,
environment,
}: { deployment: Deployment; environment?: Environment }) => {
return (
<DeploymentListTableActions
deployment={deployment}
currentActiveDeployment={activeDeployment.data.at(0)}
environment={environment}
/>
);
},
},
];
}, [selectedDeployment, isCompactView]);
}, [selectedDeployment?.deployment.id, isCompactView, deployments, activeDeployment]);

return (
<VirtualTable
Expand Down
3 changes: 2 additions & 1 deletion apps/dashboard/lib/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ export type UnkeyAuditLog = {
| "project"
| "identity"
| "auditLogBucket"
| "environment";
| "environment"
| "deployment";

id: string;
name?: string;
Expand Down
18 changes: 18 additions & 0 deletions apps/dashboard/lib/collections/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ export const environments = createCollection<Environment>(
// });
// 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];
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const env = () =>
AGENT_URL: z.string().url(),
AGENT_TOKEN: z.string(),

CTRL_URL: z.string().url().optional(),

GITHUB_KEYS_URI: z.string().optional(),

// This key is used for ratelimiting our trpc procedures
Expand Down
4 changes: 4 additions & 0 deletions apps/dashboard/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ 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";
Expand Down Expand Up @@ -330,6 +331,9 @@ export const router = t.router({
environmentVariables: t.router({
list: getEnvs,
}),
deploy: t.router({
rollback: rollback,
}),
});

// export type definition of API
Expand Down
Loading