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) => ( -
-
-

{title}

- -
- -
-); - -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) => ( -
-
-

Domain

- -
-
-
- -
{domain.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) => ( -
-
-

{title}

- -
- -
-); - -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) => ( -
-
-

Domain

- -
-
-
- -
{domain.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) => ( +
+
+

{title}

+ +
+ +
+); 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) => {
-
+ -
+
{/* 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 ( ); }; 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 - +
+ Clear filters +
+ +
+
+
+
+ Focus filters +
+ +
+
);