mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
@@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
{mount.type === "file" && (
|
{mount.type === "file" && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium">Content</span>
|
<span className="font-medium">Content</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
|
||||||
{mount.content}
|
{mount.content}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,12 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
{mount.type === "file" ? (
|
||||||
<span className="font-medium">Mount Path</span>
|
<div className="flex flex-col gap-1">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="font-medium">File Path</span>
|
||||||
{mount.mountPath}
|
<span className="text-sm text-muted-foreground">
|
||||||
</span>
|
{mount.filePath}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="font-medium">Mount Path</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{mount.mountPath}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateVolume
|
<UpdateVolume
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:m:max-w-lg ">
|
<DialogContent className="sm:m:max-w-lg ">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add a project</DialogTitle>
|
<DialogTitle>{projectId ? "Update" : "Add a"} project</DialogTitle>
|
||||||
<DialogDescription>The home of something big!</DialogDescription>
|
<DialogDescription>The home of something big!</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|||||||
@@ -87,9 +87,12 @@ export const ShowProjects = () => {
|
|||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<div className="">
|
|
||||||
<HandleProject />
|
{(auth?.rol === "admin" || user?.canCreateProjects) && (
|
||||||
</div>
|
<div className="">
|
||||||
|
<HandleProject />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
AudioWaveform,
|
|
||||||
BarChartHorizontalBigIcon,
|
BarChartHorizontalBigIcon,
|
||||||
Bell,
|
Bell,
|
||||||
BlocksIcon,
|
BlocksIcon,
|
||||||
@@ -9,7 +8,6 @@ import {
|
|||||||
Boxes,
|
Boxes,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Command,
|
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -27,8 +25,8 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
@@ -65,243 +63,290 @@ import {
|
|||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { AppRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import type { inferRouterOutputs } from "@trpc/server";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Logo } from "../shared/logo";
|
import { Logo } from "../shared/logo";
|
||||||
import { UpdateServerButton } from "./update-server";
|
import { UpdateServerButton } from "./update-server";
|
||||||
import { UserNav } from "./user-nav";
|
import { UserNav } from "./user-nav";
|
||||||
// This is sample data.
|
|
||||||
interface NavItem {
|
// The types of the queries we are going to use
|
||||||
|
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"];
|
||||||
|
type UserQueryOutput = inferRouterOutputs<AppRouter>["user"]["byAuthId"];
|
||||||
|
|
||||||
|
type SingleNavItem = {
|
||||||
|
isSingle?: true;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isSingle: boolean;
|
isEnabled?: (opts: {
|
||||||
isActive: boolean;
|
auth?: AuthQueryOutput;
|
||||||
items?: {
|
user?: UserQueryOutput;
|
||||||
title: string;
|
isCloud: boolean;
|
||||||
url: string;
|
}) => boolean;
|
||||||
icon?: LucideIcon;
|
};
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExternalLink {
|
// NavItem type
|
||||||
|
// Consists of a single item or a group of items
|
||||||
|
// If `isSingle` is true or undefined, the item is a single item
|
||||||
|
// If `isSingle` is false, the item is a group of items
|
||||||
|
type NavItem =
|
||||||
|
| SingleNavItem
|
||||||
|
| {
|
||||||
|
isSingle: false;
|
||||||
|
title: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
items: SingleNavItem[];
|
||||||
|
isEnabled?: (opts: {
|
||||||
|
auth?: AuthQueryOutput;
|
||||||
|
user?: UserQueryOutput;
|
||||||
|
isCloud: boolean;
|
||||||
|
}) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ExternalLink type
|
||||||
|
// Represents an external link item (used for the help section)
|
||||||
|
type ExternalLink = {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
}
|
isEnabled?: (opts: {
|
||||||
|
auth?: AuthQueryOutput;
|
||||||
|
user?: UserQueryOutput;
|
||||||
|
isCloud: boolean;
|
||||||
|
}) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const data = {
|
// Menu type
|
||||||
user: {
|
// Consists of home, settings, and help items
|
||||||
name: "shadcn",
|
type Menu = {
|
||||||
email: "m@example.com",
|
home: NavItem[];
|
||||||
avatar: "/avatars/shadcn.jpg",
|
settings: NavItem[];
|
||||||
},
|
help: ExternalLink[];
|
||||||
teams: [
|
};
|
||||||
{
|
|
||||||
name: "Dokploy",
|
// Menu items
|
||||||
logo: Logo,
|
// Consists of unfiltered home, settings, and help items
|
||||||
plan: "Enterprise",
|
// The items are filtered based on the user's role and permissions
|
||||||
},
|
// The `isEnabled` function is called to determine if the item should be displayed
|
||||||
{
|
const MENU: Menu = {
|
||||||
name: "Acme Corp.",
|
|
||||||
logo: AudioWaveform,
|
|
||||||
plan: "Startup",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Evil Corp.",
|
|
||||||
logo: Command,
|
|
||||||
plan: "Free",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
home: [
|
home: [
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
url: "/dashboard/projects",
|
url: "/dashboard/projects",
|
||||||
icon: Folder,
|
icon: Folder,
|
||||||
isSingle: true,
|
|
||||||
isActive: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Monitoring",
|
title: "Monitoring",
|
||||||
url: "/dashboard/monitoring",
|
url: "/dashboard/monitoring",
|
||||||
icon: BarChartHorizontalBigIcon,
|
icon: BarChartHorizontalBigIcon,
|
||||||
isSingle: true,
|
// Only enabled in non-cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) => !isCloud,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
url: "/dashboard/traefik",
|
url: "/dashboard/traefik",
|
||||||
icon: GalleryVerticalEnd,
|
icon: GalleryVerticalEnd,
|
||||||
isSingle: true,
|
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) =>
|
||||||
|
!!(
|
||||||
|
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Docker",
|
title: "Docker",
|
||||||
url: "/dashboard/docker",
|
url: "/dashboard/docker",
|
||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
isSingle: true,
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) =>
|
||||||
|
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Swarm",
|
title: "Swarm",
|
||||||
url: "/dashboard/swarm",
|
url: "/dashboard/swarm",
|
||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
isSingle: true,
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) =>
|
||||||
|
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Requests",
|
title: "Requests",
|
||||||
url: "/dashboard/requests",
|
url: "/dashboard/requests",
|
||||||
icon: Forward,
|
icon: Forward,
|
||||||
isSingle: true,
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) =>
|
||||||
|
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Legacy unused menu, adjusted to the new structure
|
||||||
// {
|
// {
|
||||||
|
// isSingle: true,
|
||||||
// title: "Projects",
|
// title: "Projects",
|
||||||
// url: "/dashboard/projects",
|
// url: "/dashboard/projects",
|
||||||
// icon: Folder,
|
// icon: Folder,
|
||||||
// isSingle: true,
|
|
||||||
// },
|
// },
|
||||||
// {
|
// {
|
||||||
|
// isSingle: true,
|
||||||
// title: "Monitoring",
|
// title: "Monitoring",
|
||||||
// icon: BarChartHorizontalBigIcon,
|
// icon: BarChartHorizontalBigIcon,
|
||||||
// url: "/dashboard/settings/monitoring",
|
// url: "/dashboard/settings/monitoring",
|
||||||
// isSingle: true,
|
|
||||||
// },
|
// },
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// title: "Settings",
|
// isSingle: false,
|
||||||
// url: "#",
|
// title: "Settings",
|
||||||
// icon: Settings2,
|
// icon: Settings2,
|
||||||
// isActive: true,
|
// items: [
|
||||||
// items: [
|
// {
|
||||||
// {
|
// title: "Profile",
|
||||||
// title: "Profile",
|
// url: "/dashboard/settings/profile",
|
||||||
// url: "/dashboard/settings/profile",
|
// },
|
||||||
// },
|
// {
|
||||||
// {
|
// title: "Users",
|
||||||
// title: "Users",
|
// url: "/dashboard/settings/users",
|
||||||
// url: "/dashboard/settings/users",
|
// },
|
||||||
// },
|
// {
|
||||||
// {
|
// title: "SSH Key",
|
||||||
// title: "SSH Key",
|
// url: "/dashboard/settings/ssh-keys",
|
||||||
// url: "/dashboard/settings/ssh-keys",
|
// },
|
||||||
// },
|
// {
|
||||||
// {
|
// title: "Git",
|
||||||
// title: "Git",
|
// url: "/dashboard/settings/git-providers",
|
||||||
// url: "/dashboard/settings/git-providers",
|
// },
|
||||||
// },
|
// ],
|
||||||
// ],
|
|
||||||
// },
|
// },
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// title: "Integrations",
|
// isSingle: false,
|
||||||
// icon: BlocksIcon,
|
// title: "Integrations",
|
||||||
// items: [
|
// icon: BlocksIcon,
|
||||||
// {
|
// items: [
|
||||||
// title: "S3 Destinations",
|
// {
|
||||||
// url: "/dashboard/settings/destinations",
|
// title: "S3 Destinations",
|
||||||
// },
|
// url: "/dashboard/settings/destinations",
|
||||||
// {
|
// },
|
||||||
// title: "Registry",
|
// {
|
||||||
// url: "/dashboard/settings/registry",
|
// title: "Registry",
|
||||||
// },
|
// url: "/dashboard/settings/registry",
|
||||||
// {
|
// },
|
||||||
// title: "Notifications",
|
// {
|
||||||
// url: "/dashboard/settings/notifications",
|
// title: "Notifications",
|
||||||
// },
|
// url: "/dashboard/settings/notifications",
|
||||||
// ],
|
// },
|
||||||
] as NavItem[],
|
// ],
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Server",
|
title: "Server",
|
||||||
url: "/dashboard/settings/server",
|
url: "/dashboard/settings/server",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
isSingle: true,
|
// Only enabled for admins in non-cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) =>
|
||||||
|
!!(auth?.rol === "admin" && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Profile",
|
title: "Profile",
|
||||||
url: "/dashboard/settings/profile",
|
url: "/dashboard/settings/profile",
|
||||||
icon: User,
|
icon: User,
|
||||||
isSingle: true,
|
|
||||||
isActive: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Servers",
|
title: "Servers",
|
||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
isSingle: true,
|
// Only enabled for admins
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Users",
|
title: "Users",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
isSingle: true,
|
// Only enabled for admins
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "SSH Keys",
|
title: "SSH Keys",
|
||||||
icon: KeyRound,
|
icon: KeyRound,
|
||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
isSingle: true,
|
// Only enabled for admins and users with access to SSH keys
|
||||||
isActive: false,
|
isEnabled: ({ auth, user }) =>
|
||||||
|
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Git",
|
title: "Git",
|
||||||
url: "/dashboard/settings/git-providers",
|
url: "/dashboard/settings/git-providers",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
isSingle: true,
|
// Only enabled for admins and users with access to Git providers
|
||||||
isActive: false,
|
isEnabled: ({ auth, user }) =>
|
||||||
|
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Registry",
|
title: "Registry",
|
||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
isSingle: true,
|
// Only enabled for admins
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "S3 Destinations",
|
title: "S3 Destinations",
|
||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
isSingle: true,
|
// Only enabled for admins
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Certificates",
|
title: "Certificates",
|
||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
isSingle: true,
|
// Only enabled for admins
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Cluster",
|
title: "Cluster",
|
||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
isSingle: true,
|
// Only enabled for admins in non-cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) =>
|
||||||
|
!!(auth?.rol === "admin" && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Notifications",
|
title: "Notifications",
|
||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
isSingle: true,
|
// Only enabled for admins
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
isSingle: true,
|
||||||
title: "Billing",
|
title: "Billing",
|
||||||
url: "/dashboard/settings/billing",
|
url: "/dashboard/settings/billing",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
isSingle: true,
|
// Only enabled for admins in cloud environments
|
||||||
isActive: false,
|
isEnabled: ({ auth, user, isCloud }) =>
|
||||||
|
!!(auth?.rol === "admin" && isCloud),
|
||||||
},
|
},
|
||||||
] as NavItem[],
|
],
|
||||||
|
|
||||||
help: [
|
help: [
|
||||||
{
|
{
|
||||||
name: "Documentation",
|
name: "Documentation",
|
||||||
@@ -325,8 +370,108 @@ const data = {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
] as ExternalLink[],
|
],
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a menu based on the current user's role and permissions
|
||||||
|
* @returns a menu object with the home, settings, and help items
|
||||||
|
*/
|
||||||
|
function createMenuForAuthUser(opts: {
|
||||||
|
auth?: AuthQueryOutput;
|
||||||
|
user?: UserQueryOutput;
|
||||||
|
isCloud: boolean;
|
||||||
|
}): Menu {
|
||||||
|
return {
|
||||||
|
// Filter the home items based on the user's role and permissions
|
||||||
|
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
||||||
|
home: MENU.home.filter((item) =>
|
||||||
|
!item.isEnabled
|
||||||
|
? true
|
||||||
|
: item.isEnabled({
|
||||||
|
auth: opts.auth,
|
||||||
|
user: opts.user,
|
||||||
|
isCloud: opts.isCloud,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
// Filter the settings items based on the user's role and permissions
|
||||||
|
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
||||||
|
settings: MENU.settings.filter((item) =>
|
||||||
|
!item.isEnabled
|
||||||
|
? true
|
||||||
|
: item.isEnabled({
|
||||||
|
auth: opts.auth,
|
||||||
|
user: opts.user,
|
||||||
|
isCloud: opts.isCloud,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
// Filter the help items based on the user's role and permissions
|
||||||
|
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
||||||
|
help: MENU.help.filter((item) =>
|
||||||
|
!item.isEnabled
|
||||||
|
? true
|
||||||
|
: item.isEnabled({
|
||||||
|
auth: opts.auth,
|
||||||
|
user: opts.user,
|
||||||
|
isCloud: opts.isCloud,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an item url is active based on the current pathname
|
||||||
|
* @returns true if the item url is active, false otherwise
|
||||||
|
*/
|
||||||
|
function isActiveRoute(opts: {
|
||||||
|
/** The url of the item. Usually obtained from `item.url` */
|
||||||
|
itemUrl: string;
|
||||||
|
/** The current pathname. Usually obtained from `usePathname()` */
|
||||||
|
pathname: string;
|
||||||
|
}): boolean {
|
||||||
|
const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project");
|
||||||
|
const normalizedPathname = opts.pathname?.replace("/projects", "/project");
|
||||||
|
|
||||||
|
if (!normalizedPathname) return false;
|
||||||
|
|
||||||
|
if (normalizedPathname === normalizedItemUrl) return true;
|
||||||
|
|
||||||
|
if (normalizedPathname.startsWith(normalizedItemUrl)) {
|
||||||
|
const nextChar = normalizedPathname.charAt(normalizedItemUrl.length);
|
||||||
|
return nextChar === "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the active nav item based on the current pathname
|
||||||
|
* @returns the active nav item with `SingleNavItem` type or undefined if none is active
|
||||||
|
*/
|
||||||
|
function findActiveNavItem(
|
||||||
|
navItems: NavItem[],
|
||||||
|
pathname: string,
|
||||||
|
): SingleNavItem | undefined {
|
||||||
|
const found = navItems.find((item) =>
|
||||||
|
item.isSingle !== false
|
||||||
|
? // The current item is single, so check if the item url is active
|
||||||
|
isActiveRoute({ itemUrl: item.url, pathname })
|
||||||
|
: // The current item is not single, so check if any of the sub items are active
|
||||||
|
item.items.some((item) =>
|
||||||
|
isActiveRoute({ itemUrl: item.url, pathname }),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (found?.isSingle !== false) {
|
||||||
|
// The found item is single, so return it
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The found item is not single, so find the active sub item
|
||||||
|
return found?.items.find((item) =>
|
||||||
|
isActiveRoute({ itemUrl: item.url, pathname }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -398,64 +543,21 @@ export default function Page({ children }: Props) {
|
|||||||
|
|
||||||
const includesProjects = pathname?.includes("/dashboard/project");
|
const includesProjects = pathname?.includes("/dashboard/project");
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
const isActiveRoute = (itemUrl: string) => {
|
|
||||||
const normalizedItemUrl = itemUrl?.replace("/projects", "/project");
|
|
||||||
const normalizedPathname = pathname?.replace("/projects", "/project");
|
|
||||||
|
|
||||||
if (!normalizedPathname) return false;
|
const {
|
||||||
|
home: filteredHome,
|
||||||
|
settings: filteredSettings,
|
||||||
|
help,
|
||||||
|
} = createMenuForAuthUser({ auth, user, isCloud: !!isCloud });
|
||||||
|
|
||||||
if (normalizedPathname === normalizedItemUrl) return true;
|
const activeItem = findActiveNavItem(
|
||||||
|
[...filteredHome, ...filteredSettings],
|
||||||
|
pathname,
|
||||||
|
);
|
||||||
|
|
||||||
if (normalizedPathname.startsWith(normalizedItemUrl)) {
|
// const showProjectsButton =
|
||||||
const nextChar = normalizedPathname.charAt(normalizedItemUrl.length);
|
// currentPath === "/dashboard/projects" &&
|
||||||
return nextChar === "/";
|
// (auth?.rol === "admin" || user?.canCreateProjects);
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let filteredHome = isCloud
|
|
||||||
? data.home.filter(
|
|
||||||
(item) =>
|
|
||||||
![
|
|
||||||
"/dashboard/monitoring",
|
|
||||||
"/dashboard/traefik",
|
|
||||||
"/dashboard/docker",
|
|
||||||
"/dashboard/swarm",
|
|
||||||
"/dashboard/requests",
|
|
||||||
].includes(item.url),
|
|
||||||
)
|
|
||||||
: data.home;
|
|
||||||
|
|
||||||
let filteredSettings = isCloud
|
|
||||||
? data.settings.filter(
|
|
||||||
(item) =>
|
|
||||||
![
|
|
||||||
"/dashboard/settings/server",
|
|
||||||
"/dashboard/settings/cluster",
|
|
||||||
].includes(item.url),
|
|
||||||
)
|
|
||||||
: data.settings.filter(
|
|
||||||
(item) => !["/dashboard/settings/billing"].includes(item.url),
|
|
||||||
);
|
|
||||||
|
|
||||||
filteredHome = filteredHome.map((item) => ({
|
|
||||||
...item,
|
|
||||||
isActive: isActiveRoute(item.url),
|
|
||||||
}));
|
|
||||||
|
|
||||||
filteredSettings = filteredSettings.map((item) => ({
|
|
||||||
...item,
|
|
||||||
isActive: isActiveRoute(item.url),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const activeItem =
|
|
||||||
filteredHome.find((item) => item.isActive) ||
|
|
||||||
filteredSettings.find((item) => item.isActive);
|
|
||||||
|
|
||||||
const showProjectsButton =
|
|
||||||
currentPath === "/dashboard/projects" &&
|
|
||||||
(auth?.rol === "admin" || user?.canCreateProjects);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
@@ -486,173 +588,185 @@ export default function Page({ children }: Props) {
|
|||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Home</SidebarGroupLabel>
|
<SidebarGroupLabel>Home</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{filteredHome.map((item) => (
|
{filteredHome.map((item) => {
|
||||||
<Collapsible
|
const isSingle = item.isSingle !== false;
|
||||||
key={item.title}
|
const isActive = isSingle
|
||||||
asChild
|
? isActiveRoute({ itemUrl: item.url, pathname })
|
||||||
defaultOpen={item.isActive}
|
: item.items.some((item) =>
|
||||||
className="group/collapsible"
|
isActiveRoute({ itemUrl: item.url, pathname }),
|
||||||
>
|
);
|
||||||
<SidebarMenuItem>
|
|
||||||
{item.isSingle ? (
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild
|
|
||||||
tooltip={item.title}
|
|
||||||
className={cn(isActiveRoute(item.url) && "bg-border")}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={item.url}
|
|
||||||
className="flex w-full items-center gap-2"
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className={cn(
|
|
||||||
isActiveRoute(item.url) && "text-primary",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<SidebarMenuButton
|
|
||||||
tooltip={item.title}
|
|
||||||
isActive={item.isActive}
|
|
||||||
>
|
|
||||||
{item.icon && <item.icon />}
|
|
||||||
|
|
||||||
<span>{item.title}</span>
|
return (
|
||||||
{item.items?.length && (
|
<Collapsible
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
key={item.title}
|
||||||
|
asChild
|
||||||
|
defaultOpen={isActive}
|
||||||
|
className="group/collapsible"
|
||||||
|
>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
{isSingle ? (
|
||||||
|
<SidebarMenuButton
|
||||||
|
asChild
|
||||||
|
tooltip={item.title}
|
||||||
|
className={cn(isActive && "bg-border")}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={item.url}
|
||||||
|
className="flex w-full items-center gap-2"
|
||||||
|
>
|
||||||
|
{item.icon && (
|
||||||
|
<item.icon
|
||||||
|
className={cn(isActive && "text-primary")}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarMenuButton>
|
<span>{item.title}</span>
|
||||||
</CollapsibleTrigger>
|
</Link>
|
||||||
<CollapsibleContent>
|
</SidebarMenuButton>
|
||||||
<SidebarMenuSub>
|
) : (
|
||||||
{item.items?.map((subItem) => (
|
<>
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuButton
|
||||||
asChild
|
tooltip={item.title}
|
||||||
className={cn(
|
isActive={isActive}
|
||||||
isActiveRoute(subItem.url) && "bg-border",
|
>
|
||||||
)}
|
{item.icon && <item.icon />}
|
||||||
>
|
|
||||||
<Link
|
<span>{item.title}</span>
|
||||||
href={subItem.url}
|
{item.items?.length && (
|
||||||
className="flex w-full items-center"
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
|
)}
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{item.items?.map((subItem) => (
|
||||||
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
asChild
|
||||||
|
className={cn(isActive && "bg-border")}
|
||||||
>
|
>
|
||||||
{subItem.icon && (
|
<Link
|
||||||
<span className="mr-2">
|
href={subItem.url}
|
||||||
<subItem.icon
|
className="flex w-full items-center"
|
||||||
className={cn(
|
>
|
||||||
"h-4 w-4 text-muted-foreground",
|
{subItem.icon && (
|
||||||
isActiveRoute(subItem.url) &&
|
<span className="mr-2">
|
||||||
"text-primary",
|
<subItem.icon
|
||||||
)}
|
className={cn(
|
||||||
/>
|
"h-4 w-4 text-muted-foreground",
|
||||||
</span>
|
isActive && "text-primary",
|
||||||
)}
|
)}
|
||||||
<span>{subItem.title}</span>
|
/>
|
||||||
</Link>
|
</span>
|
||||||
</SidebarMenuSubButton>
|
)}
|
||||||
</SidebarMenuSubItem>
|
<span>{subItem.title}</span>
|
||||||
))}
|
</Link>
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSubButton>
|
||||||
</CollapsibleContent>
|
</SidebarMenuSubItem>
|
||||||
</>
|
))}
|
||||||
)}
|
</SidebarMenuSub>
|
||||||
</SidebarMenuItem>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</>
|
||||||
))}
|
)}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Settings</SidebarGroupLabel>
|
<SidebarGroupLabel>Settings</SidebarGroupLabel>
|
||||||
<SidebarMenu className="gap-2">
|
<SidebarMenu className="gap-2">
|
||||||
{filteredSettings.map((item) => (
|
{filteredSettings.map((item) => {
|
||||||
<Collapsible
|
const isSingle = item.isSingle !== false;
|
||||||
key={item.title}
|
const isActive = isSingle
|
||||||
asChild
|
? isActiveRoute({ itemUrl: item.url, pathname })
|
||||||
defaultOpen={item.isActive}
|
: item.items.some((item) =>
|
||||||
className="group/collapsible"
|
isActiveRoute({ itemUrl: item.url, pathname }),
|
||||||
>
|
);
|
||||||
<SidebarMenuItem>
|
|
||||||
{item.isSingle ? (
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild
|
|
||||||
tooltip={item.title}
|
|
||||||
className={cn(isActiveRoute(item.url) && "bg-border")}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={item.url}
|
|
||||||
className="flex w-full items-center gap-2"
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className={cn(
|
|
||||||
isActiveRoute(item.url) && "text-primary",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<SidebarMenuButton
|
|
||||||
tooltip={item.title}
|
|
||||||
isActive={item.isActive}
|
|
||||||
>
|
|
||||||
{item.icon && <item.icon />}
|
|
||||||
|
|
||||||
<span>{item.title}</span>
|
return (
|
||||||
{item.items?.length && (
|
<Collapsible
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
key={item.title}
|
||||||
|
asChild
|
||||||
|
defaultOpen={isActive}
|
||||||
|
className="group/collapsible"
|
||||||
|
>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
{isSingle ? (
|
||||||
|
<SidebarMenuButton
|
||||||
|
asChild
|
||||||
|
tooltip={item.title}
|
||||||
|
className={cn(isActive && "bg-border")}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={item.url}
|
||||||
|
className="flex w-full items-center gap-2"
|
||||||
|
>
|
||||||
|
{item.icon && (
|
||||||
|
<item.icon
|
||||||
|
className={cn(isActive && "text-primary")}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarMenuButton>
|
<span>{item.title}</span>
|
||||||
</CollapsibleTrigger>
|
</Link>
|
||||||
<CollapsibleContent>
|
</SidebarMenuButton>
|
||||||
<SidebarMenuSub>
|
) : (
|
||||||
{item.items?.map((subItem) => (
|
<>
|
||||||
<SidebarMenuSubItem key={subItem.title}>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuButton
|
||||||
asChild
|
tooltip={item.title}
|
||||||
className={cn(
|
isActive={isActive}
|
||||||
isActiveRoute(subItem.url) && "bg-border",
|
>
|
||||||
)}
|
{item.icon && <item.icon />}
|
||||||
>
|
|
||||||
<Link
|
<span>{item.title}</span>
|
||||||
href={subItem.url}
|
{item.items?.length && (
|
||||||
className="flex w-full items-center"
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
|
)}
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{item.items?.map((subItem) => (
|
||||||
|
<SidebarMenuSubItem key={subItem.title}>
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
asChild
|
||||||
|
className={cn(isActive && "bg-border")}
|
||||||
>
|
>
|
||||||
{subItem.icon && (
|
<Link
|
||||||
<span className="mr-2">
|
href={subItem.url}
|
||||||
<subItem.icon
|
className="flex w-full items-center"
|
||||||
className={cn(
|
>
|
||||||
"h-4 w-4 text-muted-foreground",
|
{subItem.icon && (
|
||||||
isActiveRoute(subItem.url) &&
|
<span className="mr-2">
|
||||||
"text-primary",
|
<subItem.icon
|
||||||
)}
|
className={cn(
|
||||||
/>
|
"h-4 w-4 text-muted-foreground",
|
||||||
</span>
|
isActive && "text-primary",
|
||||||
)}
|
)}
|
||||||
<span>{subItem.title}</span>
|
/>
|
||||||
</Link>
|
</span>
|
||||||
</SidebarMenuSubButton>
|
)}
|
||||||
</SidebarMenuSubItem>
|
<span>{subItem.title}</span>
|
||||||
))}
|
</Link>
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSubButton>
|
||||||
</CollapsibleContent>
|
</SidebarMenuSubItem>
|
||||||
</>
|
))}
|
||||||
)}
|
</SidebarMenuSub>
|
||||||
</SidebarMenuItem>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</>
|
||||||
))}
|
)}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||||
<SidebarGroupLabel>Extra</SidebarGroupLabel>
|
<SidebarGroupLabel>Extra</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{data.help.map((item: ExternalLink) => (
|
{help.map((item: ExternalLink) => (
|
||||||
<SidebarMenuItem key={item.name}>
|
<SidebarMenuItem key={item.name}>
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const Languages = {
|
export const Languages = {
|
||||||
english: { code: "en", name: "English" },
|
english: { code: "en", name: "English" },
|
||||||
polish: { code: "pl", name: "Polski" },
|
polish: { code: "pl", name: "Polski" },
|
||||||
|
ukrainian: { code: "uk", name: "Українська" },
|
||||||
russian: { code: "ru", name: "Русский" },
|
russian: { code: "ru", name: "Русский" },
|
||||||
french: { code: "fr", name: "Français" },
|
french: { code: "fr", name: "Français" },
|
||||||
german: { code: "de", name: "Deutsch" },
|
german: { code: "de", name: "Deutsch" },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.17.5",
|
"version": "v0.17.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default async function handler(
|
|||||||
if (IS_CLOUD && app.serverId) {
|
if (IS_CLOUD && app.serverId) {
|
||||||
jobData.serverId = app.serverId;
|
jobData.serverId = app.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
continue;
|
||||||
}
|
}
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -156,7 +156,7 @@ export default async function handler(
|
|||||||
if (IS_CLOUD && composeApp.serverId) {
|
if (IS_CLOUD && composeApp.serverId) {
|
||||||
jobData.serverId = composeApp.serverId;
|
jobData.serverId = composeApp.serverId;
|
||||||
await deploy(jobData);
|
await deploy(jobData);
|
||||||
return true;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await myQueue.add(
|
await myQueue.add(
|
||||||
|
|||||||
1
apps/dokploy/public/locales/uk/common.json
Normal file
1
apps/dokploy/public/locales/uk/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
58
apps/dokploy/public/locales/uk/settings.json
Normal file
58
apps/dokploy/public/locales/uk/settings.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Зберегти",
|
||||||
|
"settings.common.enterTerminal": "Увійти в термінал",
|
||||||
|
"settings.server.domain.title": "Домен сервера",
|
||||||
|
"settings.server.domain.description": "Додайте домен до вашого серверного застосунку.",
|
||||||
|
"settings.server.domain.form.domain": "Домен",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Електронна пошта для Let's Encrypt",
|
||||||
|
"settings.server.domain.form.certificate.label": "Постачальник сертифікатів",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Оберіть сертифікат",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "Відсутній",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Веб-сервер",
|
||||||
|
"settings.server.webServer.description": "Перезавантажте або очистьте веб-сервер.",
|
||||||
|
"settings.server.webServer.actions": "Дії",
|
||||||
|
"settings.server.webServer.reload": "Перезавантажити",
|
||||||
|
"settings.server.webServer.watchLogs": "Перегляд логів",
|
||||||
|
"settings.server.webServer.updateServerIp": "Оновити IP-адресу сервера",
|
||||||
|
"settings.server.webServer.server.label": "Сервер",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Змінити середовище",
|
||||||
|
"settings.server.webServer.traefik.managePorts": "Додаткові порти",
|
||||||
|
"settings.server.webServer.traefik.managePortsDescription": "Додайте або видаліть порти для Traefik",
|
||||||
|
"settings.server.webServer.traefik.targetPort": "Цільовий порт",
|
||||||
|
"settings.server.webServer.traefik.publishedPort": "Опублікований порт",
|
||||||
|
"settings.server.webServer.traefik.addPort": "Додати порт",
|
||||||
|
"settings.server.webServer.traefik.portsUpdated": "Порти успішно оновлено",
|
||||||
|
"settings.server.webServer.traefik.portsUpdateError": "Не вдалося оновити порти",
|
||||||
|
"settings.server.webServer.traefik.publishMode": "Режим публікації",
|
||||||
|
"settings.server.webServer.storage.label": "Дисковий простір",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Очистити невикористані образи",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Очистити невикористані томи",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Очистити зупинені контейнери",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Очистити Docker Builder і систему",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Очистити моніторинг",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Очистити все",
|
||||||
|
|
||||||
|
"settings.profile.title": "Обліковий запис",
|
||||||
|
"settings.profile.description": "Змініть дані вашого профілю.",
|
||||||
|
"settings.profile.email": "Електронна пошта",
|
||||||
|
"settings.profile.password": "Пароль",
|
||||||
|
"settings.profile.avatar": "Аватар",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Зовнішній вигляд",
|
||||||
|
"settings.appearance.description": "Налаштуйте тему вашої панелі керування.",
|
||||||
|
"settings.appearance.theme": "Тема",
|
||||||
|
"settings.appearance.themeDescription": "Оберіть тему для вашої панелі керування",
|
||||||
|
"settings.appearance.themes.light": "Світла",
|
||||||
|
"settings.appearance.themes.dark": "Темна",
|
||||||
|
"settings.appearance.themes.system": "Системна",
|
||||||
|
"settings.appearance.language": "Мова",
|
||||||
|
"settings.appearance.languageDescription": "Оберіть мову для вашої панелі керування",
|
||||||
|
|
||||||
|
"settings.terminal.connectionSettings": "Налаштування з'єднання",
|
||||||
|
"settings.terminal.ipAddress": "IP-адреса",
|
||||||
|
"settings.terminal.port": "Порт",
|
||||||
|
"settings.terminal.username": "Ім'я користувача"
|
||||||
|
}
|
||||||
9
apps/dokploy/public/templates/superset.svg
Normal file
9
apps/dokploy/public/templates/superset.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="256px" height="128px" viewBox="0 0 256 128" version="1.1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
|
||||||
|
<title>Superset</title>
|
||||||
|
<g>
|
||||||
|
<path d="M190.218924,0 C168.269282,0 148.049828,12.3487941 128.508879,33.9252584 C109.307183,12.0095415 88.748476,0 65.7810761,0 C27.7508614,0 0,27.1402067 0,63.67771 C0,100.215213 27.7508614,127.016168 65.7810761,127.016168 C89.1555791,127.016168 107.271667,116.058309 127.491121,94.2104426 C147.03207,116.12616 166.912271,127.084018 190.218924,127.084018 C228.249139,127.016168 256,100.316989 256,63.67771 C256,27.038431 228.249139,0 190.218924,0 Z M66.0524781,88.6806255 C49.9379804,88.6806255 40.3371323,78.0620196 40.3371323,64.0169626 C40.3371323,49.9719056 49.9379804,39.0479724 66.0524781,39.0479724 C79.6225815,39.0479724 90.716141,49.9719056 102.725682,64.6954678 C91.3946462,78.4012722 79.4190299,88.6806255 66.0524781,88.6806255 Z M189.065465,88.6806255 C175.698913,88.6806255 164.401802,78.0620196 152.392261,64.0169626 C164.741055,49.2934005 175.359661,39.0479724 189.065465,39.0479724 C205.179963,39.0479724 214.679035,50.1076067 214.679035,64.0169626 C214.679035,77.9263186 205.179963,88.6806255 189.065465,88.6806255 Z" fill="#484848"></path>
|
||||||
|
<path d="M156.124039,117.958124 L181.703684,87.4253909 C171.526107,84.3721177 162.12881,75.2122979 152.392261,63.8473363 L127.491121,94.2104426 C135.643361,103.668805 145.322237,111.695521 156.124039,117.958124 Z" fill="#20A7C9"></path>
|
||||||
|
<path d="M128.508879,33.8913332 C120.41092,24.2972701 110.793109,16.0907501 100.045587,9.60084813 L74.432017,40.4728333 C84.1685661,43.8653591 92.7855818,52.6180758 101.945402,63.7794858 L102.963159,64.4919162 L128.508879,33.8913332 Z" fill="#20A7C9"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -345,7 +345,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
writeConfig("middlewares", input.traefikConfig);
|
writeConfig("middlewares", input.traefikConfig);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
getUpdateData: adminProcedure.mutation(async () => {
|
getUpdateData: protectedProcedure.mutation(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return DEFAULT_UPDATE_DATA;
|
return DEFAULT_UPDATE_DATA;
|
||||||
}
|
}
|
||||||
@@ -373,10 +373,10 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getDokployVersion: adminProcedure.query(() => {
|
getDokployVersion: protectedProcedure.query(() => {
|
||||||
return packageInfo.version;
|
return packageInfo.version;
|
||||||
}),
|
}),
|
||||||
getReleaseTag: adminProcedure.query(() => {
|
getReleaseTag: protectedProcedure.query(() => {
|
||||||
return getDokployImageTag();
|
return getDokployImageTag();
|
||||||
}),
|
}),
|
||||||
readDirectories: protectedProcedure
|
readDirectories: protectedProcedure
|
||||||
|
|||||||
62
apps/dokploy/templates/superset/docker-compose.yml
Normal file
62
apps/dokploy/templates/superset/docker-compose.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Note: this is an UNOFFICIAL production docker image build for Superset:
|
||||||
|
# - https://github.com/amancevice/docker-superset
|
||||||
|
#
|
||||||
|
# After deploying this image, you will need to run one of the two
|
||||||
|
# commands below in a terminal within the superset container:
|
||||||
|
# $ superset-demo # Initialise database + load demo charts/datasets
|
||||||
|
# $ superset-init # Initialise database only
|
||||||
|
#
|
||||||
|
# You will be prompted to enter the credentials for the admin user.
|
||||||
|
|
||||||
|
services:
|
||||||
|
superset:
|
||||||
|
image: amancevice/superset
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
- redis
|
||||||
|
environment:
|
||||||
|
SECRET_KEY: ${SECRET_KEY}
|
||||||
|
MAPBOX_API_KEY: ${MAPBOX_API_KEY}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
# Note: superset_config.py can be edited in Dokploy's UI Volume Mount
|
||||||
|
- ../files/superset/superset_config.py:/etc/superset/superset_config.py
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
volumes:
|
||||||
|
- postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- redis:/data
|
||||||
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres:
|
||||||
|
redis:
|
||||||
67
apps/dokploy/templates/superset/index.ts
Normal file
67
apps/dokploy/templates/superset/index.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
type DomainSchema,
|
||||||
|
type Schema,
|
||||||
|
type Template,
|
||||||
|
generatePassword,
|
||||||
|
generateRandomDomain,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
export function generate(schema: Schema): Template {
|
||||||
|
const mapboxApiKey = "";
|
||||||
|
const secretKey = generatePassword(30);
|
||||||
|
const postgresDb = "superset";
|
||||||
|
const postgresUser = "superset";
|
||||||
|
const postgresPassword = generatePassword(30);
|
||||||
|
const redisPassword = generatePassword(30);
|
||||||
|
|
||||||
|
const domains: DomainSchema[] = [
|
||||||
|
{
|
||||||
|
host: generateRandomDomain(schema),
|
||||||
|
port: 8088,
|
||||||
|
serviceName: "superset",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const envs = [
|
||||||
|
`SECRET_KEY=${secretKey}`,
|
||||||
|
`MAPBOX_API_KEY=${mapboxApiKey}`,
|
||||||
|
`POSTGRES_DB=${postgresDb}`,
|
||||||
|
`POSTGRES_USER=${postgresUser}`,
|
||||||
|
`POSTGRES_PASSWORD=${postgresPassword}`,
|
||||||
|
`REDIS_PASSWORD=${redisPassword}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
const mounts: Template["mounts"] = [
|
||||||
|
{
|
||||||
|
filePath: "./superset/superset_config.py",
|
||||||
|
content: `
|
||||||
|
import os
|
||||||
|
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
|
MAPBOX_API_KEY = os.getenv("MAPBOX_API_KEY", "")
|
||||||
|
|
||||||
|
CACHE_CONFIG = {
|
||||||
|
"CACHE_TYPE": "RedisCache",
|
||||||
|
"CACHE_DEFAULT_TIMEOUT": 300,
|
||||||
|
"CACHE_KEY_PREFIX": "superset_",
|
||||||
|
"CACHE_REDIS_HOST": "redis",
|
||||||
|
"CACHE_REDIS_PORT": 6379,
|
||||||
|
"CACHE_REDIS_DB": 1,
|
||||||
|
"CACHE_REDIS_URL": f"redis://:{os.getenv('REDIS_PASSWORD')}@redis:6379/1",
|
||||||
|
}
|
||||||
|
|
||||||
|
FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"}
|
||||||
|
EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"}
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@db:5432/{os.getenv('POSTGRES_DB')}"
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
`.trim(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
envs,
|
||||||
|
domains,
|
||||||
|
mounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1298,4 +1298,18 @@ export const templates: TemplateData[] = [
|
|||||||
tags: ["developer", "tools"],
|
tags: ["developer", "tools"],
|
||||||
load: () => import("./it-tools/index").then((m) => m.generate),
|
load: () => import("./it-tools/index").then((m) => m.generate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "superset",
|
||||||
|
name: "Superset (Unofficial)",
|
||||||
|
version: "latest",
|
||||||
|
description: "Data visualization and data exploration platform.",
|
||||||
|
logo: "superset.svg",
|
||||||
|
links: {
|
||||||
|
github: "https://github.com/amancevice/docker-superset",
|
||||||
|
website: "https://superset.apache.org",
|
||||||
|
docs: "https://superset.apache.org/docs/intro",
|
||||||
|
},
|
||||||
|
tags: ["analytics", "bi", "dashboard", "database", "sql"],
|
||||||
|
load: () => import("./superset/index").then((m) => m.generate),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user