Merge pull request #1168 from Dokploy/canary

🚀 Release v0.17.6
This commit is contained in:
Mauricio Siu
2025-01-22 00:39:41 -06:00
committed by GitHub
14 changed files with 683 additions and 345 deletions

View File

@@ -100,7 +100,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
{mount.type === "file" && (
<div className="flex flex-col gap-1">
<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}
</span>
</div>
@@ -113,12 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</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>
{mount.type === "file" ? (
<div className="flex flex-col gap-1">
<span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground">
{mount.filePath}
</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 className="flex flex-row gap-1">
<UpdateVolume

View File

@@ -118,7 +118,7 @@ export const HandleProject = ({ projectId }: Props) => {
</DialogTrigger>
<DialogContent className="sm:m:max-w-lg ">
<DialogHeader>
<DialogTitle>Add a project</DialogTitle>
<DialogTitle>{projectId ? "Update" : "Add a"} project</DialogTitle>
<DialogDescription>The home of something big!</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}

View File

@@ -87,9 +87,12 @@ export const ShowProjects = () => {
Create and manage your projects
</CardDescription>
</CardHeader>
<div className="">
<HandleProject />
</div>
{(auth?.rol === "admin" || user?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>
)}
</div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">

View File

@@ -1,7 +1,6 @@
"use client";
import {
Activity,
AudioWaveform,
BarChartHorizontalBigIcon,
Bell,
BlocksIcon,
@@ -9,7 +8,6 @@ import {
Boxes,
ChevronRight,
CircleHelp,
Command,
CreditCard,
Database,
Folder,
@@ -27,8 +25,8 @@ import {
Users,
} from "lucide-react";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import type * as React from "react";
import { useEffect, useState } from "react";
import {
Breadcrumb,
@@ -65,243 +63,290 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { cn } from "@/lib/utils";
import type { AppRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import type { inferRouterOutputs } from "@trpc/server";
import Link from "next/link";
import { useRouter } from "next/router";
import { Logo } from "../shared/logo";
import { UpdateServerButton } from "./update-server";
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;
url: string;
icon: LucideIcon;
isSingle: boolean;
isActive: boolean;
items?: {
title: string;
url: string;
icon?: LucideIcon;
}[];
}
icon?: LucideIcon;
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
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;
url: string;
icon: React.ComponentType<{ className?: string }>;
}
isEnabled?: (opts: {
auth?: AuthQueryOutput;
user?: UserQueryOutput;
isCloud: boolean;
}) => boolean;
};
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "/avatars/shadcn.jpg",
},
teams: [
{
name: "Dokploy",
logo: Logo,
plan: "Enterprise",
},
{
name: "Acme Corp.",
logo: AudioWaveform,
plan: "Startup",
},
{
name: "Evil Corp.",
logo: Command,
plan: "Free",
},
],
// Menu type
// Consists of home, settings, and help items
type Menu = {
home: NavItem[];
settings: NavItem[];
help: ExternalLink[];
};
// Menu items
// Consists of unfiltered home, settings, and help items
// 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 = {
home: [
{
isSingle: true,
title: "Projects",
url: "/dashboard/projects",
icon: Folder,
isSingle: true,
isActive: false,
},
{
isSingle: true,
title: "Monitoring",
url: "/dashboard/monitoring",
icon: BarChartHorizontalBigIcon,
isSingle: true,
isActive: false,
// Only enabled in non-cloud environments
isEnabled: ({ auth, user, isCloud }) => !isCloud,
},
{
isSingle: true,
title: "Traefik File System",
url: "/dashboard/traefik",
icon: GalleryVerticalEnd,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Traefik files in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(
(auth?.rol === "admin" || user?.canAccessToTraefikFiles) &&
!isCloud
),
},
{
isSingle: true,
title: "Docker",
url: "/dashboard/docker",
icon: BlocksIcon,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
{
isSingle: true,
title: "Swarm",
url: "/dashboard/swarm",
icon: PieChart,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
{
isSingle: true,
title: "Requests",
url: "/dashboard/requests",
icon: Forward,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Docker in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!((auth?.rol === "admin" || user?.canAccessToDocker) && !isCloud),
},
// Legacy unused menu, adjusted to the new structure
// {
// isSingle: true,
// title: "Projects",
// url: "/dashboard/projects",
// icon: Folder,
// isSingle: true,
// },
// {
// isSingle: true,
// title: "Monitoring",
// icon: BarChartHorizontalBigIcon,
// url: "/dashboard/settings/monitoring",
// isSingle: true,
// },
// {
// title: "Settings",
// url: "#",
// icon: Settings2,
// isActive: true,
// items: [
// {
// title: "Profile",
// url: "/dashboard/settings/profile",
// },
// {
// title: "Users",
// url: "/dashboard/settings/users",
// },
// {
// title: "SSH Key",
// url: "/dashboard/settings/ssh-keys",
// },
// {
// title: "Git",
// url: "/dashboard/settings/git-providers",
// },
// ],
// isSingle: false,
// title: "Settings",
// icon: Settings2,
// items: [
// {
// title: "Profile",
// url: "/dashboard/settings/profile",
// },
// {
// title: "Users",
// url: "/dashboard/settings/users",
// },
// {
// title: "SSH Key",
// url: "/dashboard/settings/ssh-keys",
// },
// {
// title: "Git",
// url: "/dashboard/settings/git-providers",
// },
// ],
// },
// {
// title: "Integrations",
// icon: BlocksIcon,
// items: [
// {
// title: "S3 Destinations",
// url: "/dashboard/settings/destinations",
// },
// {
// title: "Registry",
// url: "/dashboard/settings/registry",
// },
// {
// title: "Notifications",
// url: "/dashboard/settings/notifications",
// },
// ],
] as NavItem[],
// isSingle: false,
// title: "Integrations",
// icon: BlocksIcon,
// items: [
// {
// title: "S3 Destinations",
// url: "/dashboard/settings/destinations",
// },
// {
// title: "Registry",
// url: "/dashboard/settings/registry",
// },
// {
// title: "Notifications",
// url: "/dashboard/settings/notifications",
// },
// ],
// },
],
settings: [
{
isSingle: true,
title: "Server",
url: "/dashboard/settings/server",
icon: Activity,
isSingle: true,
isActive: false,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
},
{
isSingle: true,
title: "Profile",
url: "/dashboard/settings/profile",
icon: User,
isSingle: true,
isActive: false,
},
{
isSingle: true,
title: "Servers",
url: "/dashboard/settings/servers",
icon: Server,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Users",
icon: Users,
url: "/dashboard/settings/users",
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "SSH Keys",
icon: KeyRound,
url: "/dashboard/settings/ssh-keys",
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to SSH keys
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToSSHKeys),
},
{
isSingle: true,
title: "Git",
url: "/dashboard/settings/git-providers",
icon: GitBranch,
isSingle: true,
isActive: false,
// Only enabled for admins and users with access to Git providers
isEnabled: ({ auth, user }) =>
!!(auth?.rol === "admin" || user?.canAccessToGitProviders),
},
{
isSingle: true,
title: "Registry",
url: "/dashboard/settings/registry",
icon: Package,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "S3 Destinations",
url: "/dashboard/settings/destinations",
icon: Database,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Certificates",
url: "/dashboard/settings/certificates",
icon: ShieldCheck,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Cluster",
url: "/dashboard/settings/cluster",
icon: Boxes,
isSingle: true,
isActive: false,
// Only enabled for admins in non-cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && !isCloud),
},
{
isSingle: true,
title: "Notifications",
url: "/dashboard/settings/notifications",
icon: Bell,
isSingle: true,
isActive: false,
// Only enabled for admins
isEnabled: ({ auth, user, isCloud }) => !!(auth?.rol === "admin"),
},
{
isSingle: true,
title: "Billing",
url: "/dashboard/settings/billing",
icon: CreditCard,
isSingle: true,
isActive: false,
// Only enabled for admins in cloud environments
isEnabled: ({ auth, user, isCloud }) =>
!!(auth?.rol === "admin" && isCloud),
},
] as NavItem[],
],
help: [
{
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 {
children: React.ReactNode;
@@ -398,64 +543,21 @@ export default function Page({ children }: Props) {
const includesProjects = pathname?.includes("/dashboard/project");
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 nextChar = normalizedPathname.charAt(normalizedItemUrl.length);
return nextChar === "/";
}
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);
// const showProjectsButton =
// currentPath === "/dashboard/projects" &&
// (auth?.rol === "admin" || user?.canCreateProjects);
return (
<SidebarProvider
@@ -486,173 +588,185 @@ export default function Page({ children }: Props) {
<SidebarGroup>
<SidebarGroupLabel>Home</SidebarGroupLabel>
<SidebarMenu>
{filteredHome.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<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 />}
{filteredHome.map((item) => {
const isSingle = item.isSingle !== false;
const isActive = isSingle
? isActiveRoute({ itemUrl: item.url, pathname })
: item.items.some((item) =>
isActiveRoute({ itemUrl: item.url, pathname }),
);
<span>{item.title}</span>
{item.items?.length && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
return (
<Collapsible
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>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
className={cn(
isActiveRoute(subItem.url) && "bg-border",
)}
>
<Link
href={subItem.url}
className="flex w-full items-center"
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
) : (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={isActive}
>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.items?.length && (
<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 && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActiveRoute(subItem.url) &&
"text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
))}
<Link
href={subItem.url}
className="flex w-full items-center"
>
{subItem.icon && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActive && "text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
);
})}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup>
<SidebarGroupLabel>Settings</SidebarGroupLabel>
<SidebarMenu className="gap-2">
{filteredSettings.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<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 />}
{filteredSettings.map((item) => {
const isSingle = item.isSingle !== false;
const isActive = isSingle
? isActiveRoute({ itemUrl: item.url, pathname })
: item.items.some((item) =>
isActiveRoute({ itemUrl: item.url, pathname }),
);
<span>{item.title}</span>
{item.items?.length && (
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
return (
<Collapsible
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>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton
asChild
className={cn(
isActiveRoute(subItem.url) && "bg-border",
)}
>
<Link
href={subItem.url}
className="flex w-full items-center"
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
) : (
<>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={isActive}
>
{item.icon && <item.icon />}
<span>{item.title}</span>
{item.items?.length && (
<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 && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActiveRoute(subItem.url) &&
"text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
))}
<Link
href={subItem.url}
className="flex w-full items-center"
>
{subItem.icon && (
<span className="mr-2">
<subItem.icon
className={cn(
"h-4 w-4 text-muted-foreground",
isActive && "text-primary",
)}
/>
</span>
)}
<span>{subItem.title}</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</>
)}
</SidebarMenuItem>
</Collapsible>
);
})}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Extra</SidebarGroupLabel>
<SidebarMenu>
{data.help.map((item: ExternalLink) => (
{help.map((item: ExternalLink) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a

View File

@@ -1,6 +1,7 @@
export const Languages = {
english: { code: "en", name: "English" },
polish: { code: "pl", name: "Polski" },
ukrainian: { code: "uk", name: "Українська" },
russian: { code: "ru", name: "Русский" },
french: { code: "fr", name: "Français" },
german: { code: "de", name: "Deutsch" },

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.17.5",
"version": "v0.17.6",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -121,7 +121,7 @@ export default async function handler(
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(
"deployments",
@@ -156,7 +156,7 @@ export default async function handler(
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
continue;
}
await myQueue.add(

View File

@@ -0,0 +1 @@
{}

View 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": "Ім'я користувача"
}

View 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

View File

@@ -345,7 +345,7 @@ export const settingsRouter = createTRPCRouter({
writeConfig("middlewares", input.traefikConfig);
return true;
}),
getUpdateData: adminProcedure.mutation(async () => {
getUpdateData: protectedProcedure.mutation(async () => {
if (IS_CLOUD) {
return DEFAULT_UPDATE_DATA;
}
@@ -373,10 +373,10 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
getDokployVersion: adminProcedure.query(() => {
getDokployVersion: protectedProcedure.query(() => {
return packageInfo.version;
}),
getReleaseTag: adminProcedure.query(() => {
getReleaseTag: protectedProcedure.query(() => {
return getDokployImageTag();
}),
readDirectories: protectedProcedure

View 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:

View 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,
};
}

View File

@@ -1298,4 +1298,18 @@ export const templates: TemplateData[] = [
tags: ["developer", "tools"],
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),
},
];