feat: move deployments into an overview page tab

This commit is contained in:
Narciso
2026-08-13 18:30:45 -04:00
parent 046a608f8c
commit 518ee44ea6
7 changed files with 114 additions and 122 deletions
@@ -223,7 +223,7 @@ export const ShowHome = () => {
</div>
{canReadDeployments && (
<Link
href="/dashboard/deployments"
href="/dashboard/overview?tab=deployments"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
view all →
@@ -0,0 +1,77 @@
import { Rocket } from "lucide-react";
import { useRouter } from "next/router";
import { ShowDeploymentsTable } from "@/components/dashboard/deployments/show-deployments-table";
import { ShowQueueTable } from "@/components/dashboard/deployments/show-queue-table";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const SUBTAB_VALUES = ["deployments", "queue"] as const;
type SubtabValue = (typeof SUBTAB_VALUES)[number];
const DEFAULT_SUBTAB: SubtabValue = "deployments";
function isValidSubtab(t: string): t is SubtabValue {
return SUBTAB_VALUES.includes(t as SubtabValue);
}
export const ShowOverviewDeployments = () => {
const router = useRouter();
const subtab =
typeof router.query.subtab === "string" &&
isValidSubtab(router.query.subtab)
? router.query.subtab
: DEFAULT_SUBTAB;
const setSubtab = (value: string) => {
if (!isValidSubtab(value)) return;
const { subtab: _current, ...query } = router.query;
router.replace(
{
pathname: router.pathname,
query: value === DEFAULT_SUBTAB ? query : { ...query, subtab: value },
},
undefined,
{ shallow: true },
);
};
return (
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full">
<CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2">
<Rocket className="size-5" />
Deployments
</CardTitle>
<CardDescription>
All application and compose deployments in one place.
</CardDescription>
</div>
</div>
<Tabs
value={subtab}
onValueChange={setSubtab}
className="w-full min-w-0"
>
<TabsList className="mt-2">
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="queue">Queue</TabsTrigger>
</TabsList>
<TabsContent value="deployments" className="mt-0 min-w-0 pt-4">
<ShowDeploymentsTable />
</TabsContent>
<TabsContent value="queue" className="mt-0 pt-4">
<ShowQueueTable />
</TabsContent>
</Tabs>
</CardHeader>
</div>
</Card>
);
};
@@ -4,6 +4,7 @@ import { ExternalLink, Loader2 } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { COMPOSE_REDEPLOY_TOAST } from "@/components/dashboard/application/domains/redeploy-hint";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -71,7 +72,12 @@ export const ShowOverviewDomains = () => {
try {
const result = await toggleEnable({ domainId: domain.domainId });
utils.overview.domains.invalidate();
toast.success(result.enabled ? "Domain enabled" : "Domain disabled");
toast.success(
result.enabled ? "Domain enabled" : "Domain disabled",
result.requiresRedeploy
? { description: COMPOSE_REDEPLOY_TOAST }
: undefined,
);
} catch {
toast.error(`Error updating "${domain.host}"`);
}
@@ -175,7 +175,7 @@ export const SearchCommand = () => {
</CommandItem>
<CommandItem
onSelect={() => {
router.push("/dashboard/deployments");
router.push("/dashboard/overview?tab=deployments");
setOpen(false);
}}
>
+5 -13
View File
@@ -29,7 +29,6 @@ import {
type LucideIcon,
Package,
Palette,
Rocket,
Server,
ShieldCheck,
Smartphone,
@@ -179,10 +178,11 @@ const MENU: Menu = {
},
{
isSingle: true,
title: "Deployments",
url: "/dashboard/deployments",
icon: Rocket,
isEnabled: ({ permissions }) => !!permissions?.deployment.read,
title: "Overview",
url: "/dashboard/overview",
icon: LayoutGrid,
// Only enabled for users with access to services
isEnabled: ({ permissions }) => !!permissions?.service.read,
},
{
isSingle: true,
@@ -216,14 +216,6 @@ const MENU: Menu = {
// Only enabled for users with access to Docker
isEnabled: ({ permissions }) => !!permissions?.docker.read,
},
{
isSingle: true,
title: "Overview",
url: "/dashboard/overview",
icon: LayoutGrid,
// Only enabled for users with access to services
isEnabled: ({ permissions }) => !!permissions?.service.read,
},
{
isSingle: true,
title: "Requests",
+10 -104
View File
@@ -1,113 +1,19 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { hasPermission } from "@dokploy/server/services/permission";
import { Rocket } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import type { ReactElement } from "react";
import { ShowDeploymentsTable } from "@/components/dashboard/deployments/show-deployments-table";
import { ShowQueueTable } from "@/components/dashboard/deployments/show-queue-table";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const TAB_VALUES = ["deployments", "queue"] as const;
type TabValue = (typeof TAB_VALUES)[number];
function isValidTab(t: string): t is TabValue {
return TAB_VALUES.includes(t as TabValue);
export default function DeploymentsRedirect() {
return null;
}
function DeploymentsPage() {
const router = useRouter();
const tab =
router.query.tab && isValidTab(router.query.tab as string)
? (router.query.tab as TabValue)
: "deployments";
const setTab = (value: string) => {
if (!isValidTab(value)) return;
router.replace(
{ pathname: "/dashboard/deployments", query: { tab: value } },
undefined,
{ shallow: true },
);
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full">
<CardHeader>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2">
<Rocket className="size-5" />
Deployments
</CardTitle>
<CardDescription>
All application and compose deployments in one place.
</CardDescription>
</div>
</div>
<Tabs value={tab} onValueChange={setTab} className="w-full min-w-0">
<TabsList className="mt-2">
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="queue">Queue</TabsTrigger>
</TabsList>
<TabsContent value="deployments" className="mt-0 min-w-0 pt-4">
<ShowDeploymentsTable />
</TabsContent>
<TabsContent value="queue" className="mt-0 pt-4">
<ShowQueueTable />
</TabsContent>
</Tabs>
</CardHeader>
</div>
</Card>
</div>
);
}
export default DeploymentsPage;
DeploymentsPage.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { user, session } = await validateRequest(ctx.req);
if (!user) {
return {
redirect: {
permanent: false,
destination: "/",
},
};
}
const canView = await hasPermission(
{
user: { id: user.id },
session: { activeOrganizationId: session?.activeOrganizationId || "" },
},
{ deployment: ["read"] },
);
if (!canView) {
return {
redirect: {
permanent: false,
destination: "/dashboard/home",
},
};
}
const destination =
ctx.query.tab === "queue"
? "/dashboard/overview?tab=deployments&subtab=queue"
: "/dashboard/overview?tab=deployments";
return {
props: {},
redirect: {
permanent: false,
destination,
},
};
}
+13 -2
View File
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowOverviewBackups } from "@/components/dashboard/overview/show-overview-backups";
import { ShowOverviewDeployments } from "@/components/dashboard/overview/show-overview-deployments";
import { ShowOverviewDomains } from "@/components/dashboard/overview/show-overview-domains";
import { ShowOverviewServices } from "@/components/dashboard/overview/show-overview-services";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
@@ -20,17 +21,19 @@ const Overview = () => {
const canSeeBackups =
!!permissions?.backup.read && !!permissions?.volumeBackup.read;
const canSeeDomains = !!permissions?.domain.read;
const canSeeDeployments = !!permissions?.deployment.read;
const queryTab =
typeof router.query.tab === "string" ? router.query.tab : DEFAULT_TAB;
const activeTab =
(queryTab === "backups" && !canSeeBackups) ||
(queryTab === "domains" && !canSeeDomains)
(queryTab === "domains" && !canSeeDomains) ||
(queryTab === "deployments" && !canSeeDeployments)
? DEFAULT_TAB
: queryTab;
const setTab = (value: string) => {
const { tab: _current, ...query } = router.query;
const { tab: _current, subtab: _subtab, ...query } = router.query;
router.replace(
{
pathname: router.pathname,
@@ -47,6 +50,9 @@ const Overview = () => {
<TabsTrigger value="services">Services</TabsTrigger>
{canSeeBackups && <TabsTrigger value="backups">Backups</TabsTrigger>}
{canSeeDomains && <TabsTrigger value="domains">Domains</TabsTrigger>}
{canSeeDeployments && (
<TabsTrigger value="deployments">Deployments</TabsTrigger>
)}
</TabsList>
<TabsContent value="services">
<ShowOverviewServices />
@@ -61,6 +67,11 @@ const Overview = () => {
<ShowOverviewDomains />
</TabsContent>
)}
{canSeeDeployments && (
<TabsContent value="deployments">
<ShowOverviewDeployments />
</TabsContent>
)}
</Tabs>
);
};