From 46c3abb5c0a514d6b34c645f1908a2fe1077b704 Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Mon, 25 Nov 2024 20:54:38 +0800 Subject: [PATCH] feat: Select Engine versions (#5479) --- .../src/@3rdweb-sdk/react/cache-keys.ts | 3 +- .../src/@3rdweb-sdk/react/hooks/useEngine.ts | 38 +++- .../overview/engine-instances-table.tsx | 2 +- .../_components/EnginePageLayout.tsx | 9 +- .../[engineId]/_components/version.tsx | 214 +++++++++++++----- .../overview/overview-page.client.tsx | 6 +- .../~/engine/(instance)/[engineId]/page.tsx | 2 +- 7 files changed, 199 insertions(+), 75 deletions(-) diff --git a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts index 1d2c0ca1f90..4d99b6174ac 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts @@ -111,7 +111,8 @@ export const engineKeys = { ] as const, health: (instance: string) => [...engineKeys.all, instance, "health"] as const, - latestVersion: () => [...engineKeys.all, "latestVersion"] as const, + deploymentPublicConfiguration: () => + [...engineKeys.all, "deploymentPublicConfiguration"] as const, systemMetrics: (engineId: string) => [...engineKeys.all, engineId, "systemMetrics"] as const, queueMetrics: (engineId: string) => diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts index 560f8359bed..8d161234531 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useEngine.ts @@ -191,32 +191,48 @@ export function useEngineQueueMetrics( }); } -export function useEngineLatestVersion() { - return useQuery({ - queryKey: engineKeys.latestVersion(), +interface GetDeploymentPublicConfigurationInput { + teamSlug: string; +} + +interface DeploymentPublicConfigurationResponse { + serverVersions: { + name: string; + createdAt: string; + }[]; +} + +export function useEngineGetDeploymentPublicConfiguration( + input: GetDeploymentPublicConfigurationInput, +) { + return useQuery({ + queryKey: engineKeys.deploymentPublicConfiguration(), queryFn: async () => { - const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/latest-version`, { - method: "GET", - }); + const res = await fetch( + `${THIRDWEB_API_HOST}/v1/teams/${input.teamSlug}/engine/deployments/public-configuration`, + { method: "GET" }, + ); if (!res.ok) { throw new Error(`Unexpected status ${res.status}: ${await res.text()}`); } + const json = await res.json(); - return json.data.version as string; + return json.data as DeploymentPublicConfigurationResponse; }, }); } -interface UpdateVersionInput { +interface UpdateDeploymentInput { + teamSlug: string; deploymentId: string; serverVersion: string; } -export function useEngineUpdateServerVersion() { +export function useEngineUpdateDeployment() { return useMutation({ - mutationFn: async (input: UpdateVersionInput) => { + mutationFn: async (input: UpdateDeploymentInput) => { const res = await fetch( - `${THIRDWEB_API_HOST}/v2/engine/deployments/${input.deploymentId}/infrastructure`, + `${THIRDWEB_API_HOST}/v1/teams/${input.teamSlug}/engine/deployments/${input.deploymentId}`, { method: "PUT", headers: { diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/overview/engine-instances-table.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/overview/engine-instances-table.tsx index 17bbce29d11..9a351051350 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/overview/engine-instances-table.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(general)/overview/engine-instances-table.tsx @@ -465,7 +465,7 @@ function DeleteSubscriptionModalContent(props: {
- + This action is irreversible! diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/EnginePageLayout.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/EnginePageLayout.tsx index 78cd63c5b49..a7bd43d2e6c 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/EnginePageLayout.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/EnginePageLayout.tsx @@ -119,6 +119,7 @@ export function WithEngineInstance(props: { instance={sandboxEngine} content={props.content} rootPath={rootPath} + teamSlug={props.teamSlug} /> ); } @@ -128,6 +129,7 @@ export function WithEngineInstance(props: { content={props.content} engineId={props.engineId} rootPath={rootPath} + teamSlug={props.teamSlug} /> ); } @@ -136,6 +138,7 @@ function QueryAndRenderInstanceHeader(props: { engineId: string; content: React.FC<{ instance: EngineInstance }>; rootPath: string; + teamSlug: string; }) { const instancesQuery = useEngineInstances(); const instance = instancesQuery.data?.find((x) => x.id === props.engineId); @@ -157,6 +160,7 @@ function QueryAndRenderInstanceHeader(props: { instance={instance} content={props.content} rootPath={props.rootPath} + teamSlug={props.teamSlug} /> ); } @@ -165,6 +169,7 @@ function EnsurePermissionAndRenderInstance(props: { content: React.FC<{ instance: EngineInstance }>; instance: EngineInstance; rootPath: string; + teamSlug: string; }) { const permissionQuery = useHasEnginePermission({ instanceUrl: props.instance.url, @@ -210,6 +215,7 @@ function EnsurePermissionAndRenderInstance(props: { rootPath={props.rootPath} instance={props.instance} content={props.content} + teamSlug={props.teamSlug} /> ); } @@ -218,6 +224,7 @@ function RenderEngineInstanceHeader(props: { instance: EngineInstance; content: React.FC<{ instance: EngineInstance }>; rootPath: string; + teamSlug: string; }) { const { instance } = props; @@ -261,7 +268,7 @@ function RenderEngineInstanceHeader(props: { )}
- +
diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/version.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/version.tsx index 390584f8623..641428a5cc5 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/version.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/_components/version.tsx @@ -1,15 +1,7 @@ -import { Button } from "@/components/ui/button"; -import { ToolTipLabel } from "@/components/ui/tooltip"; -import { - type EngineInstance, - useEngineLatestVersion, - useEngineSystemHealth, - useEngineUpdateServerVersion, -} from "@3rdweb-sdk/react/hooks/useEngine"; -import { CircleArrowDownIcon, CloudDownloadIcon } from "lucide-react"; -import { useState } from "react"; - import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, @@ -18,40 +10,68 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { ToolTipLabel } from "@/components/ui/tooltip"; import { TrackedLinkTW } from "@/components/ui/tracked-link"; +import { + type EngineInstance, + useEngineGetDeploymentPublicConfiguration, + useEngineSystemHealth, + useEngineUpdateDeployment, +} from "@3rdweb-sdk/react/hooks/useEngine"; +import { formatDistanceToNow } from "date-fns"; +import { CircleArrowUpIcon, TriangleAlertIcon } from "lucide-react"; +import { useState } from "react"; import { toast } from "sonner"; import invariant from "tiny-invariant"; export const EngineVersionBadge = ({ instance, + teamSlug, }: { instance: EngineInstance; + teamSlug: string; }) => { const healthQuery = useEngineSystemHealth(instance.url); - const latestVersionQuery = useEngineLatestVersion(); + const publicConfigurationQuery = useEngineGetDeploymentPublicConfiguration({ + teamSlug, + }); const [isModalOpen, setModalOpen] = useState(false); - const currentVersion = healthQuery.data?.engineVersion ?? "..."; - const latestVersion = latestVersionQuery.data; - const isStale = latestVersion && currentVersion !== latestVersion; + if (!healthQuery.data || !publicConfigurationQuery.data) { + return null; + } + + const serverVersions = publicConfigurationQuery.data.serverVersions; + const latestVersion = serverVersions[0]; + const currentVersion = healthQuery.data.engineVersion ?? "N/A"; + const hasNewerVersion = latestVersion?.name !== currentVersion; - if (!isStale) { + // Hide the change version modal unless owner. + if (!instance.deploymentId) { return ( - - - + ); } return ( <> + label={ + hasNewerVersion + ? "An update is available" + : "Engine is on the latest update" } + leftIcon={} > - {latestVersion && ( - - )} + ); }; -const UpdateVersionModal = (props: { +const ChangeVersionModal = (props: { open: boolean; onOpenChange: (open: boolean) => void; - latestVersion: string; instance: EngineInstance; + currentVersion: string; + serverVersions: { name: string; createdAt: string }[]; + teamSlug: string; }) => { - const { open, onOpenChange, latestVersion, instance } = props; - const updateEngineServerMutation = useEngineUpdateServerVersion(); + const { + open, + onOpenChange, + instance, + currentVersion, + serverVersions, + teamSlug, + } = props; + const [selectedVersion, setSelectedVersion] = useState( + serverVersions[0]?.name, + ); + const updateDeploymentMutation = useEngineUpdateDeployment(); if (!instance.deploymentId) { - // For self-hosted, show a prompt to the Github release page. + // Self-hosted modal: prompt to update manually. return ( - Update your self-hosted Engine to {latestVersion} + Update your self-hosted Engine View the{" "} @@ -119,17 +153,19 @@ const UpdateVersionModal = (props: { ); } - const onClick = async () => { + const onClickUpdate = async () => { + invariant(selectedVersion, "No version selected."); invariant(instance.deploymentId, "Engine is missing deploymentId."); try { - const promise = updateEngineServerMutation.mutateAsync({ + const promise = updateDeploymentMutation.mutateAsync({ + teamSlug, deploymentId: instance.deploymentId, - serverVersion: latestVersion, + serverVersion: selectedVersion, }); toast.promise(promise, { - success: `Upgrading your Engine to ${latestVersion}. Please confirm after a few minutes.`, - error: "Unexpected error updating your Engine.", + success: `Updating your Engine to ${selectedVersion}.`, + error: "Unexpected error updating Engine.", }); await promise; } finally { @@ -137,6 +173,7 @@ const UpdateVersionModal = (props: { } }; + // For cloud-hosted, prompt the user to select a version to update to. return ( - Update Engine to {latestVersion}? - - - It is recommended to pause traffic to Engine before performing this - upgrade. There is < 1 minute of expected downtime. - + Update Engine version + + + +
+ + {currentVersion.startsWith("v") && ( +
+ + View changes: {currentVersion} → {selectedVersion} + +
+ )} + +
+ + + + There may be up to 1 minute of downtime. + + + We recommended pausing traffic to Engine before performing this + version update. + + + + diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/overview-page.client.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/overview-page.client.tsx index 565c1a78d9d..7b327d21c27 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/overview-page.client.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/overview-page.client.tsx @@ -5,15 +5,15 @@ import { EngineOverview } from "./components/engine-overview"; export function EngineOverviewPage(props: { engineId: string; - team_slug: string; + teamSlug: string; }) { return ( ( - + )} - teamSlug={props.team_slug} + teamSlug={props.teamSlug} /> ); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/page.tsx index f6947e239bd..a55e89332d0 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/page.tsx @@ -6,7 +6,7 @@ export default async function Page(props: EngineInstancePageProps) { return ( ); }