diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/promotion-dialog.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/promotion-dialog.tsx
deleted file mode 100644
index f2a3820abd..0000000000
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/promotion-dialog.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-"use client";
-
-import { type Deployment, collection, collectionManager } from "@/lib/collections";
-import { shortenId } from "@/lib/shorten-id";
-import { trpc } from "@/lib/trpc/client";
-import { eq, inArray, useLiveQuery } from "@tanstack/react-db";
-import { CircleInfo, CodeBranch, CodeCommit, Link4 } from "@unkey/icons";
-import { Badge, Button, DialogContainer, toast } from "@unkey/ui";
-import { StatusIndicator } from "../../details/active-deployment-card/status-indicator";
-
-type DeploymentSectionProps = {
- title: string;
- deployment: Deployment;
- isLive: boolean;
- showSignal?: boolean;
-};
-
-const DeploymentSection = ({ title, deployment, isLive, showSignal }: DeploymentSectionProps) => (
-
-);
-
-type PromotionDialogProps = {
- isOpen: boolean;
- onClose: () => void;
- targetDeployment: Deployment;
- liveDeployment: Deployment;
-};
-
-export const PromotionDialog = ({
- isOpen,
- onClose,
- targetDeployment,
- liveDeployment,
-}: PromotionDialogProps) => {
- const utils = trpc.useUtils();
- const domainCollection = collectionManager.getProjectCollections(
- liveDeployment.projectId,
- ).domains;
- const domains = useLiveQuery((q) =>
- q
- .from({ domain: domainCollection })
- .where(({ domain }) => inArray(domain.sticky, ["environment", "live"]))
- .where(({ domain }) => eq(domain.deploymentId, liveDeployment.id)),
- );
- const promote = trpc.deploy.deployment.promote.useMutation({
- onSuccess: () => {
- utils.invalidate();
- toast.success("Promotion completed", {
- description: `Successfully promoted to deployment ${targetDeployment.id}`,
- });
- // hack to revalidate
- try {
- // @ts-expect-error Their docs say it's here
- collection.projects.utils.refetch();
- // @ts-expect-error Their docs say it's here
- collection.deployments.utils.refetch();
- // @ts-expect-error Their docs say it's here
- collection.domains.utils.refetch();
- } catch (error) {
- console.error("Refetch error:", error);
- }
-
- onClose();
- },
- onError: (error) => {
- toast.error("Promotion failed", {
- description: error.message,
- });
- },
- });
-
- const handlePromotion = async () => {
- await promote
- .mutateAsync({
- targetDeploymentId: targetDeployment.id,
- })
- .catch((error) => {
- console.error("Promotion error:", error);
- });
- };
-
- return (
-
- Promote to
- {targetDeployment.gitCommitSha
- ? shortenId(targetDeployment.gitCommitSha)
- : targetDeployment.id}
-
- }
- >
-
-
-
- {domains.data.map((domain) => (
-
- ))}
-
-
-
-
- );
-};
-
-type DeploymentCardProps = {
- deployment: Deployment;
- isLive: boolean;
- showSignal?: boolean;
-};
-
-const DeploymentCard = ({ deployment, isLive, showSignal }: DeploymentCardProps) => (
-
-
-
-
-
-
-
- {`${deployment.id.slice(0, 3)}...${deployment.id.slice(-4)}`}
-
-
- {isLive ? "Live" : deployment.status}
-
-
-
- {deployment.gitCommitMessage || `${isLive ? "Current active" : "Target"} deployment`}
-
-
-
-
-
-
- {deployment.gitBranch}
-
-
-
- {shortenId(deployment.gitCommitSha ?? "")}
-
-
-
-
-);
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/rollback-dialog.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/rollback-dialog.tsx
deleted file mode 100644
index 223377e42b..0000000000
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/rollback-dialog.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-"use client";
-
-import { type Deployment, collection } from "@/lib/collections";
-import { shortenId } from "@/lib/shorten-id";
-import { trpc } from "@/lib/trpc/client";
-import { inArray, useLiveQuery } from "@tanstack/react-db";
-import { CircleInfo, CodeBranch, CodeCommit, Link4 } from "@unkey/icons";
-import { Badge, Button, DialogContainer, toast } from "@unkey/ui";
-import { StatusIndicator } from "../../details/active-deployment-card/status-indicator";
-import { useProject } from "../../layout-provider";
-
-type DeploymentSectionProps = {
- title: string;
- deployment: Deployment;
- isLive: boolean;
- showSignal?: boolean;
-};
-
-const DeploymentSection = ({ title, deployment, isLive, showSignal }: DeploymentSectionProps) => (
-
-);
-
-type RollbackDialogProps = {
- isOpen: boolean;
- onClose: () => void;
- targetDeployment: Deployment;
- liveDeployment: Deployment;
-};
-
-export const RollbackDialog = ({
- isOpen,
- onClose,
- targetDeployment,
- liveDeployment,
-}: RollbackDialogProps) => {
- const utils = trpc.useUtils();
-
- const {
- collections: { domains: domainCollection },
- } = useProject();
- const domains = useLiveQuery((q) =>
- q
- .from({ domain: domainCollection })
- .where(({ domain }) => inArray(domain.sticky, ["environment", "live"])),
- );
-
- const rollback = trpc.deploy.deployment.rollback.useMutation({
- onSuccess: () => {
- utils.invalidate();
- toast.success("Rollback completed", {
- description: `Successfully rolled back to deployment ${targetDeployment.id}`,
- });
- // hack to revalidate
- try {
- // @ts-expect-error Their docs say it's here
- collection.projects.utils.refetch();
- // @ts-expect-error Their docs say it's here
- collection.deployments.utils.refetch();
- // @ts-expect-error Their docs say it's here
- collection.domains.utils.refetch();
- } catch (error) {
- console.error("Refetch error:", error);
- }
-
- onClose();
- },
- onError: (error) => {
- toast.error("Rollback failed", {
- description: error.message,
- });
- },
- });
-
- const handleRollback = async () => {
- await rollback
- .mutateAsync({
- targetDeploymentId: targetDeployment.id,
- })
- .catch((error) => {
- console.error("Rollback error:", error);
- });
- };
-
- return (
-
- Rollback to target version
-
- }
- >
-
-
-
- {domains.data.map((domain) => (
-
- ))}
-
-
-
-
- );
-};
-
-type DeploymentCardProps = {
- deployment: Deployment;
- isLive: boolean;
- showSignal?: boolean;
-};
-
-const DeploymentCard = ({ deployment, isLive, showSignal }: DeploymentCardProps) => (
-
-
-
-
-
-
-
- {`${deployment.id.slice(0, 3)}...${deployment.id.slice(-4)}`}
-
-
- {isLive ? "Live" : deployment.status}
-
-
-
- {deployment.gitCommitMessage || `${isLive ? "Current active" : "Target"} deployment`}
-
-
-
-
-
-
- {deployment.gitBranch}
-
-
-
- {shortenId(deployment.gitCommitSha ?? "")}
-
-
-
-
-);
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/deployment-card.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/deployment-card.tsx
new file mode 100644
index 0000000000..0c972974db
--- /dev/null
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/deployment-card.tsx
@@ -0,0 +1,47 @@
+import { StatusIndicator } from "@/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/status-indicator";
+import type { Deployment } from "@/lib/collections";
+import { shortenId } from "@/lib/shorten-id";
+import { CodeBranch, CodeCommit } from "@unkey/icons";
+import { Badge } from "@unkey/ui";
+
+type DeploymentCardProps = {
+ deployment: Deployment;
+ isLive: boolean;
+ showSignal?: boolean;
+};
+
+export const DeploymentCard = ({ deployment, isLive, showSignal }: DeploymentCardProps) => (
+
+
+
+
+
+
+
+ {`${deployment.id.slice(0, 3)}...${deployment.id.slice(-4)}`}
+
+
+ {isLive ? "Live" : deployment.status}
+
+
+
+ {deployment.gitCommitMessage || `${isLive ? "Current active" : "Target"} deployment`}
+
+
+
+
+
+
+ {deployment.gitBranch}
+
+
+
+ {shortenId(deployment.gitCommitSha ?? "")}
+
+
+
+
+);
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/deployment-section.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/deployment-section.tsx
new file mode 100644
index 0000000000..d23dd16636
--- /dev/null
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/deployment-section.tsx
@@ -0,0 +1,25 @@
+import type { Deployment } from "@/lib/collections";
+import { CircleInfo } from "@unkey/icons";
+import { DeploymentCard } from "./deployment-card";
+
+type DeploymentSectionProps = {
+ title: string;
+ deployment: Deployment;
+ isLive: boolean;
+ showSignal?: boolean;
+};
+
+export const DeploymentSection = ({
+ title,
+ deployment,
+ isLive,
+ showSignal,
+}: DeploymentSectionProps) => (
+
+);
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/domains-section.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/domains-section.tsx
new file mode 100644
index 0000000000..220e7a6cfd
--- /dev/null
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/components/domains-section.tsx
@@ -0,0 +1,30 @@
+import { DomainRow } from "@/app/(app)/[workspaceSlug]/projects/[projectId]/details/domain-row";
+import { CircleInfo } from "@unkey/icons";
+
+type DomainsSectionProps = {
+ domains: Array<{ id: string; domain: string }>;
+};
+
+export const DomainsSection = ({ domains }: DomainsSectionProps) => {
+ if (domains.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
{domains.length === 1 ? "Domain" : "Domains"}
+
+
+
+ {domains.map((domain) => (
+
+ ))}
+
+
+ );
+};
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx
index 6207ead338..9a303b41bb 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/deployment-list-table-action.popover.constants.tsx
@@ -7,8 +7,8 @@ import { eq, useLiveQuery } from "@tanstack/react-db";
import { ArrowDottedRotateAnticlockwise, ChevronUp, Layers3 } from "@unkey/icons";
import { useRouter } from "next/navigation";
import { useMemo } from "react";
-import { PromotionDialog } from "../../../promotion-dialog";
-import { RollbackDialog } from "../../../rollback-dialog";
+import { PromotionDialog } from "./promotion-dialog";
+import { RollbackDialog } from "./rollback-dialog";
type DeploymentListTableActionsProps = {
liveDeployment?: Deployment;
@@ -80,7 +80,7 @@ export const DeploymentListTableActions = ({
onClick: () => {
//INFO: This will produce a long query, but once we start using `contains` instead of `is` this will be a shorter query.
router.push(
- `${workspace.slug}/projects/${selectedDeployment.projectId}/gateway-logs?host=${data
+ `/${workspace.slug}/projects/${selectedDeployment.projectId}/gateway-logs?host=${data
.map((item) => `is:${item.host}`)
.join(",")}`,
);
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/promotion-dialog.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/promotion-dialog.tsx
new file mode 100644
index 0000000000..155c152226
--- /dev/null
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/promotion-dialog.tsx
@@ -0,0 +1,105 @@
+"use client";
+
+import { type Deployment, collection, collectionManager } from "@/lib/collections";
+import { shortenId } from "@/lib/shorten-id";
+import { trpc } from "@/lib/trpc/client";
+import { eq, inArray, useLiveQuery } from "@tanstack/react-db";
+import { Button, DialogContainer, toast } from "@unkey/ui";
+import { DeploymentSection } from "./components/deployment-section";
+import { DomainsSection } from "./components/domains-section";
+
+type PromotionDialogProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ targetDeployment: Deployment;
+ liveDeployment: Deployment;
+};
+
+export const PromotionDialog = ({
+ isOpen,
+ onClose,
+ targetDeployment,
+ liveDeployment,
+}: PromotionDialogProps) => {
+ const utils = trpc.useUtils();
+ const domainCollection = collectionManager.getProjectCollections(
+ liveDeployment.projectId,
+ ).domains;
+ const domains = useLiveQuery((q) =>
+ q
+ .from({ domain: domainCollection })
+ .where(({ domain }) => inArray(domain.sticky, ["environment", "live"]))
+ .where(({ domain }) => eq(domain.deploymentId, liveDeployment.id)),
+ );
+ const promote = trpc.deploy.deployment.promote.useMutation({
+ onSuccess: () => {
+ utils.invalidate();
+ toast.success("Promotion completed", {
+ description: `Successfully promoted to deployment ${targetDeployment.id}`,
+ });
+ // hack to revalidate
+ try {
+ // @ts-expect-error Their docs say it's here
+ collection.projects.utils.refetch();
+ // @ts-expect-error Their docs say it's here
+ collection.deployments.utils.refetch();
+ // @ts-expect-error Their docs say it's here
+ collection.domains.utils.refetch();
+ } catch (error) {
+ console.error("Refetch error:", error);
+ }
+
+ onClose();
+ },
+ onError: (error) => {
+ toast.error("Promotion failed", {
+ description: error.message,
+ });
+ },
+ });
+
+ const handlePromotion = async () => {
+ await promote
+ .mutateAsync({
+ targetDeploymentId: targetDeployment.id,
+ })
+ .catch((error) => {
+ console.error("Promotion error:", error);
+ });
+ };
+
+ return (
+
+ Promote to
+ {targetDeployment.gitCommitSha
+ ? shortenId(targetDeployment.gitCommitSha)
+ : targetDeployment.id}
+
+ }
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/rollback-dialog.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/rollback-dialog.tsx
new file mode 100644
index 0000000000..afb05b5585
--- /dev/null
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/components/actions/rollback-dialog.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import { type Deployment, collection } from "@/lib/collections";
+import { trpc } from "@/lib/trpc/client";
+import { inArray, useLiveQuery } from "@tanstack/react-db";
+import { Button, DialogContainer, toast } from "@unkey/ui";
+import { useProject } from "../../../../../layout-provider";
+import { DeploymentSection } from "./components/deployment-section";
+import { DomainsSection } from "./components/domains-section";
+
+type RollbackDialogProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ targetDeployment: Deployment;
+ liveDeployment: Deployment;
+};
+
+export const RollbackDialog = ({
+ isOpen,
+ onClose,
+ targetDeployment,
+ liveDeployment,
+}: RollbackDialogProps) => {
+ const utils = trpc.useUtils();
+
+ const {
+ collections: { domains: domainCollection },
+ } = useProject();
+ const domains = useLiveQuery((q) =>
+ q
+ .from({ domain: domainCollection })
+ .where(({ domain }) => inArray(domain.sticky, ["environment", "live"])),
+ );
+
+ const rollback = trpc.deploy.deployment.rollback.useMutation({
+ onSuccess: () => {
+ utils.invalidate();
+ toast.success("Rollback completed", {
+ description: `Successfully rolled back to deployment ${targetDeployment.id}`,
+ });
+ // hack to revalidate
+ try {
+ // @ts-expect-error Their docs say it's here
+ collection.projects.utils.refetch();
+ // @ts-expect-error Their docs say it's here
+ collection.deployments.utils.refetch();
+ // @ts-expect-error Their docs say it's here
+ collection.domains.utils.refetch();
+ } catch (error) {
+ console.error("Refetch error:", error);
+ }
+
+ onClose();
+ },
+ onError: (error) => {
+ toast.error("Rollback failed", {
+ description: error.message,
+ });
+ },
+ });
+
+ const handleRollback = async () => {
+ await rollback
+ .mutateAsync({
+ targetDeploymentId: targetDeployment.id,
+ })
+ .catch((error) => {
+ console.error("Rollback error:", error);
+ });
+ };
+
+ return (
+
+ Rollback to target version
+
+ }
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/deployments-list.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/deployments-list.tsx
index a0217b114c..70b667c491 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/deployments-list.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/deployments/components/table/deployments-list.tsx
@@ -116,7 +116,7 @@ export const DeploymentsList = () => {
render: ({ deployment }) => (
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/filter-button.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/filter-button.tsx
index 451265b42e..41f27202cb 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/filter-button.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/filter-button.tsx
@@ -25,7 +25,7 @@ export const FilterButton = ({
)}
onClick={onClick}
>
-
+
{label}
{count}
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/index.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/index.tsx
index b35a541d68..d6103c21a2 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/index.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/details/active-deployment-card/index.tsx
@@ -167,7 +167,11 @@ export const ActiveDeploymentCard = ({ deploymentId }: Props) => {
-
+
setExpanded(!isExpanded)}
+ type="button"
+ >
{showBuildSteps ? "Build logs" : "Gateway logs"}
@@ -179,7 +183,7 @@ export const ActiveDeploymentCard = ({ deploymentId }: Props) => {
)}
/>
-
+
{/* Expandable Logs Section */}
@@ -204,14 +208,14 @@ export const ActiveDeploymentCard = ({ deploymentId }: Props) => {
count={formatNumber(logCounts.errors)}
onClick={() => handleFilterChange("errors")}
icon={CircleXMark}
- label="Errors"
+ label="5XX"
/>
handleFilterChange("warnings")}
icon={TriangleWarning2}
- label="Warnings"
+ label="4XX"
/>
+
{
[liveDeploymentId],
);
- const [newDeployment, oldDeployment] = query.data ?? [];
+ const newDeployment = query.data?.find((d) => d.id !== liveDeploymentId);
const diff = trpc.deploy.deployment.getOpenApiDiff.useQuery({
newDeploymentId: newDeployment?.id ?? "",
- oldDeploymentId: oldDeployment?.id ?? "",
+ oldDeploymentId: liveDeploymentId ?? "",
});
// @ts-expect-error I have no idea why this whines about type diff
const status = getDiffStatus(diff.data);
- if (newDeployment && !oldDeployment) {
+ if (newDeployment && !liveDeploymentId) {
return (
@@ -71,23 +71,25 @@ export const OpenApiDiff = () => {
return null;
}
- const diffUrl = `/${params?.workspaceSlug}/projects/${params?.projectId}/openapi-diff?from=${oldDeployment.id}&to=${newDeployment.id}`;
+ const diffUrl = `/${params?.workspaceSlug}/projects/${params?.projectId}/openapi-diff?from=${liveDeploymentId}&to=${newDeployment.id}`;
return (
-
+
from
-
{shortenId(oldDeployment.id)}
+
+ {shortenId(liveDeploymentId ?? "")}
+
-
+
to
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/layout.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/layout.tsx
index 17cc6f6a73..9a1402381d 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/layout.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/layout.tsx
@@ -35,14 +35,17 @@ const ProjectLayout = ({ projectId, children }: ProjectLayoutProps) => {
);
const liveDeploymentId = projects.data.at(0)?.liveDeploymentId;
+ const lastestDeploymentId = projects.data.at(0)?.latestDeploymentId;
- // biome-ignore lint/correctness/useExhaustiveDependencies: We just wanna refetch domains as soon as liveDeploymentId changes.
+ // We just wanna refetch domains as soon as lastestCommitTimestamp changes.
+ // We could use the liveDeploymentId for that but when user make `env=preview` this doesn't refetch properly.
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Read above.
useEffect(() => {
//@ts-expect-error Without this we can't refetch domains on-demand. It's either this or we do `refetchInternal` on domains collection level.
// Second approach causing too any re-renders. This is fine because data is partitioned and centralized in this context.
// Until they introduce a way to invalidate collections properly we stick to this.
collections.domains.utils.refetch();
- }, [liveDeploymentId]);
+ }, [lastestDeploymentId]);
const getTooltipContent = () => {
if (!liveDeploymentId) {
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/client.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/client.tsx
index 2da39a49fb..b28cd84eb5 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/client.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/client.tsx
@@ -112,14 +112,27 @@ export const DiffViewerContent: React.FC
= ({
if (!changelog || changelog.length === 0) {
return (
-
-
- No differences between {fromDeployment} and {toDeployment}
-
+
+
+
+
No noteworthy changes
+
+ The specifications for {fromDeployment}
+ and {toDeployment}
+ are functionally identical.
+
+
);
}
-
return (
<>
{/* Stats header */}
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/deployment-select.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/deployment-select.tsx
index 86b5e5fe77..6e51f84e29 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/deployment-select.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/components/deployment-select.tsx
@@ -37,6 +37,40 @@ export function DeploymentSelect({
disabledDeploymentId,
}: DeploymentSelectProps) {
const { liveDeploymentId } = useProject();
+ const latestDeploymentId = deployments.find(
+ ({ deployment }) => deployment.id !== liveDeploymentId,
+ )?.deployment.id;
+
+ const getTooltipContent = (
+ deploymentId: string,
+ isDisabled: boolean,
+ isLive: boolean,
+ isLatest: boolean,
+ ): string | undefined => {
+ if (isDisabled) {
+ return deploymentId === disabledDeploymentId
+ ? "Already selected for comparison"
+ : "No OpenAPI spec available";
+ }
+ if (isLive) {
+ return "Live deployment";
+ }
+ if (isLatest) {
+ return "Latest preview deployment";
+ }
+ return undefined;
+ };
+
+ const getTriggerTitle = (): string => {
+ if (value === liveDeploymentId) {
+ return "Live deployment";
+ }
+ if (value === latestDeploymentId) {
+ return "Latest preview deployment";
+ }
+ return "";
+ };
+
const renderOptions = () => {
if (isLoading) {
return (
@@ -52,24 +86,18 @@ export function DeploymentSelect({
);
}
-
return deployments.map(({ deployment }) => {
const isDisabled = deployment.id === disabledDeploymentId || !deployment.hasOpenApiSpec;
const deployedAt = format(deployment.createdAt, "MMM d, h:mm a");
+ const isLatest = deployment.id === latestDeploymentId;
+ const isLive = deployment.id === liveDeploymentId;
+ const tooltipContent = getTooltipContent(deployment.id, isDisabled, isLive, isLatest);
return (
{shortenId(deployment.id)}
•
{deployedAt}
- {deployment.id === liveDeploymentId && }
+ {isLive && }
+ {isLatest && !isLive && (
+
+ )}
@@ -94,11 +128,7 @@ export function DeploymentSelect({
onValueChange={onValueChange}
disabled={disabled || isLoading || deployments.length === 0}
>
-
+
{renderOptions()}
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/page.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/page.tsx
index d8a169b980..e7e1e61e3e 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/page.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/openapi-diff/page.tsx
@@ -1,5 +1,6 @@
"use client";
+import { shortenId } from "@/lib/shorten-id";
import { trpc } from "@/lib/trpc/client";
import { eq, useLiveQuery } from "@tanstack/react-db";
import { ArrowRight, Magnifier } from "@unkey/icons";
@@ -58,7 +59,7 @@ export default function DiffPage() {
if (liveDeploymentId) {
const exists = sortedDeployments.some((d) => d.deployment.id === liveDeploymentId);
if (exists) {
- setSelectedToDeployment(liveDeploymentId);
+ setSelectedFromDeployment(liveDeploymentId);
}
}
}, [liveDeploymentId, sortedDeployments, deployments.isLoading, searchParams]);
@@ -84,12 +85,7 @@ export default function DiffPage() {
return deploymentId;
}
- const commitSha =
- deployment.deployment.gitCommitSha?.substring(0, 7) ||
- deployment.deployment.id.substring(0, 7);
- const branch = deployment.deployment.gitBranch || "unknown";
-
- return `${branch}:${commitSha}`;
+ return shortenId(deploymentId);
},
[sortedDeployments],
);
@@ -175,7 +171,7 @@ export default function DiffPage() {
{showContent && (
<>
{diffLoading && (
-
+
Analyzing changes...
Comparing API specifications
@@ -183,11 +179,26 @@ export default function DiffPage() {
)}
{diffError && (
-
-
- Failed to generate diff
+
+
+
+
+
+ Unable to compare deployments
+
+
+ {diffError.message}
+
+
-
{diffError.message}
)}
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/create-project/create-project-dialog.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/create-project/create-project-dialog.tsx
index 949976e6cc..74b0e2c423 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/create-project/create-project-dialog.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/create-project/create-project-dialog.tsx
@@ -41,8 +41,8 @@ export const CreateProjectDialog = () => {
gitRepositoryUrl: values.gitRepositoryUrl || null,
liveDeploymentId: null,
isRolledBack: false,
- updatedAt: null,
id: "will-be-replace-by-server",
+ latestDeploymentId: null,
author: "will-be-replace-by-server",
authorAvatar: "will-be-replace-by-server",
branch: "will-be-replace-by-server",
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/index.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/index.tsx
index 5d72463ba3..eef3e237cd 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/index.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/index.tsx
@@ -1,4 +1,5 @@
-import { collection } from "@/lib/collections";
+import { ProximityPrefetch } from "@/components/proximity-prefetch";
+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";
@@ -76,31 +77,39 @@ export const ProjectsList = () => {
}}
>
{projects.data.map((project) => (
-
-
-
-
-
- }
- />
+ onEnterProximity={() => {
+ collectionManager.preloadProject(project.id);
+ }}
+ >
+
+
+
+
+
+ }
+ />
+
))}
diff --git a/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/project-actions.tsx b/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/project-actions.tsx
index d9f02d5559..40d2a0be61 100644
--- a/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/project-actions.tsx
+++ b/apps/dashboard/app/(app)/[workspaceSlug]/projects/_components/list/project-actions.tsx
@@ -1,6 +1,7 @@
"use client";
import { type MenuItem, TableActionPopover } from "@/components/logs/table-action.popover";
+import { useWorkspace } from "@/providers/workspace-provider";
import { Clone, Gear, Layers3, Trash } from "@unkey/icons";
import { toast } from "@unkey/ui";
@@ -14,12 +15,18 @@ type ProjectActionsProps = {
export const ProjectActions = ({ projectId, children }: PropsWithChildren
) => {
const router = useRouter();
- const menuItems = getProjectActionItems(projectId, router);
+ const { workspace } = useWorkspace();
+ // biome-ignore lint/style/noNonNullAssertion: This cannot be null
+ const menuItems = getProjectActionItems(projectId, workspace?.slug!, router);
return {children} ;
};
-const getProjectActionItems = (projectId: string, router: AppRouterInstance): MenuItem[] => {
+const getProjectActionItems = (
+ projectId: string,
+ workspaceSlug: string,
+ router: AppRouterInstance,
+): MenuItem[] => {
return [
{
id: "favorite-project",
@@ -48,12 +55,10 @@ const getProjectActionItems = (projectId: string, router: AppRouterInstance): Me
},
{
id: "view-log",
- label: "View logs",
+ label: "View gateway logs",
icon: ,
onClick: () => {
- //INFO: This will change soon
- const fakeDeploymentId = "idk";
- router.push(`/projects/${projectId}/deployments/${fakeDeploymentId}/logs`);
+ router.push(`/${workspaceSlug}/projects/${projectId}/gateway-logs`);
},
},
{
diff --git a/apps/dashboard/components/logs/live-switch-button/index.tsx b/apps/dashboard/components/logs/live-switch-button/index.tsx
index fa67e40254..c834c9a2df 100644
--- a/apps/dashboard/components/logs/live-switch-button/index.tsx
+++ b/apps/dashboard/components/logs/live-switch-button/index.tsx
@@ -9,16 +9,15 @@ type LiveSwitchProps = {
};
export const LiveSwitchButton = ({ isLive, onToggle }: LiveSwitchProps) => {
- useKeyboardShortcut("option+shift+l", onToggle);
-
+ useKeyboardShortcut("option+shift+q", onToggle);
return (
{
)}
Live
-
+
+
+
);
};
diff --git a/apps/dashboard/components/proximity-prefetch.tsx b/apps/dashboard/components/proximity-prefetch.tsx
new file mode 100644
index 0000000000..4419db468e
--- /dev/null
+++ b/apps/dashboard/components/proximity-prefetch.tsx
@@ -0,0 +1,123 @@
+"use client";
+import { useRouter } from "next/navigation";
+import type { ReactNode } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
+
+type ProximityPrefetchProps = {
+ /** The content to wrap and monitor for proximity */
+ children: ReactNode;
+ /** The route path to prefetch when mouse enters proximity. Optional if only using onEnterProximity */
+ route?: string;
+ /** Distance in pixels from element center to trigger prefetch. Default: 500px */
+ distance?: number;
+ /** Debounce delay in milliseconds for proximity checks. Default: 100ms */
+ debounceDelay?: number;
+ /** Callback fired once when mouse enters proximity threshold */
+ onEnterProximity?: () => void | Promise;
+};
+
+/**
+ * Wraps children and prefetches a Next.js route when the user's mouse enters a defined proximity.
+ *
+ * Triggers once per route/element and resets when the route changes.
+ * Useful for preloading data or routes before user interaction.
+ *
+ * @example
+ * // Basic route prefetch
+ *
+ *
+ *
+ *
+ * @example
+ * // Custom callback for data preloading
+ * {
+ * await fetch('/api/projects/123/metrics');
+ * }}
+ * >
+ *
+ *
+ *
+ * @example
+ * // Adjust sensitivity
+ *
+ *
+ *
+ */
+export function ProximityPrefetch({
+ children,
+ route,
+ distance = 500,
+ debounceDelay = 100,
+ onEnterProximity,
+}: ProximityPrefetchProps) {
+ const router = useRouter();
+ const containerRef = useRef(null);
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+ const hasTriggered = useRef(false);
+ const debounceTimeout = useRef();
+
+ const checkProximity = useCallback(() => {
+ // Skip if element doesn't exist or already triggered
+ if (!containerRef.current || hasTriggered.current) {
+ return;
+ }
+
+ // Calculate distance from mouse to element center
+ const rect = containerRef.current.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+ const distanceFromCenter = Math.sqrt(
+ (mousePosition.x - centerX) ** 2 + (mousePosition.y - centerY) ** 2,
+ );
+
+ // Trigger prefetch and callback when mouse enters proximity threshold
+ if (distanceFromCenter < distance) {
+ hasTriggered.current = true;
+
+ if (route) {
+ router.prefetch(route);
+ }
+
+ if (onEnterProximity) {
+ Promise.resolve(onEnterProximity()).catch((error) => {
+ console.error("ProximityPrefetch callback error:", error);
+ });
+ }
+ }
+ }, [mousePosition, distance, route, router, onEnterProximity]);
+
+ // Tracks the mouse position
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ setMousePosition({ x: e.clientX, y: e.clientY });
+ };
+
+ window.addEventListener("mousemove", handleMouseMove, { passive: true });
+ return () => window.removeEventListener("mousemove", handleMouseMove);
+ }, []);
+
+ useEffect(() => {
+ // Skip checking proximity until mouse has moved from initial (0,0) position
+ if (mousePosition.x === 0 && mousePosition.y === 0) {
+ return;
+ }
+
+ // Debounce proximity checks to avoid excessive calculations on every mouse move
+ clearTimeout(debounceTimeout.current);
+ debounceTimeout.current = setTimeout(checkProximity, debounceDelay);
+
+ return () => clearTimeout(debounceTimeout.current);
+ }, [mousePosition, checkProximity, debounceDelay]);
+
+ useEffect(() => {
+ hasTriggered.current = false;
+ }, []);
+
+ return {children}
;
+}
diff --git a/apps/dashboard/components/virtual-table/index.tsx b/apps/dashboard/components/virtual-table/index.tsx
index 9ad586b6ea..2695927400 100644
--- a/apps/dashboard/components/virtual-table/index.tsx
+++ b/apps/dashboard/components/virtual-table/index.tsx
@@ -390,13 +390,16 @@ export const VirtualTable = forwardRef>(
/>
-
+ {/* Without this check bottom status section blinks in the UI and disappears */}
+ {loadMoreFooterProps && (
+
+ )}
);
diff --git a/apps/dashboard/lib/collections/deploy/projects.ts b/apps/dashboard/lib/collections/deploy/projects.ts
index fb34785e5f..875fd8bd1d 100644
--- a/apps/dashboard/lib/collections/deploy/projects.ts
+++ b/apps/dashboard/lib/collections/deploy/projects.ts
@@ -9,7 +9,7 @@ const schema = z.object({
name: z.string(),
slug: z.string(),
gitRepositoryUrl: z.string().nullable(),
- updatedAt: z.number().int().nullable(),
+ latestDeploymentId: z.string().nullable(),
liveDeploymentId: z.string().nullable(),
isRolledBack: z.boolean(),
// Flattened deployment fields for UI
diff --git a/apps/dashboard/lib/collections/index.ts b/apps/dashboard/lib/collections/index.ts
index 52b53d2e00..fee52f0525 100644
--- a/apps/dashboard/lib/collections/index.ts
+++ b/apps/dashboard/lib/collections/index.ts
@@ -38,15 +38,17 @@ class CollectionManager {
return this.projectCollections.get(projectId)!;
}
+ async preloadProject(projectId: string): Promise {
+ const collections = this.getProjectCollections(projectId);
+ // Preload all collections in the object
+ await Promise.all(Object.values(collections).map((collection) => collection.preload()));
+ }
+
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
- ]);
+ // Cleanup all collections in the object
+ await Promise.all(Object.values(collections).map((collection) => collection.cleanup()));
this.projectCollections.delete(projectId);
}
}
@@ -56,10 +58,8 @@ class CollectionManager {
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]);
}
}
diff --git a/apps/dashboard/lib/trpc/routers/deploy/project/list.ts b/apps/dashboard/lib/trpc/routers/deploy/project/list.ts
index 59ea4c6f7c..1dfa42d8d0 100644
--- a/apps/dashboard/lib/trpc/routers/deploy/project/list.ts
+++ b/apps/dashboard/lib/trpc/routers/deploy/project/list.ts
@@ -8,7 +8,6 @@ type ProjectRow = {
id: string;
name: string;
slug: string;
- updated_at: number | null;
git_repository_url: string | null;
live_deployment_id: string | null;
is_rolled_back: boolean;
@@ -19,6 +18,7 @@ type ProjectRow = {
git_commit_timestamp: number | null;
runtime_config: Deployment["runtimeConfig"] | null;
domain: string | null;
+ latest_deployment_id: string | null;
};
export const listProjects = t.procedure
@@ -41,7 +41,15 @@ export const listProjects = t.procedure
${deployments.gitCommitAuthorAvatarUrl},
${deployments.gitCommitTimestamp},
${deployments.runtimeConfig},
- ${domains.domain}
+ ${domains.domain},
+ (
+ SELECT id
+ FROM ${deployments} d
+ WHERE d.project_id = ${projects.id}
+ AND d.workspace_id = ${ctx.workspace.id}
+ ORDER BY d.created_at DESC
+ LIMIT 1
+ ) as latest_deployment_id
FROM ${projects}
LEFT JOIN ${deployments}
ON ${projects.liveDeploymentId} = ${deployments.id}
@@ -58,7 +66,6 @@ export const listProjects = t.procedure
id: row.id,
name: row.name,
slug: row.slug,
- updatedAt: row.updated_at,
gitRepositoryUrl: row.git_repository_url,
liveDeploymentId: row.live_deployment_id,
isRolledBack: row.is_rolled_back,
@@ -69,6 +76,7 @@ export const listProjects = t.procedure
authorAvatar: row.git_commit_author_avatar_url,
regions: row.runtime_config?.regions?.map((r) => r.region) ?? ["us-east-1"],
domain: row.domain,
+ latestDeploymentId: row.latest_deployment_id,
}),
);
});
diff --git a/go/cmd/deploy/control_plane.go b/go/cmd/deploy/control_plane.go
index 3dead4a8bc..e1403dade6 100644
--- a/go/cmd/deploy/control_plane.go
+++ b/go/cmd/deploy/control_plane.go
@@ -113,7 +113,6 @@ func (c *ControlPlaneClient) PollDeploymentStatus(
logger logging.Logger,
deploymentID string,
onStatusChange func(DeploymentStatusEvent) error,
- onStepUpdate func(DeploymentStepEvent) error,
) error {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
@@ -121,7 +120,6 @@ func (c *ControlPlaneClient) PollDeploymentStatus(
defer timeout.Stop()
// Track processed steps by creation time to avoid duplicates
- processedSteps := make(map[int64]bool)
lastStatus := ctrlv1.DeploymentStatus_DEPLOYMENT_STATUS_UNSPECIFIED
for {
@@ -154,11 +152,6 @@ func (c *ControlPlaneClient) PollDeploymentStatus(
lastStatus = currentStatus
}
- // Process new step updates
- if err := c.processNewSteps(deploymentID, deployment.GetSteps(), processedSteps, currentStatus, onStepUpdate); err != nil {
- return err
- }
-
// Check for completion
if currentStatus == ctrlv1.DeploymentStatus_DEPLOYMENT_STATUS_READY {
return nil
@@ -167,51 +160,6 @@ func (c *ControlPlaneClient) PollDeploymentStatus(
}
}
-// processNewSteps processes new deployment steps and calls the event handler
-func (c *ControlPlaneClient) processNewSteps(
- deploymentID string,
- steps []*ctrlv1.DeploymentStep,
- processedSteps map[int64]bool,
- currentStatus ctrlv1.DeploymentStatus,
- onStepUpdate func(DeploymentStepEvent) error,
-) error {
- for _, step := range steps {
- // Creation timestamp as unique identifier
- stepTimestamp := step.GetCreatedAt()
-
- if processedSteps[stepTimestamp] {
- continue // Already processed this step
- }
-
- // Handle step errors first
- if step.GetErrorMessage() != "" {
- return fmt.Errorf("deployment failed: %s", step.GetErrorMessage())
- }
-
- // Call step update handler
- if step.GetMessage() != "" {
- event := DeploymentStepEvent{
- DeploymentID: deploymentID,
- Step: step,
- Status: currentStatus,
- }
- if err := onStepUpdate(event); err != nil {
- return err
- }
-
- // INFO: This is for demo purposes only.
- // Adding a small delay between deployment steps to make the progression
- // visually observable during demos. This allows viewers to see each
- // individual step (VM boot, rootfs loading, etc.) rather than having
- // everything complete too quickly to follow.
- time.Sleep(800 * time.Millisecond)
- }
- // Mark this step as processed
- processedSteps[stepTimestamp] = true
- }
- return nil
-}
-
// getFailureMessage extracts failure message from version
func (c *ControlPlaneClient) getFailureMessage(deployment *ctrlv1.Deployment) string {
if deployment.GetErrorMessage() != "" {
diff --git a/go/cmd/deploy/main.go b/go/cmd/deploy/main.go
index 2fc1bfa05f..cd919be2b8 100644
--- a/go/cmd/deploy/main.go
+++ b/go/cmd/deploy/main.go
@@ -9,6 +9,8 @@ import (
"github.com/unkeyed/unkey/go/pkg/cli"
"github.com/unkeyed/unkey/go/pkg/git"
"github.com/unkeyed/unkey/go/pkg/otel/logging"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
)
const (
@@ -274,13 +276,13 @@ func executeDeploy(ctx context.Context, opts DeployOptions) error {
// Create deployment
ui.Print(MsgCreatingDeployment)
controlPlane := NewControlPlaneClient(opts)
- deploymentId, err := controlPlane.CreateDeployment(ctx, dockerImage)
+ deploymentID, err := controlPlane.CreateDeployment(ctx, dockerImage)
if err != nil {
ui.PrintError(MsgFailedToCreateDeployment)
ui.PrintErrorDetails(err.Error())
return err
}
- ui.PrintSuccess(fmt.Sprintf("Deployment created: %s", deploymentId))
+ ui.PrintSuccess(fmt.Sprintf("Deployment created: %s", deploymentID))
// Track final deployment for completion info
var finalDeployment *ctrlv1.Deployment
@@ -297,13 +299,8 @@ func executeDeploy(ctx context.Context, opts DeployOptions) error {
return nil
}
- // Handle deployment step updates
- onStepUpdate := func(event DeploymentStepEvent) error {
- return handleStepUpdate(event, ui)
- }
-
// Poll for deployment completion
- err = controlPlane.PollDeploymentStatus(ctx, logger, deploymentId, onStatusChange, onStepUpdate)
+ err = controlPlane.PollDeploymentStatus(ctx, logger, deploymentID, onStatusChange)
if err != nil {
ui.CompleteCurrentStep(MsgDeploymentFailed, false)
return err
@@ -314,7 +311,7 @@ func executeDeploy(ctx context.Context, opts DeployOptions) error {
ui.CompleteCurrentStep(MsgDeploymentStepCompleted, true)
ui.PrintSuccess(MsgDeploymentCompleted)
fmt.Printf("\n")
- printCompletionInfo(finalDeployment)
+ printCompletionInfo(finalDeployment, opts.Environment)
fmt.Printf("\n")
}
@@ -331,32 +328,6 @@ func getNextStepMessage(currentMessage string) string {
return ""
}
-func handleStepUpdate(event DeploymentStepEvent, ui *UI) error {
- step := event.Step
-
- if step.GetErrorMessage() != "" {
- ui.CompleteCurrentStep(step.GetMessage(), false)
- ui.PrintErrorDetails(step.GetErrorMessage())
- return fmt.Errorf("deployment failed: %s", step.GetErrorMessage())
- }
-
- if step.GetMessage() != "" {
- message := step.GetMessage()
- nextStep := getNextStepMessage(message)
-
- if !ui.stepSpinning {
- // First step - start spinner, then complete and start next
- ui.StartStepSpinner(message)
- ui.CompleteStepAndStartNext(message, nextStep)
- } else {
- // Complete current step and start next
- ui.CompleteStepAndStartNext(message, nextStep)
- }
- }
-
- return nil
-}
-
func handleDeploymentFailure(controlPlane *ControlPlaneClient, deployment *ctrlv1.Deployment, ui *UI) error {
errorMsg := controlPlane.getFailureMessage(deployment)
ui.CompleteCurrentStep(MsgDeploymentFailed, false)
@@ -386,17 +357,19 @@ func printSourceInfo(opts DeployOptions, gitInfo git.Info) {
fmt.Printf("\n")
}
-func printCompletionInfo(deployment *ctrlv1.Deployment) {
+func printCompletionInfo(deployment *ctrlv1.Deployment, env string) {
if deployment == nil || deployment.GetId() == "" {
fmt.Printf("✓ Deployment completed\n")
return
}
+ caser := cases.Title(language.English)
+
fmt.Println()
fmt.Println(CompletionTitle)
fmt.Printf(" %s: %s\n", CompletionDeploymentID, deployment.GetId())
fmt.Printf(" %s: %s\n", CompletionStatus, CompletionReady)
- fmt.Printf(" %s: %s\n", CompletionEnvironment, DefaultEnvironment)
+ fmt.Printf(" %s: %s\n", CompletionEnvironment, caser.String(env))
fmt.Println()
fmt.Println(CompletionDomains)
diff --git a/internal/ui/src/components/buttons/keyboard-button.tsx b/internal/ui/src/components/buttons/keyboard-button.tsx
index 96c9860577..ee8970d3e4 100644
--- a/internal/ui/src/components/buttons/keyboard-button.tsx
+++ b/internal/ui/src/components/buttons/keyboard-button.tsx
@@ -26,7 +26,7 @@ const KeyboardButton = ({
"focus:border-grayA-12 focus:ring-4 focus:ring-gray-6 focus-visible:outline-none focus:ring-offset-0 drop-shadow-button",
"disabled:border disabled:border-grayA-4 disabled:text-grayA-7",
"active:bg-grayA-5 max-md:hidden",
- { className },
+ className,
)}
aria-label={`Keyboard shortcut ${modifierKey || ""} ${shortcut}`}
role="presentation"
diff --git a/internal/ui/src/components/buttons/refresh-button.tsx b/internal/ui/src/components/buttons/refresh-button.tsx
index b8261d1b8c..57a1b00cc6 100644
--- a/internal/ui/src/components/buttons/refresh-button.tsx
+++ b/internal/ui/src/components/buttons/refresh-button.tsx
@@ -44,7 +44,7 @@ const RefreshButton = ({ onRefresh, isEnabled, isLive, toggleLive }: RefreshButt
setRefreshTimeout(timeout);
};
- useKeyboardShortcut("option+shift+r", handleRefresh, {
+ useKeyboardShortcut("option+shift+w", handleRefresh, {
preventDefault: true,
disabled: !isEnabled,
});
@@ -62,14 +62,16 @@ const RefreshButton = ({ onRefresh, isEnabled, isLive, toggleLive }: RefreshButt
onClick={handleRefresh}
variant="ghost"
size="md"
- title={isEnabled ? "Refresh data (Shortcut: ⌥+⇧+R)" : ""}
+ title={isEnabled ? "Refresh data (Shortcut: ⌥+⇧+W)" : ""}
disabled={!isEnabled || isLoading}
loading={isLoading}
- className="flex w-full items-center justify-center rounded-lg border border-gray-4"
+ className="flex w-full items-center justify-center rounded-lg border border-gray-4 group overflow-hidden"
>
Refresh
-
+
+
+
@@ -77,4 +79,5 @@ const RefreshButton = ({ onRefresh, isEnabled, isLive, toggleLive }: RefreshButt
};
RefreshButton.displayName = "RefreshButton";
+
export { RefreshButton, type RefreshButtonProps };
diff --git a/internal/ui/src/components/logs/control-cloud/index.tsx b/internal/ui/src/components/logs/control-cloud/index.tsx
index fb3803b95a..b55fc25000 100644
--- a/internal/ui/src/components/logs/control-cloud/index.tsx
+++ b/internal/ui/src/components/logs/control-cloud/index.tsx
@@ -27,7 +27,7 @@ export const ControlCloud =
({
}: ControlCloudProps) => {
const [focusedIndex, setFocusedIndex] = useState(null);
- useKeyboardShortcut("option+shift+d", () => {
+ useKeyboardShortcut("option+shift+a", () => {
const timestamp = Date.now();
updateFilters([
{
@@ -45,7 +45,7 @@ export const ControlCloud = ({
] as TFilter[]);
});
- useKeyboardShortcut("option+shift+c", () => {
+ useKeyboardShortcut("option+shift+s", () => {
setFocusedIndex(0);
});
@@ -130,7 +130,7 @@ export const ControlCloud = ({
return (
{filters.map((filter, index) => (
@@ -146,11 +146,19 @@ export const ControlCloud =
({
/>
))}
-
Clear filters
-
-
-
Focus filters
-
+
+
+
);