diff --git a/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts index 3138085e3d..bb6f5f18b0 100644 --- a/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts +++ b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts @@ -1,8 +1,8 @@ -import { describe, it, expect } from "vitest"; import { enterpriseOnlyResources, statements, } from "@dokploy/server/lib/access-control"; +import { describe, expect, it } from "vitest"; const FREE_TIER_RESOURCES = [ "organization", diff --git a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx index 7b1614fda6..831710e31e 100644 --- a/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/import/show-import.tsx @@ -187,7 +187,7 @@ export const ShowImport = ({ composeId }: Props) => { - + Template Information @@ -322,7 +322,7 @@ export const ShowImport = ({ composeId }: Props) => { - + {selectedMount?.filePath} Mount File Content diff --git a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx index bfd4b99dcc..49237b79ff 100644 --- a/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/volumes/add-volumes.tsx @@ -155,7 +155,7 @@ export const AddVolumes = ({ return ( - + diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 3cecef1ec7..ccf2564b06 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -1,3 +1,4 @@ +import copy from "copy-to-clipboard"; import { ChevronDown, ChevronUp, @@ -11,7 +12,6 @@ import { } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; -import copy from "copy-to-clipboard"; import { AlertBlock } from "@/components/shared/alert-block"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { DialogAction } from "@/components/shared/dialog-action"; diff --git a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx index 00eb622727..86fd1c54d3 100644 --- a/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/handle-domain.tsx @@ -297,9 +297,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => { }; return ( - - {children} - + {children} Domain diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 06428ae217..d16a008a57 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -193,7 +193,7 @@ export const ShowDomains = ({ id, type }: Props) => { return (
diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx index 86f7a0dff0..1f3cc8705a 100644 --- a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx +++ b/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx @@ -168,7 +168,7 @@ export const ShowEnvironment = ({ id, type }: Props) => { name="environment" render={({ field }) => ( - + { if (isLoading) { return ( - +
@@ -106,7 +106,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => { application.sourceType !== "drop" ) { return ( - +
@@ -131,7 +131,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => { } return ( - +
diff --git a/apps/dokploy/components/dashboard/application/general/show.tsx b/apps/dokploy/components/dashboard/application/general/show.tsx index 01fc9e84ad..ef4cab4808 100644 --- a/apps/dokploy/components/dashboard/application/general/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/show.tsx @@ -54,11 +54,11 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => { return ( <> - + Deploy Settings - + {canDeploy && ( { > {canUpdateService && ( -
- Autodeploy +
+ + Autodeploy + { toast.error("Error updating Auto Deploy"); }); }} - className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" + size="sm" />
)} {canUpdateService && ( -
- Clean Cache +
+ + Clean Cache + { toast.error("Error updating Clean Cache"); }); }} - className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" + size="sm" />
)} diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index cbb6bce099..aa8ad83ff0 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -2,13 +2,6 @@ import { Loader2 } from "lucide-react"; import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Select, @@ -95,95 +88,84 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => { option === "native" ? containers?.length : services?.length; return ( - - - Logs - - Watch the logs of the application in real time - - - - -
- -
- - {option === "native" ? "Native" : "Swarm"} - - { - setOption(checked ? "native" : "swarm"); - }} - /> -
+
+
+ +
+ + {option === "native" ? "Native" : "Swarm"} + + { + setOption(checked ? "native" : "swarm"); + }} + />
+
- + + {isLoading ? ( +
+ Loading... + +
+ ) : ( + + )} +
+ + + {option === "native" ? ( +
+ {containers?.map((container) => ( + + {container.name} ({container.containerId}){" "} + + {container.state} + + {container.status ? ` ${container.status}` : ""} + + ))}
) : ( - + <> + {services?.map((container) => ( + + {container.name} ({container.containerId}@{container.node}) + + {container.state} + + {container.currentState ? ` ${container.currentState}` : ""} + + ))} + )} - - - - {option === "native" ? ( -
- {containers?.map((container) => ( - - {container.name} ({container.containerId}){" "} - - {container.state} - - {container.status ? ` ${container.status}` : ""} - - ))} -
- ) : ( - <> - {services?.map((container) => ( - - {container.name} ({container.containerId}@{container.node} - ) - - {container.state} - - {container.currentState - ? ` ${container.currentState}` - : ""} - - ))} - - )} - Containers ({containersLenght}) -
-
- - {option === "swarm" && - services?.find((c) => c.containerId === containerId)?.error && ( -
- Error: - {services?.find((c) => c.containerId === containerId)?.error} -
- )} - - - + Containers ({containersLenght}) +
+
+ + + {option === "swarm" && + services?.find((c) => c.containerId === containerId)?.error && ( +
+ Error: + {services?.find((c) => c.containerId === containerId)?.error} +
+ )} + +
); }; diff --git a/apps/dokploy/components/dashboard/application/patches/index.ts b/apps/dokploy/components/dashboard/application/patches/index.ts index 1854bd3e58..053e644b77 100644 --- a/apps/dokploy/components/dashboard/application/patches/index.ts +++ b/apps/dokploy/components/dashboard/application/patches/index.ts @@ -1,2 +1,2 @@ -export * from "./show-patches"; export * from "./patch-editor"; +export * from "./show-patches"; diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx index 72815fd8fa..57e9392e23 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx @@ -137,9 +137,7 @@ export const AddPreviewDomain = ({ }; return ( - - {children} - + {children} Domain diff --git a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx index a9550fda2f..30c8d01aac 100644 --- a/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx +++ b/apps/dokploy/components/dashboard/application/schedules/show-schedules.tsx @@ -11,13 +11,6 @@ import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Tooltip, TooltipContent, @@ -73,23 +66,8 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => { }; return ( - - -
-
- - Scheduled Tasks - - - Schedule tasks to run automatically at specified intervals. - -
- {schedules && schedules.length > 0 && ( - - )} -
-
- +
+
{isLoadingSchedules ? (
@@ -240,7 +218,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
)} - - +
+
); }; diff --git a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx index 684620947f..db3bf0a80a 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/restore-volume-backups.tsx @@ -173,7 +173,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { control={form.control} name="destinationId" render={({ field }) => ( - + Destination @@ -239,7 +239,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => { control={form.control} name="backupFile" render={({ field }) => ( - + Search Backup Files {field.value && ( diff --git a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx index 526bcfa770..705b920778 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/show-volume-backups.tsx @@ -81,7 +81,7 @@ export const ShowVolumeBackups = ({
- + Volume Backups diff --git a/apps/dokploy/components/dashboard/compose/general/actions.tsx b/apps/dokploy/components/dashboard/compose/general/actions.tsx index d04725e263..53af2397de 100644 --- a/apps/dokploy/components/dashboard/compose/general/actions.tsx +++ b/apps/dokploy/components/dashboard/compose/general/actions.tsx @@ -36,7 +36,7 @@ export const ComposeActions = ({ composeId }: Props) => { const { mutateAsync: stop, isPending: isStopping } = api.compose.stop.useMutation(); return ( -
+
{canDeploy && ( { > {canUpdateService && ( -
- Autodeploy +
+ + Autodeploy + { toast.error("Error updating Auto Deploy"); }); }} - className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary" + size="sm" />
)} diff --git a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx index 28f958e3ea..0ef03ece2e 100644 --- a/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx +++ b/apps/dokploy/components/dashboard/compose/general/compose-file-editor.tsx @@ -134,7 +134,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => { name="composeFile" render={({ field }) => ( - +
{ if (isLoading) { return ( - +
@@ -94,7 +94,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => { compose.sourceType !== "raw" ) { return ( - +
@@ -119,7 +119,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => { } return ( - +
diff --git a/apps/dokploy/components/dashboard/compose/general/show.tsx b/apps/dokploy/components/dashboard/compose/general/show.tsx index 4199363d89..8cc3615762 100644 --- a/apps/dokploy/components/dashboard/compose/general/show.tsx +++ b/apps/dokploy/components/dashboard/compose/general/show.tsx @@ -24,7 +24,7 @@ export const ShowGeneralCompose = ({ composeId }: Props) => { return ( <> - +
Deploy Settings diff --git a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx index 26880e9b53..9da3be9a21 100644 --- a/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/handle-backup.tsx @@ -401,7 +401,7 @@ export const HandleBackup = ({ control={form.control} name="destinationId" render={({ field }) => ( - + Destination diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 00647aea74..69a8e8c1ed 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -338,7 +338,7 @@ export const RestoreBackup = ({ control={form.control} name="destinationId" render={({ field }) => ( - + Destination @@ -404,7 +404,7 @@ export const RestoreBackup = ({ control={form.control} name="backupFile" render={({ field }) => ( - + Search Backup Files {field.value && ( diff --git a/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx b/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx index 770d4efd05..8472e8315e 100644 --- a/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx +++ b/apps/dokploy/components/dashboard/deployments/show-deployments-table.tsx @@ -20,13 +20,13 @@ import { ExternalLink, Loader2, Rocket, + Search, Server, } from "lucide-react"; import Link from "next/link"; import { useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -452,14 +452,17 @@ export function ShowDeploymentsTable() { }); return ( -
+
- setGlobalFilter(e.target.value)} - className="max-w-xs" - /> +
+ + setGlobalFilter(e.target.value)} + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+
+ + + + +
+ + {isPaused && ( +
+ + + Logs paused + {messageBuffer.length > 0 && ( + + ({messageBuffer.length} messages buffered) + + )} + +
+ )} + + {/* Log viewer with filter panel */} +
+ {/* Filter panel */} +
+
+
+

Filters

+ {activeFilterCount > 0 && ( + + )} +
+ +
+

+ Lines +

+
+
+

+ Time range +

+
+
+

+ Log type +

+
+
+
- + {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : isLoading ? ( +
+
- -
- - - + ) : ( +
+ No logs found
-
- {isPaused && ( - -
- - - Logs paused - {messageBuffer.length > 0 && ( - - ({messageBuffer.length} messages buffered) - - )} - -
-
)} -
- {filteredLogs.length > 0 ? ( - filteredLogs.map((filteredLog: LogLine, index: number) => ( - - )) - ) : isLoading ? ( -
- -
- ) : ( -
- No logs found -
- )} -
diff --git a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx index 69b0a0da2c..224cc4dd2b 100644 --- a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx +++ b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx @@ -9,23 +9,15 @@ import { useReactTable, type VisibilityState, } from "@tanstack/react-table"; -import { ChevronDown, Container } from "lucide-react"; +import { ChevronDown, Search } from "lucide-react"; import * as React from "react"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; import { Table, TableBody, @@ -77,164 +69,140 @@ export const ShowContainers = ({ serverId }: Props) => { }); return ( -
- -
- - - - Docker Containers - - - See all the containers of your dokploy server - - - -
-
-
- - table - .getColumn("name") - ?.setFilterValue(event.target.value) +
+
+
+ + + table.getColumn("name")?.setFilterValue(event.target.value) + } + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" + /> +
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) } - className="md:max-w-sm" - /> - - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - -
-
- {isPending ? ( -
- - Loading... - -
- ) : data?.length === 0 ? ( -
- - No results. - -
- ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table?.getRowModel()?.rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - {isPending ? ( -
- - Loading... - -
- ) : ( - <>No results. - )} -
-
+ > + {column.id} + + ); + })} + + + +
+ {isPending ? ( +
+ + Loading... + +
+ ) : data?.length === 0 ? ( +
+ + No results. + +
+ ) : ( +
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table?.getRowModel()?.rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), )} - -
- )} -
- {data && data?.length > 0 && ( -
-
- - -
-
- )} -
-
- + + ))} + + )) + ) : ( + + + {isPending ? ( +
+ + Loading... + +
+ ) : ( + <>No results. + )} +
+
+ )} + + + )} +
+ {data && data?.length > 0 && ( +
+
+ + +
- + )}
); }; diff --git a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx index 94a5c72a6d..b8c80f4e25 100644 --- a/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx +++ b/apps/dokploy/components/dashboard/file-system/show-traefik-system.tsx @@ -1,13 +1,6 @@ import { FileIcon, Folder, Loader2, Workflow } from "lucide-react"; import React from "react"; import { AlertBlock } from "@/components/shared/alert-block"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Tree } from "@/components/ui/file-tree"; import { api } from "@/utils/api"; import { ShowTraefikFile } from "./show-traefik-file"; @@ -34,76 +27,60 @@ export const ShowTraefikSystem = ({ serverId }: Props) => { return (
- -
- - - - Traefik File System - - - Manage all the files and directories in {"'/etc/dokploy/traefik'"} - . - - - - Adding invalid configuration to existing files, can break your - Traefik instance, preventing access to your applications. - - - -
-
- {isError && ( - - {error?.message} - - )} - {isLoading && ( -
- - Loading... - - -
- )} - {directories?.length === 0 && ( -
- - No directories or files detected in{" "} - {"'/etc/dokploy/traefik'"} - - -
- )} - {directories && directories?.length > 0 && ( - <> - setFile(item?.id || null)} - folderIcon={Folder} - itemIcon={Workflow} - /> -
- {file ? ( - - ) : ( -
- - No file selected - - -
- )} -
- - )} + + Adding invalid configuration to existing files, can break your Traefik + instance, preventing access to your applications. + +
+
+
+ {isError && ( + + {error?.message} + + )} + {isLoading && ( +
+ + Loading... + +
-
- + )} + {directories?.length === 0 && ( +
+ + No directories or files detected in {"'/etc/dokploy/traefik'"} + + +
+ )} + {directories && directories?.length > 0 && ( + <> + setFile(item?.id || null)} + folderIcon={Folder} + itemIcon={Workflow} + /> +
+ {file ? ( + + ) : ( +
+ + No file selected + + +
+ )} +
+ + )} +
- +
); }; diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx index 42bb361bb2..43dd8e2c53 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx @@ -201,16 +201,7 @@ export const ContainerFreeMonitoring = ({ }, [appName]); return ( -
-
-
-

Monitoring

-

- Watch the usage of your server in the current app -

-
-
- +
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx index 32e30f62ab..04e123f1c5 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-block-chart.tsx @@ -115,7 +115,7 @@ export const ContainerBlockChart = ({ data }: Props) => { if (active && payload && payload.length) { const data = payload?.[0]?.payload; return ( -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx index 76b010c7c7..ebba3793a0 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-cpu-chart.tsx @@ -84,7 +84,7 @@ export const ContainerCPUChart = ({ data }: Props) => { if (active && payload && payload.length) { const data = payload?.[0]?.payload; return ( -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx index ff5e858432..ac0e34c300 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-memory-chart.tsx @@ -99,7 +99,7 @@ export const ContainerMemoryChart = ({ data }: Props) => { if (active && payload && payload.length) { const data = payload?.[0]?.payload; return ( -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx index f962e2ae36..c4518e6a72 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/container-network-chart.tsx @@ -122,7 +122,7 @@ export const ContainerNetworkChart = ({ data }: Props) => { if (active && payload && payload.length) { const data = payload?.[0]?.payload; return ( -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx index db087afa0e..7b76e35e7b 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx @@ -132,7 +132,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { return ( <>
-

+

Container Monitoring

@@ -189,7 +189,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

CPU Usage

-

{metrics.CPU}%

+

{metrics.CPU}%

@@ -197,7 +197,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Memory Usage

-

+

{metrics?.Memory?.percentage}%

@@ -211,7 +211,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Network I/O

-

+

{metrics?.Network?.input} {metrics?.Network?.inputUnit} /{" "} {metrics?.Network?.output} {metrics?.Network?.outputUnit}

@@ -222,7 +222,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Block I/O

-

+

{metrics?.BlockIO?.read} {metrics?.BlockIO?.readUnit} /{" "} {metrics?.BlockIO?.write} {metrics?.BlockIO?.writeUnit}

diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx index efa84ffc48..46aa4ea77b 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/cpu-chart.tsx @@ -71,7 +71,7 @@ export function CPUChart({ data }: CPUChartProps) { if (active && payload && payload.length) { const data = payload?.[0]?.payload; return ( -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx index 1981dace32..b469adc508 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/memory-chart.tsx @@ -86,7 +86,7 @@ export function MemoryChart({ data }: MemoryChartProps) { if (active && payload && payload.length) { const data = payload?.[0]?.payload; return ( -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx index bbb522fdc3..6af0ac7a4d 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/network-chart.tsx @@ -90,7 +90,7 @@ export function NetworkChart({ data }: NetworkChartProps) { if (active && payload && payload.length) { const data = payload?.[0]?.payload; return ( -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx index af0dacc1d9..2bfce74cae 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx @@ -150,9 +150,8 @@ export const ShowPaidMonitoring = ({ } return ( -
-
-

System Monitoring

+
+
Data points: @@ -202,45 +201,45 @@ export const ShowPaidMonitoring = ({ {/* Stats Cards */}
-
+

Uptime

-

+

{formatUptime(metrics.uptime || 0)}

-
+

CPU Usage

-

{metrics.cpu}%

+

{metrics.cpu}%

-
+

Memory Usage

-

+

{metrics.memUsedGB} GB / {metrics.memTotal} GB

-
+

Disk Usage

-

{metrics.diskUsed}%

+

{metrics.diskUsed}%

{/* System Information */} -
+

System Information

diff --git a/apps/dokploy/components/dashboard/organization/handle-organization.tsx b/apps/dokploy/components/dashboard/organization/handle-organization.tsx index ecc3e7c9c4..86158c3738 100644 --- a/apps/dokploy/components/dashboard/organization/handle-organization.tsx +++ b/apps/dokploy/components/dashboard/organization/handle-organization.tsx @@ -151,7 +151,7 @@ export function AddOrganization({ organizationId }: Props) { className="col-span-3" /> - + )} /> diff --git a/apps/dokploy/components/dashboard/project/ai/step-one.tsx b/apps/dokploy/components/dashboard/project/ai/step-one.tsx index 2a7039f6ca..1bb82c1a53 100644 --- a/apps/dokploy/components/dashboard/project/ai/step-one.tsx +++ b/apps/dokploy/components/dashboard/project/ai/step-one.tsx @@ -37,7 +37,7 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => { }; return (
-
+

Step 1: Describe Your Needs

diff --git a/apps/dokploy/components/dashboard/project/ai/step-two.tsx b/apps/dokploy/components/dashboard/project/ai/step-two.tsx index e13ff40ad4..e4c7547edc 100644 --- a/apps/dokploy/components/dashboard/project/ai/step-two.tsx +++ b/apps/dokploy/components/dashboard/project/ai/step-two.tsx @@ -244,7 +244,9 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => { {selectedVariant && ( <>
-

{selectedVariant?.name}

+

+ {selectedVariant?.name} +

{selectedVariant?.shortDescription}

@@ -491,7 +493,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => { )}
-
+
{selectedVariant && ( + + e.stopPropagation()} + > + + Actions + +
e.stopPropagation()}> + +
+
e.stopPropagation()}> + +
- {project.projectTags && - project.projectTags.length > 0 && ( -
- {project.projectTags.map((pt) => ( - - ))} -
+
e.stopPropagation()}> + {permissions?.project.delete && ( + + + + e.preventDefault() + } + > + + Delete + + + + + + Are you sure to delete this + project? + + {!emptyServices ? ( +
+ + + You have active services, + please delete them first + +
+ ) : ( + + This action cannot be undone + + )} +
+ + + Cancel + + { + await mutateAsync({ + projectId: + project.projectId, + }) + .then(() => { + toast.success( + "Project deleted successfully", + ); + }) + .catch(() => { + toast.error( + "Error deleting this project", + ); + }) + .finally(() => { + utils.project.all.invalidate(); + }); + }} + > + Delete + + +
+
)} - - {hasNoEnvironments && ( -
- - - You have access to this project but no - environments are available - -
- )} - -
- - - - - e.stopPropagation()} - > - - Actions - -
e.stopPropagation()} - > - -
-
e.stopPropagation()} - > - -
- -
e.stopPropagation()} - > - {permissions?.project.delete && ( - - - - e.preventDefault() - } - > - - Delete - - - - - - Are you sure to delete this - project? - - {!emptyServices ? ( -
- - - You have active - services, please delete - them first - -
- ) : ( - - This action cannot be - undone - - )} -
- - - Cancel - - { - await mutateAsync({ - projectId: - project.projectId, - }) - .then(() => { - toast.success( - "Project deleted successfully", - ); - }) - .catch(() => { - toast.error( - "Error deleting this project", - ); - }) - .finally(() => { - utils.project.all.invalidate(); - }); - }} - > - Delete - - -
-
- )} -
-
-
-
- - - -
- - Created - - - {totalServices}{" "} - {totalServices === 1 - ? "service" - : "services"} - -
-
- - -
- ); - })} -
- - )} - -
- +
+ + +
+ + + +
+ + Created + + + {totalServices}{" "} + {totalServices === 1 ? "service" : "services"} + +
+
+ + +
+ ); + })} +
+ + )} +
); diff --git a/apps/dokploy/components/dashboard/requests/show-requests.tsx b/apps/dokploy/components/dashboard/requests/show-requests.tsx index cc4f1764a0..7df1f2ecf1 100644 --- a/apps/dokploy/components/dashboard/requests/show-requests.tsx +++ b/apps/dokploy/components/dashboard/requests/show-requests.tsx @@ -1,10 +1,5 @@ import { format } from "date-fns"; -import { - AlertCircle, - ArrowDownUp, - Calendar as CalendarIcon, - InfoIcon, -} from "lucide-react"; +import { AlertCircle, Calendar as CalendarIcon, InfoIcon } from "lucide-react"; import Link from "next/link"; import { useEffect, useState } from "react"; import { toast } from "sonner"; @@ -12,13 +7,6 @@ import { AlertBlock } from "@/components/shared/alert-block"; import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -93,178 +81,161 @@ export const ShowRequests = () => { return ( <>
- -
- - - - Requests - - - See all the incoming requests that pass trough Traefik - - - {shouldShowWarning && ( - - When you activate, you need to reload traefik to apply the - changes, you can reload traefik in{" "} - - Settings - - - )} - - -
-
-
- - - - - - - -

- At the scheduled time, the cleanup job will keep - only the last 1000 entries in the access log file - and signal Traefik to reopen its log files. The - default schedule is daily at midnight (0 0 * * *). -

-
-
-
-
-
- setCronExpression(e.target.value)} - className="max-w-60" - required - /> - -
-
- + When you activate, you need to reload traefik to apply the changes, + you can reload traefik in{" "} + + Settings + + + )} +
+
+
+
+ + + + + + + +

+ At the scheduled time, the cleanup job will keep only + the last 1000 entries in the access log file and signal + Traefik to reopen its log files. The default schedule is + daily at midnight (0 0 * * *). +

+
+
+
+
+
+ setCronExpression(e.target.value)} + className="max-w-60" + required + /> + - + Update Schedule +
+
+ { + await toggleRequests({ enable: !isActive }) + .then(() => { + refetch(); + toast.success( + `Requests ${isActive ? "deactivated" : "activated"}`, + ); + }) + .catch((err) => { + toast.error(err.message); + }); + }} + > + + +
- {isActive ? ( - <> -
+ {isActive ? ( + <> +
+ + + - - - - - - { - setDateRange({ - from: range?.from, - to: range?.to, - }); - }} - numberOfMonths={2} - /> - - -
- - - - ) : ( -
- -
-

- Requests are not activated -

-

- Activate requests to see incoming traffic statistics and - monitor your application's usage. After activation, you'll - need to reload Traefik for the changes to take effect. -

-
-
- )} - -
- + + + { + setDateRange({ + from: range?.from, + to: range?.to, + }); + }} + numberOfMonths={2} + /> + + +
+ + + + ) : ( +
+ +
+

+ Requests are not activated +

+

+ Activate requests to see incoming traffic statistics and + monitor your application's usage. After activation, you'll + need to reload Traefik for the changes to take effect. +

+
+
+ )} +
); diff --git a/apps/dokploy/components/dashboard/settings/ai-form.tsx b/apps/dokploy/components/dashboard/settings/ai-form.tsx index c3518fdccd..df5d39aaad 100644 --- a/apps/dokploy/components/dashboard/settings/ai-form.tsx +++ b/apps/dokploy/components/dashboard/settings/ai-form.tsx @@ -4,13 +4,6 @@ import { BotIcon, Loader2, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { api } from "@/utils/api"; import { HandleAi } from "./handle-ai"; @@ -20,19 +13,21 @@ export const AiForm = () => { return (
- -
- +
+
+
- - +

+ AI Settings - - Manage your AI configurations +

+

+ Manage your AI configurations +

{aiConfigs && aiConfigs?.length > 0 && } - - +
+
{isPending ? (
Loading... @@ -53,14 +48,16 @@ export const AiForm = () => { {aiConfigs?.map((config) => (
{config.name} - {config.model} +

+ {config.model} +

@@ -98,9 +95,9 @@ export const AiForm = () => { )} )} - +
- +
); }; diff --git a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx index 2e90a9bb6a..9f0dede79f 100644 --- a/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx +++ b/apps/dokploy/components/dashboard/settings/api/show-api-keys.tsx @@ -5,13 +5,6 @@ import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { api } from "@/utils/api"; import { AddApiKey } from "./add-api-key"; @@ -22,17 +15,17 @@ export const ShowApiKeys = () => { return (
- -
- +
+
+
- +

API/CLI Keys - - +

+

Generate and manage API keys to access the API/CLI - +

@@ -47,8 +40,8 @@ export const ShowApiKeys = () => {
- - +
+
{data?.user.apiKeys && data.user.apiKeys.length > 0 ? ( data.user.apiKeys.map((apiKey) => ( @@ -134,9 +127,9 @@ export const ShowApiKeys = () => {
- +
- +
); }; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx index 67c15ee636..577742cb8c 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing-invoices.tsx @@ -1,13 +1,6 @@ import { CreditCard, FileText } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { cn } from "@/lib/utils"; import { ShowInvoices } from "./show-invoices"; @@ -29,18 +22,16 @@ export const ShowBillingInvoices = () => { return (
- -
- - - - Billing - - - Manage your subscription and invoices - - - +
+
+

+ + Billing +

+

+ Manage your subscription and invoices +

+
- +
); }; diff --git a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx index 2f04620f39..787f702dcf 100644 --- a/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx +++ b/apps/dokploy/components/dashboard/settings/billing/show-billing.tsx @@ -16,13 +16,6 @@ import { toast } from "sonner"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { NumberInput } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -147,18 +140,16 @@ export const ShowBilling = () => { return (
- -
- - - - Billing - - - Manage your subscription and invoices - - - +
+
+

+ + Billing +

+

+ Manage your subscription and invoices +

+
- +
); }; diff --git a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx index bc29a4c955..63f8c35951 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx @@ -104,7 +104,7 @@ export const AddCertificate = () => { }; return ( - + )}
- +
-
+
); } diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 28d762c1cb..f33334e6a4 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -12,9 +12,9 @@ import { toast } from "sonner"; import { z } from "zod"; import { DiscordIcon, - MattermostIcon, GotifyIcon, LarkIcon, + MattermostIcon, NtfyIcon, PushoverIcon, ResendIcon, @@ -802,7 +802,7 @@ export const HandleNotifications = ({ notificationId }: Props) => { }; return ( - + {notificationId ? ( diff --git a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx index fdcf51ad75..402dd41a70 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx @@ -279,7 +279,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => { }; return ( - + e.preventDefault()} diff --git a/apps/dokploy/components/dashboard/settings/users/change-role.tsx b/apps/dokploy/components/dashboard/settings/users/change-role.tsx index 2178284b13..dec6831d99 100644 --- a/apps/dokploy/components/dashboard/settings/users/change-role.tsx +++ b/apps/dokploy/components/dashboard/settings/users/change-role.tsx @@ -88,7 +88,7 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => { return ( - + e.preventDefault()} diff --git a/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx b/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx index b6e95cb75a..2de6182f31 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-invitations.tsx @@ -1,16 +1,9 @@ import copy from "copy-to-clipboard"; import { format, isPast } from "date-fns"; -import { Loader2, Mail, MoreHorizontal, Users } from "lucide-react"; +import { Loader2, MoreHorizontal, Users } from "lucide-react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, @@ -40,184 +33,165 @@ export const ShowInvitations = () => { return (
- -
- - - - Invitations - - - Create invitations to your organization. - - - - {isPending ? ( -
- Loading... - -
- ) : ( - <> - {data?.length === 0 ? ( -
- - - Invite users to your organization - - -
- ) : ( -
- - See all invitations - - - Email - Role - Status - - Expires At - - Actions - - - - {data?.map((invitation) => { - const isExpired = isPast( - new Date(invitation.expiresAt), - ); - return ( - - - {invitation.email} - - - - {invitation.role} - - - - - {invitation.status} - - - - {format(new Date(invitation.expiresAt), "PPpp")}{" "} - {isExpired ? ( - - (Expired) - - ) : null} - +
+
+ {isPending ? ( +
+ Loading... + +
+ ) : ( + <> + {data?.length === 0 ? ( +
+ + + Invite users to your organization + + +
+ ) : ( +
+
+ See all invitations + + + Email + Role + Status + + Expires At + + Actions + + + + {data?.map((invitation) => { + const isExpired = isPast( + new Date(invitation.expiresAt), + ); + return ( + + + {invitation.email} + + + + {invitation.role} + + + + + {invitation.status} + + + + {format(new Date(invitation.expiresAt), "PPpp")}{" "} + {isExpired ? ( + + (Expired) + + ) : null} + + + + + + + + + Actions + {!isExpired && ( + <> + {invitation.status === "pending" && ( + { + copy( + `${origin}/invitation?token=${invitation.id}`, + ); + toast.success( + "Invitation Copied to clipboard", + ); + }} + > + Copy Invitation + + )} - - - - - - - - Actions - - {!isExpired && ( - <> - {invitation.status === "pending" && ( - { - copy( - `${origin}/invitation?token=${invitation.id}`, + {invitation.status === "pending" && ( + { + const result = + await authClient.organization.cancelInvitation( + { + invitationId: invitation.id, + }, ); + + if (result.error) { + toast.error(result.error.message); + } else { toast.success( - "Invitation Copied to clipboard", + "Invitation deleted", ); - }} - > - Copy Invitation - - )} - - {invitation.status === "pending" && ( - { - const result = - await authClient.organization.cancelInvitation( - { - invitationId: invitation.id, - }, - ); - - if (result.error) { - toast.error( - result.error.message, - ); - } else { - toast.success( - "Invitation deleted", - ); - refetch(); - } - }} - > - Cancel Invitation - - )} - - )} - { - await removeInvitation({ - invitationId: invitation.id, - }).then(() => { - refetch(); - toast.success("Invitation removed"); - }); - }} - > - Remove Invitation - - - - - - ); - })} - -
- -
- -
-
- )} - - )} -
+ refetch(); + } + }} + > + Cancel Invitation + + )} + + )} + { + await removeInvitation({ + invitationId: invitation.id, + }).then(() => { + refetch(); + toast.success("Invitation removed"); + }); + }} + > + Remove Invitation + + + + + + ); + })} + + +
+ )} + + )}
- +
); }; diff --git a/apps/dokploy/components/dashboard/settings/users/show-users.tsx b/apps/dokploy/components/dashboard/settings/users/show-users.tsx index 75aa839f95..cd80f170da 100644 --- a/apps/dokploy/components/dashboard/settings/users/show-users.tsx +++ b/apps/dokploy/components/dashboard/settings/users/show-users.tsx @@ -5,13 +5,6 @@ import { AlertBlock } from "@/components/shared/alert-block"; import { DialogAction } from "@/components/shared/dialog-action"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { DropdownMenu, DropdownMenuContent, @@ -52,178 +45,201 @@ export const ShowUsers = () => { return (
- -
- - - - Users - - - Add your users to your Dokploy account. - - - - {isPending ? ( -
- Loading... - +
+ {isPending ? ( +
+ Loading... + +
+ ) : ( + <> + {data?.length === 0 ? ( +
+ + + Invite users to your Dokploy account +
) : ( - <> - {data?.length === 0 ? ( -
- - - Invite users to your Dokploy account - -
- ) : ( -
- {hasCustomRolesWithoutLicense && ( - - You have{" "} - {membersWithCustomRoles?.length === 1 - ? "1 user" - : `${membersWithCustomRoles?.length} users`}{" "} - assigned to custom roles. Custom roles will not work - without a valid Enterprise license. Please activate your - license or change these users to a free role (Admin or - Member). - - )} - - - - Email - Role - 2FA +
+ {hasCustomRolesWithoutLicense && ( + + You have{" "} + {membersWithCustomRoles?.length === 1 + ? "1 user" + : `${membersWithCustomRoles?.length} users`}{" "} + assigned to custom roles. Custom roles will not work without + a valid Enterprise license. Please activate your license or + change these users to a free role (Admin or Member). + + )} +
+ + + Email + Role + 2FA - - Created At - - Actions - - - - {data?.map((member) => { - const currentUserRole = data?.find( - (m) => m.user.id === session?.user?.id, - )?.role; + Created At + Actions + + + + {data?.map((member) => { + const currentUserRole = data?.find( + (m) => m.user.id === session?.user?.id, + )?.role; - // Owner never has "Edit Permissions" (they're absolute owner) - // Other users can edit permissions if target is not themselves and target is a member/custom role - const isStaticAdminOrOwner = - member.role === "owner" || member.role === "admin"; - const canEditPermissions = - !isStaticAdminOrOwner && - member.user.id !== session?.user?.id; + // Owner never has "Edit Permissions" (they're absolute owner) + // Other users can edit permissions if target is not themselves and target is a member/custom role + const isStaticAdminOrOwner = + member.role === "owner" || member.role === "admin"; + const canEditPermissions = + !isStaticAdminOrOwner && + member.user.id !== session?.user?.id; - // Can change role based on hierarchy: - // - Owner: Can change anyone's role (except themselves and other owners) - // - Admin: Can only change member/custom roles (not other admins or owners) - // - Owner role is intransferible - const canChangeRole = - member.role !== "owner" && - member.user.id !== session?.user?.id && - (currentUserRole === "owner" || - (currentUserRole === "admin" && - member.role !== "admin")); + // Can change role based on hierarchy: + // - Owner: Can change anyone's role (except themselves and other owners) + // - Admin: Can only change member/custom roles (not other admins or owners) + // - Owner role is intransferible + const canChangeRole = + member.role !== "owner" && + member.user.id !== session?.user?.id && + (currentUserRole === "owner" || + (currentUserRole === "admin" && + member.role !== "admin")); - const canDeleteMember = - permissions?.member.delete ?? false; + const canDeleteMember = + permissions?.member.delete ?? false; - // Self-hosted: "Delete User" removes the user entirely - // Cloud: "Unlink User" removes from the organization only - const canRemove = - member.role !== "owner" && - member.user.id !== session?.user?.id && - (currentUserRole === "owner" || - (currentUserRole === "admin" && - member.role !== "admin") || - (canDeleteMember && !isStaticAdminOrOwner)); + // Self-hosted: "Delete User" removes the user entirely + // Cloud: "Unlink User" removes from the organization only + const canRemove = + member.role !== "owner" && + member.user.id !== session?.user?.id && + (currentUserRole === "owner" || + (currentUserRole === "admin" && + member.role !== "admin") || + (canDeleteMember && !isStaticAdminOrOwner)); - const canDelete = canRemove && !isCloud; - const canUnlink = canRemove && !!isCloud; + const canDelete = canRemove && !isCloud; + const canUnlink = canRemove && !!isCloud; - const hasAnyAction = - canEditPermissions || - canChangeRole || - canDelete || - canUnlink; + const hasAnyAction = + canEditPermissions || + canChangeRole || + canDelete || + canUnlink; - return ( - - - {member.user.email} - {member.user.id === session?.user?.id && ( - - (You) - - )} - - - - {member.role} - - - - {member.user.twoFactorEnabled - ? "Enabled" - : "Disabled"} - - - - {format(new Date(member.createdAt), "PPpp")} - - + return ( + + + {member.user.email} + {member.user.id === session?.user?.id && ( + + (You) + + )} + + + + {member.role} + + + + {member.user.twoFactorEnabled + ? "Enabled" + : "Disabled"} + + + + {format(new Date(member.createdAt), "PPpp")} + + - - {hasAnyAction ? ( - - - - - - - Actions - + + {hasAnyAction ? ( + + + + + + Actions - {canChangeRole && ( - - )} + {canChangeRole && ( + + )} + + {canEditPermissions && ( + + )} + + {canDelete && ( + { + await mutateAsync({ + userId: member.user.id, + }) + .then(() => { + toast.success( + "User deleted successfully", + ); + refetch(); + }) + .catch((err) => { + toast.error( + err?.message || + "Error deleting user", + ); + }); + }} + > + e.preventDefault()} + > + Delete User + + + )} - {canEditPermissions && ( - - )} + {canUnlink && ( + { + if (!isCloud) { + const orgCount = + await utils.user.checkUserOrganizations.fetch( + { + userId: member.user.id, + }, + ); - {canDelete && ( - { + if (orgCount === 1) { await mutateAsync({ userId: member.user.id, }) @@ -233,110 +249,65 @@ export const ShowUsers = () => { ); refetch(); }) - .catch((err) => { + .catch(() => { toast.error( - err?.message || - "Error deleting user", + "Error deleting user", ); }); - }} - > - e.preventDefault()} - > - Delete User - - - )} + return; + } + } - {canUnlink && ( - { - if (!isCloud) { - const orgCount = - await utils.user.checkUserOrganizations.fetch( - { - userId: member.user.id, - }, - ); - - if (orgCount === 1) { - await mutateAsync({ - userId: member.user.id, - }) - .then(() => { - toast.success( - "User deleted successfully", - ); - refetch(); - }) - .catch(() => { - toast.error( - "Error deleting user", - ); - }); - return; - } - } + const { error } = + await authClient.organization.removeMember( + { + memberIdOrEmail: member.id, + }, + ); - const { error } = - await authClient.organization.removeMember( - { - memberIdOrEmail: member.id, - }, - ); - - if (!error) { - toast.success( - "User unlinked successfully", - ); - refetch(); - } else { - toast.error( - "Error unlinking user", - ); - } - }} - > - e.preventDefault()} - > - Unlink User - - - )} - - - ) : ( - - )} - - - ); - })} - -
-
- )} - + if (!error) { + toast.success( + "User unlinked successfully", + ); + refetch(); + } else { + toast.error("Error unlinking user"); + } + }} + > + e.preventDefault()} + > + Unlink User + + + )} + + + ) : ( + + )} + + + ); + })} + + +
)} - -
- + + )} +
); }; diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index 29c7be5eb1..247de1898e 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -6,13 +6,6 @@ import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Form, FormControl, @@ -111,139 +104,135 @@ export const WebDomain = () => { return (
- -
- -
- - - Server Domain - - - Add a domain to your server application. - +
+
+

+ + Server Domain +

+

+ Add a domain to your server application. +

+
+
+
+ {/* Warning for GitHub webhook URL changes */} + {hasChanged && ( + +
+

⚠️ Important: URL Change Impact

+

+ If you change the Dokploy Server URL make sure to update your + Github Apps to keep the auto-deploy working and preview + deployments working. +

- - - {/* Warning for GitHub webhook URL changes */} - {hasChanged && ( - -
-

⚠️ Important: URL Change Impact

-

- If you change the Dokploy Server URL make sure to update - your Github Apps to keep the auto-deploy working and preview - deployments working. -

-
-
- )} -
- - { - return ( - - Domain - - - - - - ); - }} - /> + + )} + + + { + return ( + + Domain + + + + + + ); + }} + /> - { - return ( - - Let's Encrypt Email + { + return ( + + Let's Encrypt Email + + + + + + ); + }} + /> + ( + +
+ HTTPS + + Automatically provision SSL Certificate. + + +
+ + + +
+ )} + /> + {https && ( + { + return ( + + Certificate Provider + + + + - - - ); - }} - /> - ( - -
- HTTPS - - Automatically provision SSL Certificate. - - -
- - - + + None + + Let's Encrypt + + + +
- )} - /> - {https && ( - { - return ( - - Certificate Provider - - - - ); - }} - /> - )} + ); + }} + /> + )} -
- -
- - -
-
- +
+ +
+ + +
); }; diff --git a/apps/dokploy/components/dashboard/settings/web-server.tsx b/apps/dokploy/components/dashboard/settings/web-server.tsx index d9df975e75..f5342baf94 100644 --- a/apps/dokploy/components/dashboard/settings/web-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server.tsx @@ -1,11 +1,4 @@ import { ServerIcon } from "lucide-react"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { api } from "@/utils/api"; import { ShowDokployActions } from "./servers/actions/show-dokploy-actions"; import { ShowStorageActions } from "./servers/actions/show-storage-actions"; @@ -21,46 +14,35 @@ export const WebServer = () => { return (
- {/* */} - -
- - - - Web Server - - Reload or clean the web server. - - {/* - - Web Server - - - Reload or clean the web server. - - */} - -
- - - +
+

+ + Web Server +

+

+ Reload or clean the web server. +

+
+
+
+ + + - -
+ +
-
- - Server IP: {webServerSettings?.serverIp} - - - Version: {dokployVersion} - +
+ + Server IP: {webServerSettings?.serverIp} + + + Version: {dokployVersion} + - -
- +
- +
); }; diff --git a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx index 9c6d196ad8..6bb3bf926c 100644 --- a/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx +++ b/apps/dokploy/components/dashboard/swarm/monitoring-card.tsx @@ -1,11 +1,4 @@ -import { - Activity, - Loader2, - Monitor, - Server, - Settings, - WorkflowIcon, -} from "lucide-react"; +import { Activity, Loader2, Monitor, Server, Settings } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -29,7 +22,7 @@ export default function SwarmMonitorCard({ serverId }: Props) { if (isPending) { return ( -
+
{/*
*/} @@ -45,7 +38,7 @@ export default function SwarmMonitorCard({ serverId }: Props) { if (!nodes) { return ( -
+
Failed to load data @@ -70,118 +63,107 @@ export default function SwarmMonitorCard({ serverId }: Props) { ); return ( - -
-
-
- - - Docker Swarm Overview - -

- Monitor and manage your Docker Swarm cluster -

-
- {!serverId && ( - - )} -
+
+ {!serverId && ( +
+ +
+ )} -
- - - Total Nodes -
- -
-
- -
{totalNodes}
-
-
+
+ + + Total Nodes +
+ +
+
+ +
{totalNodes}
+
+
- - -
- - Active Nodes - - Online -
-
- -
-
- - - - -
- {activeNodesCount} / {totalNodes} -
-
- -
- {activeNodes.map((node) => ( -
- {node.Hostname} -
- ))} -
-
-
-
-
-
+ + +
+ + Active Nodes + + Online +
+
+ +
+
+ + + + +
+ {activeNodesCount} / {totalNodes} +
+
+ +
+ {activeNodes.map((node) => ( +
+ {node.Hostname} +
+ ))} +
+
+
+
+
+
- - -
- - Manager Nodes - - Online -
-
- -
-
- - - - -
- {managerNodesCount} / {totalNodes} -
-
- -
- {managerNodes.map((node) => ( -
- {node.Hostname} -
- ))} -
-
-
-
-
-
-
+ + +
+ + Manager Nodes + + Online +
+
+ +
+
+ + + + +
+ {managerNodesCount} / {totalNodes} +
+
+ +
+ {managerNodes.map((node) => ( +
+ {node.Hostname} +
+ ))} +
+
+
+
+
+
+
-
- {nodes.map((node) => ( - - ))} -
+
+ {nodes.map((node) => ( + + ))}
- +
); } diff --git a/apps/dokploy/components/layouts/notification-bell.tsx b/apps/dokploy/components/layouts/notification-bell.tsx new file mode 100644 index 0000000000..55547ebf81 --- /dev/null +++ b/apps/dokploy/components/layouts/notification-bell.tsx @@ -0,0 +1,90 @@ +import { Bell } from "lucide-react"; +import { toast } from "sonner"; +import { DialogAction } from "@/components/shared/dialog-action"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { authClient } from "@/lib/auth-client"; +import { api } from "@/utils/api"; + +export function NotificationBell() { + const { data: invitations, refetch: refetchInvitations } = + api.user.getInvitations.useQuery(); + const { refetch } = api.user.get.useQuery(); + + return ( + + + + + + Pending Invitations +
+ {invitations && invitations.length > 0 ? ( + invitations.map((invitation) => ( +
+ e.preventDefault()} + > +
+ {invitation?.organization?.name} +
+
+ Expires: {new Date(invitation.expiresAt).toLocaleString()} +
+
+ Role: {invitation.role} +
+
+ { + const { error } = + await authClient.organization.acceptInvitation({ + invitationId: invitation.id, + }); + + if (error) { + toast.error( + error.message || "Error accepting invitation", + ); + } else { + toast.success("Invitation accepted successfully"); + await refetchInvitations(); + await refetch(); + } + }} + > + + +
+ )) + ) : ( + No pending invitations + )} +
+
+
+ ); +} diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index c173be620e..fba6ea24df 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -89,6 +89,7 @@ import { DialogAction } from "../shared/dialog-action"; import { Logo } from "../shared/logo"; import { Button } from "../ui/button"; import { TimeBadge } from "../ui/time-badge"; +import { NotificationBell } from "./notification-bell"; import { UpdateServerButton } from "./update-server"; import { UserNav } from "./user-nav"; @@ -589,7 +590,7 @@ function SidebarLogo() { - - {/* Notification Bell */} - - - - - - - Pending Invitations -
- {invitations && invitations.length > 0 ? ( - invitations.map((invitation) => ( -
- e.preventDefault()} - > -
- {invitation?.organization?.name} -
-
- Expires:{" "} - {new Date(invitation.expiresAt).toLocaleString()} -
-
- Role: {invitation.role} -
-
- { - const { error } = - await authClient.organization.acceptInvitation({ - invitationId: invitation.id, - }); - - if (error) { - toast.error( - error.message || "Error accepting invitation", - ); - } else { - toast.success("Invitation accepted successfully"); - await refetchInvitations(); - await refetch(); - } - }} - > - - -
- )) - ) : ( - - No pending invitations - - )} -
-
-
-
)} @@ -886,7 +805,15 @@ export default function Page({ children }: Props) { refetchOnWindowFocus: false, }); - const includesProjects = pathname?.includes("/dashboard/project"); + const hasOwnBreadcrumb = + pathname?.includes("/dashboard/project") || + pathname?.includes("/dashboard/deployments") || + pathname?.includes("/dashboard/monitoring") || + pathname?.includes("/dashboard/schedules") || + pathname?.includes("/dashboard/traefik") || + pathname?.includes("/dashboard/docker") || + pathname?.includes("/dashboard/swarm") || + pathname?.includes("/dashboard/requests"); const { data: isCloud } = api.settings.isCloud.useQuery(); const { @@ -921,12 +848,12 @@ export default function Page({ children }: Props) { }} style={ { - "--sidebar-width": "19.5rem", - "--sidebar-width-mobile": "19.5rem", + "--sidebar-width": "16rem", + "--sidebar-width-mobile": "18rem", } as React.CSSProperties } > - + {/* - {!includesProjects && ( -
-
-
- - - - - - - - {activeItem?.title} - - - - - -
+ {!hasOwnBreadcrumb && ( +
+ + +
+ + + + + + {activeItem?.title} + + + + + +
+
{!isCloud && } +
)} -
{children}
+
+ {children} +
); diff --git a/apps/dokploy/components/layouts/user-nav.tsx b/apps/dokploy/components/layouts/user-nav.tsx index 757dd987e7..ddfcd367fe 100644 --- a/apps/dokploy/components/layouts/user-nav.tsx +++ b/apps/dokploy/components/layouts/user-nav.tsx @@ -1,5 +1,20 @@ -import { ChevronsUpDown } from "lucide-react"; +import { + Activity, + ChevronsUpDown, + Container, + CreditCard, + FolderOpen, + LogOut, + Monitor, + Moon, + Palette, + Route, + Server, + Sun, + User, +} from "lucide-react"; import { useRouter } from "next/router"; +import { useTheme } from "next-themes"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, @@ -8,31 +23,71 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { authClient } from "@/lib/auth-client"; import { getFallbackAvatarInitials } from "@/lib/utils"; import { api } from "@/utils/api"; -import { ModeToggle } from "../ui/modeToggle"; import { SidebarMenuButton } from "../ui/sidebar"; const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; -export const UserNav = () => { +export const UserNav = ({ compact }: { compact?: boolean }) => { const router = useRouter(); + const { theme, setTheme } = useTheme(); const { data } = api.user.get.useQuery(); const { data: permissions } = api.user.getPermissions.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); - // const { mutateAsync } = api.auth.logout.useMutation(); + const avatarEl = ( + + + + {getFallbackAvatarInitials( + `${data?.user?.firstName} ${data?.user?.lastName}`.trim(), + )} + + + ); return ( - + {compact ? ( + + ) : ( + + {avatarEl} +
+ Account + {data?.user?.email} +
+ +
+ )} +
+ + { )} -
- Account - {data?.user?.email} -
- - - - -
- +
My Account {data?.user?.email} - - -
+
+
{ router.push("/dashboard/settings/profile"); }} > + Profile { router.push("/dashboard/projects"); }} > + Projects {!isCloud ? ( @@ -93,6 +135,7 @@ export const UserNav = () => { router.push("/dashboard/monitoring"); }} > + Monitoring {permissions?.traefikFiles.read && ( @@ -102,6 +145,7 @@ export const UserNav = () => { router.push("/dashboard/traefik"); }} > + Traefik )} @@ -114,6 +158,7 @@ export const UserNav = () => { }); }} > + Docker )} @@ -126,6 +171,7 @@ export const UserNav = () => { router.push("/dashboard/settings/servers"); }} > + Servers ) @@ -138,10 +184,41 @@ export const UserNav = () => { router.push("/dashboard/settings/billing"); }} > + Billing )} + + + + Theme + + + setTheme("system")} + > + + System + + setTheme("light")} + > + + Light + + setTheme("dark")} + > + + Dark + + + + { @@ -153,6 +230,7 @@ export const UserNav = () => { // }); }} > + Log out
diff --git a/apps/dokploy/components/proprietary/audit-logs/data-table.tsx b/apps/dokploy/components/proprietary/audit-logs/data-table.tsx index dec9678917..4599985a22 100644 --- a/apps/dokploy/components/proprietary/audit-logs/data-table.tsx +++ b/apps/dokploy/components/proprietary/audit-logs/data-table.tsx @@ -12,8 +12,8 @@ import { type VisibilityState, } from "@tanstack/react-table"; import { format } from "date-fns"; -import { CalendarIcon, ChevronDown, X } from "lucide-react"; -import React from "react"; +import { CalendarIcon, ChevronDown, Filter, Search, X } from "lucide-react"; +import React, { useState } from "react"; import type { DateRange } from "react-day-picker"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -167,96 +167,45 @@ export function DataTable({ filters.resourceType || filters.dateRange; + const [showFilters, setShowFilters] = useState(false); + const activeFilterCount = + (filters.action ? 1 : 0) + + (filters.resourceType ? 1 : 0) + + (filters.resourceName ? 1 : 0) + + (filters.dateRange?.from ? 1 : 0); + return (
-
- onFilterChange("userEmail", e.target.value)} - className="max-w-xs" - /> - onFilterChange("resourceName", e.target.value)} - className="max-w-xs" - /> - - onFilterChange("userEmail", e.target.value)} + className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground min-w-0" + /> +
+ - - - onFilterChange("dateRange", range)} - numberOfMonths={2} - initialFocus - /> - - + + Filters + {activeFilterCount > 0 && ( + + {activeFilterCount} + + )} + {hasFilters && ( @@ -294,59 +242,194 @@ export function DataTable({
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - ))} - - - {isLoading ? ( - - + {/* Filter panel */} +
+
+
+

Filters

+ {activeFilterCount > 0 && ( + + )} +
+ +
+

+ Resource name +

+ onFilterChange("resourceName", e.target.value)} + className="h-9" + /> +
+ +
+

+ Action +

+ +
+ +
+

+ Resource type +

+ +
+ +
+

+ Date range +

+ + + + + + onFilterChange("dateRange", range)} + numberOfMonths={2} + initialFocus + /> + + +
+
+
+ + {/* Table */} +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + ))} - )) - ) : ( - - - No audit logs found. - - - )} - -
+ ))} + + + {isLoading ? ( + + + Loading... + + + ) : table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No audit logs found. + + + )} + + +
diff --git a/apps/dokploy/components/proprietary/audit-logs/show-audit-logs.tsx b/apps/dokploy/components/proprietary/audit-logs/show-audit-logs.tsx index 7f18514936..9c77d37144 100644 --- a/apps/dokploy/components/proprietary/audit-logs/show-audit-logs.tsx +++ b/apps/dokploy/components/proprietary/audit-logs/show-audit-logs.tsx @@ -1,13 +1,7 @@ import { ClipboardList } from "lucide-react"; import React from "react"; import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { CardDescription, CardTitle } from "@/components/ui/card"; import { api } from "@/utils/api"; import { columns } from "./columns"; import { type AuditLogFilters, DataTable } from "./data-table"; @@ -83,8 +77,8 @@ function AuditLogsContent() { export function ShowAuditLogs() { return ( - -
+
+
- +
Audit Logs @@ -101,12 +95,12 @@ export function ShowAuditLogs() { Track all actions performed by members in your organization. - - +
+
- +
- +
); } diff --git a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx index 51b31b84c7..dae4f5be9c 100644 --- a/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx +++ b/apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx @@ -15,13 +15,6 @@ import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-featu import { AlertBlock } from "@/components/shared/alert-block"; import { DialogAction } from "@/components/shared/dialog-action"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; import { Dialog, DialogContent, @@ -549,41 +542,28 @@ type CreateRoleSchema = z.infer; export const ManageCustomRoles = () => { return ( - -
- - - - Custom Roles - - - Create and manage custom roles with fine-grained permissions - - - - - - - -
-
+
+ + + +
); }; interface HandleCustomRoleProps { roleName?: string; initialPermissions?: Record; - onSuccess: () => void; + onSuccess?: () => void; } -function HandleCustomRole({ +export function HandleCustomRole({ roleName, initialPermissions, onSuccess, @@ -646,7 +626,7 @@ function HandleCustomRole({ if (!isEdit) { setOpen(false); } - onSuccess(); + onSuccess?.(); } catch (error) { let message = `Error ${isEdit ? "updating" : "creating"} role`; if (error instanceof Error) { @@ -673,8 +653,8 @@ function HandleCustomRole({ Edit ) : ( - )} @@ -800,10 +780,6 @@ const CustomRolesContent = () => { return (
-
- -
- {customRoles?.length === 0 ? (
diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx index d53af90a24..fc4639fe94 100644 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx +++ b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-preview.tsx @@ -30,7 +30,7 @@ export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
{/* Simulated sidebar header */} -
+
{config.logoUrl ? ( +
{config.footerText}
)} diff --git a/apps/dokploy/components/shared/advance-breadcrumb.tsx b/apps/dokploy/components/shared/advance-breadcrumb.tsx index 99ab73dace..51d5efa58d 100644 --- a/apps/dokploy/components/shared/advance-breadcrumb.tsx +++ b/apps/dokploy/components/shared/advance-breadcrumb.tsx @@ -18,6 +18,7 @@ import { PostgresqlIcon, RedisIcon, } from "@/components/icons/data-tools-icons"; +import { NotificationBell } from "@/components/layouts/notification-bell"; import { Button } from "@/components/ui/button"; import { Command, @@ -35,6 +36,7 @@ import { import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { SidebarTrigger } from "@/components/ui/sidebar"; +import { TimeBadge } from "@/components/ui/time-badge"; import { api, type RouterOutputs } from "@/utils/api"; type ProjectItem = RouterOutputs["project"]["all"][number]; @@ -204,6 +206,7 @@ export const AdvanceBreadcrumb = () => { null, ); + const { data: isCloud } = api.settings.isCloud.useQuery(); // Fetch all projects const { data: allProjects } = api.project.all.useQuery(); @@ -313,7 +316,7 @@ export const AdvanceBreadcrumb = () => { // If we're just on the projects page, show simple breadcrumb if (!projectId) { return ( -
+
@@ -322,30 +325,33 @@ export const AdvanceBreadcrumb = () => { Projects
+
+ {!isCloud && } + +
); } return ( -
+
-
+
{/* Project Selector */} { {/* Environment Selector */} {projectEnvironments && projectEnvironments.length > 1 && ( - - - + + - - {currentEnvironment?.name || "production"} - - - - - - -
- - - Esc - -
- - No environments found. - - - {filteredEnvironments.map((env) => { - const isSelected = - env.environmentId === environmentId; - return ( - - handleEnvironmentSelect(env.environmentId) - } - className="flex items-center justify-between py-2 cursor-pointer" - > - {env.name} - {isSelected && ( - - )} - - ); - })} - - - -
-
-
+ +
+ + + Esc + +
+ + No environments found. + + + {filteredEnvironments.map((env) => { + const isSelected = + env.environmentId === environmentId; + return ( + + handleEnvironmentSelect(env.environmentId) + } + className="flex items-center justify-between py-2 cursor-pointer" + > + {env.name} + {isSelected && ( + + )} + + ); + })} + + + +
+
+
+ )} {projectEnvironments && projectEnvironments.length === 1 && ( -

- {currentEnvironment?.name || "production"} -

+ <> + + / + + + {currentEnvironment?.name || "production"} + + )} {/* Service Selector - only show when viewing a service */} {serviceId && currentService && ( <> - + + / + { )}
+
+ {!isCloud && } + +
); }; diff --git a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx index 16b185e0f1..af6e79a69f 100644 --- a/apps/dokploy/components/shared/breadcrumb-sidebar.tsx +++ b/apps/dokploy/components/shared/breadcrumb-sidebar.tsx @@ -1,6 +1,7 @@ import { ChevronDown } from "lucide-react"; import Link from "next/link"; import { Fragment } from "react"; +import { NotificationBell } from "@/components/layouts/notification-bell"; import { Breadcrumb, BreadcrumbItem, @@ -37,49 +38,50 @@ export const BreadcrumbSidebar = ({ list }: Props) => { const { data: isCloud } = api.settings.isCloud.useQuery(); return ( -
-
-
- - - - - {list.map((item, index) => ( - - - {item.dropdownItems && item.dropdownItems.length > 0 ? ( - - - {item.name} - - - - {item.dropdownItems.map((subItem) => ( - - {subItem.name} - - ))} - - - ) : ( - - {item.href ? ( - {item?.name} - ) : ( - {item?.name} - )} - - )} - - {index + 1 < list.length && ( - +
+ + +
+ + + {list.map((item, index) => ( + + + {item.dropdownItems && item.dropdownItems.length > 0 ? ( + + + {item.name} + + + + {item.dropdownItems.map((subItem) => ( + + {subItem.name} + + ))} + + + ) : ( + + {item.href ? ( + {item?.name} + ) : ( + {item?.name} + )} + )} - - ))} - - -
+ + {index + 1 < list.length && ( + + )} + + ))} + + +
+
{!isCloud && } +
); diff --git a/apps/dokploy/components/shared/tag-filter.tsx b/apps/dokploy/components/shared/tag-filter.tsx index 0faa37a883..947905c379 100644 --- a/apps/dokploy/components/shared/tag-filter.tsx +++ b/apps/dokploy/components/shared/tag-filter.tsx @@ -92,12 +92,9 @@ export function TagFilter({
-
- - No tags found. - - -
+ + No tags found. +
{tags.map((tag) => { @@ -119,6 +116,9 @@ export function TagFilter({ })}
+
+ +
diff --git a/apps/dokploy/components/ui/alert-dialog.tsx b/apps/dokploy/components/ui/alert-dialog.tsx index 0e7ff478c3..475e651a7d 100644 --- a/apps/dokploy/components/ui/alert-dialog.tsx +++ b/apps/dokploy/components/ui/alert-dialog.tsx @@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( ( return ( <> (({ className, ...props }, ref) => (
)); @@ -23,7 +20,7 @@ const CardHeader = React.forwardRef< >(({ className, ...props }, ref) => (
)); @@ -36,7 +33,7 @@ const CardTitle = React.forwardRef<

= 7 && colorValue.startsWith("#")) + return colorValue.slice(0, 7); + return "#000000"; +} + +function hslToRgb( + _h: number, + _s: number, + _l: number, +): { r: number; g: number; b: number } { + const h = _h / 360; + const s = _s / 100; + const l = _l / 100; + let r: number; + let g: number; + let b: number; + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p: number, q: number, _t: number) => { + let t = _t; + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255), + }; +} + +function hsvToRgb( + _h: number, + _s: number, + _v: number, +): { r: number; g: number; b: number } { + const h = _h / 360; + const s = _s / 100; + const v = _v / 100; + let r = 0; + let g = 0; + let b = 0; + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + switch (i % 6) { + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; + } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255), + }; +} + +function rgbToHsv( + _r: number, + _g: number, + _b: number, +): { h: number; s: number; v: number } { + const r = _r / 255; + const g = _g / 255; + const b = _b / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const d = max - min; + let h = 0; + const s = max === 0 ? 0 : d / max; + const v = max; + if (max !== min) { + switch (max) { + case r: + h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + break; + case g: + h = ((b - r) / d + 2) / 6; + break; + case b: + h = ((r - g) / d + 4) / 6; + break; + } + } + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + v: Math.round(v * 100), + }; +} + +// ─── SaturationValuePicker ────────────────────────────────────────── + +function SaturationValuePicker({ + hue, + saturation, + value, + onChange, +}: { + hue: number; + saturation: number; + value: number; + onChange: (s: number, v: number) => void; +}) { + const pickerRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const dragRectRef = useRef(null); + + const x = saturation; + const y = 100 - value; + + const updatePosition = useCallback( + (e: MouseEvent | React.MouseEvent) => { + const rect = + dragRectRef.current || pickerRef.current?.getBoundingClientRect(); + if (!rect) return; + const xPos = Math.max(0, Math.min(rect.width, e.clientX - rect.left)); + const yPos = Math.max(0, Math.min(rect.height, e.clientY - rect.top)); + const newS = + rect.width > 0 ? Math.min(100, (xPos / rect.width) * 100) : 0; + const newV = + rect.height > 0 ? Math.max(0, 100 - (yPos / rect.height) * 100) : 0; + onChange(newS, newV); + }, + [onChange], + ); + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + if (pickerRef.current) + dragRectRef.current = pickerRef.current.getBoundingClientRect(); + setIsDragging(true); + updatePosition(e); + }; + + useEffect(() => { + if (!isDragging) return; + const onMove = (e: MouseEvent) => updatePosition(e); + const onUp = () => { + setIsDragging(false); + dragRectRef.current = null; + }; + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + return () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + }; + }, [isDragging, updatePosition]); + + const fullColor = hslToRgb(hue, 100, 50); + const bg = `linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(0,0,0,1) 100%), linear-gradient(to right, rgba(255,255,255,1) 0%, rgb(${fullColor.r},${fullColor.g},${fullColor.b}) 100%)`; + + return ( +
+
+
+
+
+ ); +} + +// ─── HueBar ───────────────────────────────────────────────────────── + +function HueBar({ + hue, + onChange, +}: { + hue: number; + onChange: (h: number) => void; +}) { + const barRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + + const position = Math.max(2, Math.min(98, (hue / 360) * 100)); + + const updateHue = useCallback( + (e: MouseEvent | React.MouseEvent) => { + if (!barRef.current) return; + const rect = barRef.current.getBoundingClientRect(); + const x = Math.max(0, Math.min(rect.width, e.clientX - rect.left)); + onChange(Math.round(Math.max(0, Math.min(360, (x / rect.width) * 360)))); + }, + [onChange], + ); + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + setIsDragging(true); + updateHue(e); + }; + + useEffect(() => { + if (!isDragging) return; + const onMove = (e: MouseEvent) => updateHue(e); + const onUp = () => setIsDragging(false); + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); + return () => { + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); + }; + }, [isDragging, updateHue]); + + return ( +
+
+
+
+
+ ); +} + +// ─── Main ColorPicker ─────────────────────────────────────────────── + +interface ColorPickerProps { + value?: string; + onChange: (value: string) => void; + defaultValue?: string; + placeholder?: string; +} + +export function ColorPicker({ + value, + onChange, + defaultValue = "#3b82f6", + placeholder = "#000000", +}: ColorPickerProps) { + const [open, setOpen] = useState(false); + const displayValue = value || ""; + + const [rgbaColor, setRgbaColor] = useState(() => + parseColor(displayValue || defaultValue), + ); + const [hue, setHue] = useState( + () => rgbToHsv(rgbaColor.r, rgbaColor.g, rgbaColor.b).h, + ); + const [saturation, setSaturation] = useState( + () => rgbToHsv(rgbaColor.r, rgbaColor.g, rgbaColor.b).s, + ); + const [hsvValue, setHsvValue] = useState( + () => rgbToHsv(rgbaColor.r, rgbaColor.g, rgbaColor.b).v, + ); + const [hexInput, setHexInput] = useState(() => + getHexOnly(displayValue || defaultValue), + ); + + const isInternalUpdate = useRef(false); + const isHexInputUpdate = useRef(false); + + useEffect(() => { + if (!isHexInputUpdate.current) { + setHexInput(getHexOnly(rgbaToHex(rgbaColor))); + } + isHexInputUpdate.current = false; + }, [rgbaColor]); + + useEffect(() => { + if (displayValue && !isInternalUpdate.current) { + const c = parseColor(displayValue); + setRgbaColor(c); + const hsv = rgbToHsv(c.r, c.g, c.b); + setHue(hsv.h); + setSaturation(hsv.s); + setHsvValue(hsv.v); + } + isInternalUpdate.current = false; + }, [displayValue]); + + const debouncedOnChange = useRef(debounce((v: string) => onChange(v), 100)); + useEffect(() => { + debouncedOnChange.current = debounce((v: string) => onChange(v), 100); + return () => debouncedOnChange.current.cancel(); + }, [onChange]); + + const handleChange = (color: { + r: number; + g: number; + b: number; + a: number; + }) => { + setRgbaColor(color); + isInternalUpdate.current = true; + debouncedOnChange.current(rgbaToHex(color)); + }; + + const handleEyeDropper = async () => { + if (!("EyeDropper" in window)) return; + try { + // @ts-ignore + const result = await new window.EyeDropper().open(); + const parsed = parseColor(result.sRGBHex); + setRgbaColor(parsed); + const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b); + setHue(hsv.h); + setSaturation(hsv.s); + setHsvValue(hsv.v); + isInternalUpdate.current = true; + onChange(rgbaToHex(parsed)); + } catch { + /* cancelled */ + } + }; + + const handleHexChange = (val: string) => { + let v = val.trim(); + if (v && !v.startsWith("#")) v = `#${v}`; + setHexInput(v); + + if (v.length === 7 && /^#[0-9a-fA-F]{6}$/.test(v)) { + isHexInputUpdate.current = true; + isInternalUpdate.current = true; + const parsed = parseColor(v); + setRgbaColor(parsed); + const hsv = rgbToHsv(parsed.r, parsed.g, parsed.b); + setHue(hsv.h); + setSaturation(hsv.s); + setHsvValue(hsv.v); + onChange(v); + } + }; + + const handleHexBlur = () => { + setHexInput(getHexOnly(rgbaToHex(rgbaColor))); + }; + + const handleOpenChange = (newOpen: boolean) => { + setOpen(newOpen); + if (newOpen && !displayValue) { + const def = parseColor(defaultValue); + setRgbaColor(def); + const hsv = rgbToHsv(def.r, def.g, def.b); + setHue(hsv.h); + setSaturation(hsv.s); + setHsvValue(hsv.v); + } + }; + + return ( + + + + + + +
+
+ { + setSaturation(s); + setHsvValue(v); + const rgb = hsvToRgb(hue, s, v); + handleChange({ ...rgb, a: 1 }); + }} + /> +
+ + { + setHue(newHue); + const rgb = hsvToRgb(newHue, saturation, hsvValue); + handleChange({ ...rgb, a: 1 }); + }} + /> + +
+ + handleHexChange(e.target.value)} + onBlur={handleHexBlur} + onKeyDown={(e) => { + if (e.key === "Enter") (e.target as HTMLInputElement).blur(); + }} + placeholder={placeholder} + /> + + + + +
+
+
+
+ ); +} diff --git a/apps/dokploy/components/ui/dialog.tsx b/apps/dokploy/components/ui/dialog.tsx index e1237f70c2..66ace3d464 100644 --- a/apps/dokploy/components/ui/dialog.tsx +++ b/apps/dokploy/components/ui/dialog.tsx @@ -51,7 +51,7 @@ const DialogOverlay = React.forwardRef< {/* Custom overlay for modal=false - no click handler to avoid Command conflicts */}
{/* DialogFooter outside scrollable area with proper spacing */} - {dialogFooter && ( -
- {dialogFooter} -
- )} + {dialogFooter &&
{dialogFooter}
} - - + + Close
@@ -170,7 +166,7 @@ const DialogHeader = ({ }: React.HTMLAttributes) => (
({ + size: "xs", +}); + +interface InputGroupProps extends React.ComponentProps<"div"> { + size?: "xs" | "sm"; +} + +function InputGroup({ className, size = "xs", ...props }: InputGroupProps) { + const sizeClasses = { + xs: "h-8 rounded-lg", + sm: "h-10 rounded-xl", + }; + + return ( + +
[data-align=inline-start]]:[&>input]:pl-2", + "has-[>[data-align=inline-end]]:[&>input]:pr-2", + "has-[[data-slot=input-group-control]:focus-visible]:border-foreground/50", + sizeClasses[size], + className, + )} + {...props} + /> + + ); +} + +const inputGroupAddonVariants = cva( + "text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4", + { + variants: { + align: { + "inline-start": "order-first pl-2.5", + "inline-end": "order-last pr-2.5", + }, + }, + defaultVariants: { + align: "inline-start", + }, + }, +); + +function InputGroupAddon({ + className, + align = "inline-start", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
{ + if ((e.target as HTMLElement).closest("button")) { + return; + } + e.currentTarget.parentElement?.querySelector("input")?.focus(); + }} + {...props} + /> + ); +} + +interface InputGroupInputProps extends React.ComponentProps {} + +function InputGroupInput({ className, ...props }: InputGroupInputProps) { + return ( + + ); +} + +export { InputGroup, InputGroupAddon, InputGroupInput }; diff --git a/apps/dokploy/components/ui/input.tsx b/apps/dokploy/components/ui/input.tsx index ddeb368944..40330958ef 100644 --- a/apps/dokploy/components/ui/input.tsx +++ b/apps/dokploy/components/ui/input.tsx @@ -72,12 +72,13 @@ const Input = React.forwardRef( return ( <> -
+
( )}
{errorMessage && ( - - {errorMessage} - + {errorMessage} )} ); diff --git a/apps/dokploy/components/ui/popover.tsx b/apps/dokploy/components/ui/popover.tsx index 903cd059ce..b4dac1f3e8 100644 --- a/apps/dokploy/components/ui/popover.tsx +++ b/apps/dokploy/components/ui/popover.tsx @@ -17,7 +17,7 @@ const PopoverContent = React.forwardRef< align={align} sideOffset={sideOffset} className={cn( - "z-50 w-full rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className, )} {...props} diff --git a/apps/dokploy/components/ui/progress.tsx b/apps/dokploy/components/ui/progress.tsx index 1010596f35..ccc73aac0c 100644 --- a/apps/dokploy/components/ui/progress.tsx +++ b/apps/dokploy/components/ui/progress.tsx @@ -10,13 +10,13 @@ const Progress = React.forwardRef< diff --git a/apps/dokploy/components/ui/radio-group.tsx b/apps/dokploy/components/ui/radio-group.tsx index 4e8d82f11a..22487711af 100644 --- a/apps/dokploy/components/ui/radio-group.tsx +++ b/apps/dokploy/components/ui/radio-group.tsx @@ -26,7 +26,7 @@ const RadioGroupItem = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:border-foreground/50 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 transition-colors", className, )} {...props} diff --git a/apps/dokploy/components/ui/sheet.tsx b/apps/dokploy/components/ui/sheet.tsx index b79167415e..18ff88243c 100644 --- a/apps/dokploy/components/ui/sheet.tsx +++ b/apps/dokploy/components/ui/sheet.tsx @@ -19,7 +19,7 @@ const SheetOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( {children} - - + + Close diff --git a/apps/dokploy/components/ui/sidebar.tsx b/apps/dokploy/components/ui/sidebar.tsx index a146a4f974..90081b27dd 100644 --- a/apps/dokploy/components/ui/sidebar.tsx +++ b/apps/dokploy/components/ui/sidebar.tsx @@ -188,7 +188,7 @@ const Sidebar = React.forwardRef< return (