mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +00:00
refactor: unify server admin tools into dashboard pages with server selector (#4625)
* refactor: unify server admin tools into dashboard pages with server selector Replace the per-server Advanced dropdown (Traefik file system, Docker containers, swarm overview, swarm nodes, schedules) with a server selector on the existing dashboard routes, defaulting to the Dokploy server. Pages are now available in cloud too, since the dropdown was the only entry point there; the cloud-only monitoring modal moves to an icon button on the server card. * feat: add frontend-design skill and enhance dashboard UI components - Introduced a new skill for creating high-quality frontend designs, emphasizing intentional aesthetics and detailed guidelines for implementation. - Updated the Traefik system component to improve the user experience when no files or directories are found, incorporating new icons and a more informative layout. - Enhanced the server filter component with improved loading states, user prompts, and a more visually appealing design, including badges and better server information display. * [autofix.ci] apply automated fixes * style: adjust Card component layout in schedules page for improved responsiveness - Modified the Card component in the schedules page to ensure it utilizes full width while maintaining the minimum height, enhancing the overall layout and user experience. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: frontend-design
|
||||||
|
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||||
|
license: Complete terms in LICENSE.txt
|
||||||
|
---
|
||||||
|
|
||||||
|
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||||
|
|
||||||
|
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||||
|
|
||||||
|
## Design Thinking
|
||||||
|
|
||||||
|
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||||
|
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||||
|
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||||
|
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||||
|
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||||
|
|
||||||
|
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||||
|
|
||||||
|
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||||
|
- Production-grade and functional
|
||||||
|
- Visually striking and memorable
|
||||||
|
- Cohesive with a clear aesthetic point-of-view
|
||||||
|
- Meticulously refined in every detail
|
||||||
|
|
||||||
|
## Frontend Aesthetics Guidelines
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||||
|
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||||
|
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||||
|
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||||
|
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||||
|
|
||||||
|
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||||
|
|
||||||
|
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||||
|
|
||||||
|
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||||
|
|
||||||
|
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
import {
|
||||||
|
FileIcon,
|
||||||
|
Folder,
|
||||||
|
FolderOpen,
|
||||||
|
Loader2,
|
||||||
|
MousePointerClick,
|
||||||
|
Workflow,
|
||||||
|
} from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import {
|
import {
|
||||||
@@ -68,12 +75,22 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{directories?.length === 0 && (
|
{directories?.length === 0 && (
|
||||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
<div className="w-full flex-col gap-4 flex items-center justify-center h-[55vh] border border-dashed rounded-lg">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<div className="flex items-center justify-center size-14 rounded-full bg-muted">
|
||||||
No directories or files detected in{" "}
|
<FolderOpen className="size-7 text-muted-foreground" />
|
||||||
{"'/etc/dokploy/traefik'"}
|
</div>
|
||||||
</span>
|
<div className="flex flex-col items-center gap-1 text-center px-4">
|
||||||
<Folder className="size-8 text-muted-foreground" />
|
<span className="text-base font-medium">
|
||||||
|
No configuration files found
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
There are no directories or files in{" "}
|
||||||
|
<code className="bg-muted px-1.5 py-0.5 rounded text-xs">
|
||||||
|
/etc/dokploy/traefik
|
||||||
|
</code>{" "}
|
||||||
|
on this server yet.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{directories && directories?.length > 0 && (
|
{directories && directories?.length > 0 && (
|
||||||
@@ -89,11 +106,19 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
|||||||
{file ? (
|
{file ? (
|
||||||
<ShowTraefikFile path={file} serverId={serverId} />
|
<ShowTraefikFile path={file} serverId={serverId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
<div className="h-full min-h-[300px] w-full flex-col gap-4 flex items-center justify-center border border-dashed rounded-lg">
|
||||||
<span className="text-muted-foreground text-lg font-medium">
|
<div className="flex items-center justify-center size-14 rounded-full bg-muted">
|
||||||
No file selected
|
<MousePointerClick className="size-7 text-muted-foreground" />
|
||||||
</span>
|
</div>
|
||||||
<FileIcon className="size-8 text-muted-foreground" />
|
<div className="flex flex-col items-center gap-1 text-center px-4">
|
||||||
|
<span className="text-base font-medium">
|
||||||
|
Select a file to edit
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Choose a file from the tree on the left to view
|
||||||
|
and edit its contents.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { ShowNodes } from "./show-nodes";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
serverId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowNodesModal = ({ serverId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer "
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Show Swarm Nodes
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="min-w-[70vw]">
|
|
||||||
<div className="grid w-full gap-1">
|
|
||||||
<ShowNodes serverId={serverId} />
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { ShowContainers } from "../../docker/show/show-containers";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
serverId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowDockerContainersModal = ({ serverId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer "
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Show Docker Containers
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-7xl ">
|
|
||||||
<div className="grid w-full gap-1">
|
|
||||||
<ShowContainers serverId={serverId} />
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { BarChartHorizontalBigIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { ShowPaidMonitoring } from "../../monitoring/paid/servers/show-paid-monitoring";
|
import { ShowPaidMonitoring } from "../../monitoring/paid/servers/show-paid-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,12 +15,9 @@ export const ShowMonitoringModal = ({ url, token }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<DropdownMenuItem
|
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||||
className="w-full cursor-pointer "
|
<BarChartHorizontalBigIcon className="h-4 w-4" />
|
||||||
onSelect={(e) => e.preventDefault()}
|
</Button>
|
||||||
>
|
|
||||||
Show Monitoring
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-7xl ">
|
<DialogContent className="sm:max-w-7xl ">
|
||||||
<div className="flex gap-4 py-4 w-full">
|
<div className="flex gap-4 py-4 w-full">
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
serverId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowSchedulesModal = ({ serverId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer "
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Show Schedules
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-5xl ">
|
|
||||||
<ShowSchedules id={serverId} scheduleType="server" />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Key,
|
Key,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
MoreHorizontal,
|
|
||||||
Network,
|
Network,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
Terminal,
|
Terminal,
|
||||||
@@ -25,12 +24,6 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -38,16 +31,11 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
|
|
||||||
import { TerminalModal } from "../web-server/terminal-modal";
|
import { TerminalModal } from "../web-server/terminal-modal";
|
||||||
import { ShowServerActions } from "./actions/show-server-actions";
|
import { ShowServerActions } from "./actions/show-server-actions";
|
||||||
import { HandleServers } from "./handle-servers";
|
import { HandleServers } from "./handle-servers";
|
||||||
import { SetupServer } from "./setup-server";
|
import { SetupServer } from "./setup-server";
|
||||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
|
||||||
import { ShowMonitoringModal } from "./show-monitoring-modal";
|
import { ShowMonitoringModal } from "./show-monitoring-modal";
|
||||||
import { ShowSchedulesModal } from "./show-schedules-modal";
|
|
||||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
|
||||||
import { WelcomeSubscription } from "./welcome-stripe/welcome-subscription";
|
import { WelcomeSubscription } from "./welcome-stripe/welcome-subscription";
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
@@ -138,52 +126,6 @@ export const ShowServers = () => {
|
|||||||
{server.name}
|
{server.name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
{isActive &&
|
|
||||||
server.sshKeyId &&
|
|
||||||
!isBuildServer && (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-8 w-8 shrink-0 p-0"
|
|
||||||
>
|
|
||||||
<span className="sr-only">
|
|
||||||
More options
|
|
||||||
</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>
|
|
||||||
Advanced
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<ShowTraefikFileSystemModal
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
<ShowDockerContainersModal
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
{isCloud && (
|
|
||||||
<ShowMonitoringModal
|
|
||||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
|
||||||
token={
|
|
||||||
server?.metricsConfig?.server
|
|
||||||
?.token
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<ShowSwarmOverviewModal
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
<ShowNodesModal
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
<ShowSchedulesModal
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex gap-2 mt-2 flex-wrap">
|
<div className="flex gap-2 mt-2 flex-wrap">
|
||||||
@@ -361,6 +303,27 @@ export const ShowServers = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isCloud &&
|
||||||
|
server.sshKeyId &&
|
||||||
|
!isBuildServer && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<ShowMonitoringModal
|
||||||
|
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||||
|
token={
|
||||||
|
server?.metricsConfig
|
||||||
|
?.server?.token
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Monitoring</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
{permissions?.server.delete && (
|
{permissions?.server.delete && (
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import { ShowSwarmContainers } from "../../swarm/containers/show-swarm-containers";
|
|
||||||
import SwarmMonitorCard from "../../swarm/monitoring-card";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
serverId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowSwarmOverviewModal = ({ serverId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer "
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Show Swarm Overview
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-7xl ">
|
|
||||||
<Tabs defaultValue="overview">
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
||||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="overview">
|
|
||||||
<div className="grid w-full gap-1">
|
|
||||||
<SwarmMonitorCard serverId={serverId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="containers">
|
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
|
||||||
<div className="rounded-xl bg-background shadow-md p-6">
|
|
||||||
<ShowSwarmContainers serverId={serverId} />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
serverId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowTraefikFileSystemModal = ({ serverId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer "
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Show Traefik File System
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-7xl ">
|
|
||||||
<ShowTraefikSystem serverId={serverId} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -182,36 +182,31 @@ const MENU: Menu = {
|
|||||||
title: "Schedules",
|
title: "Schedules",
|
||||||
url: "/dashboard/schedules",
|
url: "/dashboard/schedules",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
// Only enabled in non-cloud environments
|
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||||
isEnabled: ({ isCloud, permissions }) =>
|
|
||||||
!isCloud && !!permissions?.organization.update,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
url: "/dashboard/traefik",
|
url: "/dashboard/traefik",
|
||||||
icon: GalleryVerticalEnd,
|
icon: GalleryVerticalEnd,
|
||||||
// Only enabled for users with access to Traefik files in non-cloud environments
|
// Only enabled for users with access to Traefik files
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ permissions }) => !!permissions?.traefikFiles.read,
|
||||||
!!(permissions?.traefikFiles.read && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Docker",
|
title: "Docker",
|
||||||
url: "/dashboard/docker",
|
url: "/dashboard/docker",
|
||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ permissions }) => !!permissions?.docker.read,
|
||||||
!!(permissions?.docker.read && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Swarm",
|
title: "Swarm",
|
||||||
url: "/dashboard/swarm",
|
url: "/dashboard/swarm",
|
||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ permissions }) => !!permissions?.docker.read,
|
||||||
!!(permissions?.docker.read && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -375,9 +370,8 @@ const MENU: Menu = {
|
|||||||
title: "Cluster",
|
title: "Cluster",
|
||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||||
!!(permissions?.organization.update && !isCloud),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Loader2, PlusIcon, ServerIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { Fragment, type ReactNode } from "react";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
const DOKPLOY_SERVER = "dokploy-server";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: (serverId?: string) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerFilter = ({ children }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { data: servers, isLoading: isLoadingServers } =
|
||||||
|
api.server.withSSHKey.useQuery();
|
||||||
|
const { data: isCloud, isLoading: isLoadingCloud } =
|
||||||
|
api.settings.isCloud.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
|
const queryServerId =
|
||||||
|
typeof router.query.serverId === "string"
|
||||||
|
? router.query.serverId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const selectedServer = servers?.find(
|
||||||
|
(server) => server.serverId === queryServerId,
|
||||||
|
);
|
||||||
|
// Cloud has no local Dokploy server, so fall back to the first remote server
|
||||||
|
const serverId = selectedServer
|
||||||
|
? selectedServer.serverId
|
||||||
|
: isCloud
|
||||||
|
? servers?.[0]?.serverId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const setServerId = (value: string) => {
|
||||||
|
const { serverId: _current, ...query } = router.query;
|
||||||
|
router.replace(
|
||||||
|
{
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: value === DOKPLOY_SERVER ? query : { ...query, serverId: value },
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ shallow: true },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoadingServers || isLoadingCloud) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-sidebar p-2.5 rounded-xl w-full">
|
||||||
|
<div className="rounded-xl bg-background shadow-md flex flex-col gap-2 items-center justify-center min-h-[60vh]">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloud && !servers?.length) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-sidebar p-2.5 rounded-xl w-full">
|
||||||
|
<div className="rounded-xl bg-background shadow-md flex flex-col items-center justify-center gap-5 min-h-[60vh] border border-dashed px-4">
|
||||||
|
<div className="flex items-center justify-center size-16 rounded-full bg-muted">
|
||||||
|
<ServerIcon className="size-8 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-1.5 text-center max-w-md">
|
||||||
|
<span className="text-lg font-medium">No servers yet</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{permissions?.server.create
|
||||||
|
? "This section works on your remote servers. Add your first server to start managing it from here."
|
||||||
|
: "This section works on your remote servers. Ask an administrator to add a server to your organization."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{permissions?.server.create && (
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/dashboard/settings/servers">
|
||||||
|
<PlusIcon className="size-4" />
|
||||||
|
Add Server
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
{!!servers?.length && (
|
||||||
|
<div className="flex w-full items-center justify-end gap-3">
|
||||||
|
<Label
|
||||||
|
htmlFor="server-filter"
|
||||||
|
className="text-sm text-muted-foreground whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Viewing server
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={serverId ?? DOKPLOY_SERVER}
|
||||||
|
onValueChange={setServerId}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="server-filter" className="w-fit min-w-[220px]">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ServerIcon className="size-4 text-muted-foreground" />
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</div>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{!isCloud && (
|
||||||
|
<SelectItem value={DOKPLOY_SERVER}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>Dokploy Server</span>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-[10px] px-1.5 py-0"
|
||||||
|
>
|
||||||
|
Local
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
{servers.map((server) => (
|
||||||
|
<SelectItem key={server.serverId} value={server.serverId}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>{server.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{server.ipAddress}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Fragment key={serverId ?? DOKPLOY_SERVER}>{children(serverId)}</Fragment>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -6,10 +5,15 @@ import type { ReactElement } from "react";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
import { ShowContainers } from "@/components/dashboard/docker/show/show-containers";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { ServerFilter } from "@/components/shared/server-filter";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
return <ShowContainers />;
|
return (
|
||||||
|
<ServerFilter>
|
||||||
|
{(serverId) => <ShowContainers serverId={serverId} />}
|
||||||
|
</ServerFilter>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
@@ -20,14 +24,6 @@ Dashboard.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: true,
|
|
||||||
destination: "/dashboard/home",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { user, session } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { ServerFilter } from "@/components/shared/server-filter";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
function SchedulesPage() {
|
function SchedulesPage() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<ServerFilter>
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
|
{(serverId) => (
|
||||||
<div className="rounded-xl bg-background shadow-md h-full">
|
<div className="w-full">
|
||||||
<ShowSchedules scheduleType="dokploy-server" id="dokploy-server" />
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full min-h-[45vh]">
|
||||||
|
<div className="rounded-xl bg-background shadow-md h-full">
|
||||||
|
<ShowSchedules
|
||||||
|
scheduleType={serverId ? "server" : "dokploy-server"}
|
||||||
|
id={serverId ?? "dokploy-server"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
)}
|
||||||
</div>
|
</ServerFilter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default SchedulesPage;
|
export default SchedulesPage;
|
||||||
@@ -26,14 +33,6 @@ SchedulesPage.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: "/dashboard/home",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { user } = await validateRequest(ctx.req);
|
const { user } = await validateRequest(ctx.req);
|
||||||
if (!user || (user.role !== "owner" && user.role !== "admin")) {
|
if (!user || (user.role !== "owner" && user.role !== "admin")) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { ServerFilter } from "@/components/shared/server-filter";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<ServerFilter>
|
||||||
<ShowNodes />
|
{(serverId) => (
|
||||||
</div>
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<ShowNodes serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ServerFilter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,14 +29,6 @@ export async function getServerSideProps(
|
|||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: "/dashboard/home",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { user, session } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user || user.role === "member") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -7,30 +6,35 @@ import superjson from "superjson";
|
|||||||
import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers";
|
import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers";
|
||||||
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { ServerFilter } from "@/components/shared/server-filter";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<ServerFilter>
|
||||||
<Tabs defaultValue="overview">
|
{(serverId) => (
|
||||||
<TabsList>
|
<div className="space-y-4">
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
<Tabs defaultValue="overview">
|
||||||
<TabsTrigger value="containers">Containers</TabsTrigger>
|
<TabsList>
|
||||||
</TabsList>
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||||
<TabsContent value="overview">
|
<TabsTrigger value="containers">Containers</TabsTrigger>
|
||||||
<SwarmMonitorCard />
|
</TabsList>
|
||||||
</TabsContent>
|
<TabsContent value="overview">
|
||||||
<TabsContent value="containers">
|
<SwarmMonitorCard serverId={serverId} />
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
</TabsContent>
|
||||||
<div className="rounded-xl bg-background shadow-md p-6">
|
<TabsContent value="containers">
|
||||||
<ShowSwarmContainers />
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||||
</div>
|
<div className="rounded-xl bg-background shadow-md p-6">
|
||||||
</Card>
|
<ShowSwarmContainers serverId={serverId} />
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</Card>
|
||||||
</div>
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ServerFilter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,14 +46,6 @@ Dashboard.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: "/dashboard/home",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { user, session } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -6,10 +5,15 @@ import type { ReactElement } from "react";
|
|||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
import { ShowTraefikSystem } from "@/components/dashboard/file-system/show-traefik-system";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { ServerFilter } from "@/components/shared/server-filter";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
return <ShowTraefikSystem />;
|
return (
|
||||||
|
<ServerFilter>
|
||||||
|
{(serverId) => <ShowTraefikSystem serverId={serverId} />}
|
||||||
|
</ServerFilter>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
@@ -20,14 +24,6 @@ Dashboard.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: "/dashboard/home",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { user, session } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user