Compare commits

..

26 Commits

Author SHA1 Message Date
Mauricio Siu
98a586478e chore: bump version to v0.29.1 in package.json 2026-04-19 12:07:02 -06:00
Mauricio Siu
13248c8d8a Merge pull request #4257 from colocated/fix/4256-preview-deployment-too-many-args
fix: preview deployments broken on v0.29.0 — postgres 100-arg limit
2026-04-19 12:06:17 -06:00
Jack
54417ca8e7 fix: limit application columns in findPreviewDeploymentById to avoid postgres 100-arg limit
Closes #4256
2026-04-19 11:14:47 +01:00
Mauricio Siu
b392e58001 Merge pull request #4244 from Dokploy/feat/dashboard-home
feat: add dashboard home page
2026-04-17 22:40:50 -06:00
Mauricio Siu
d9945c0a4f style: update ShowHome component layout for improved responsiveness
- Adjusted the Card component to have a minimum height of 85vh for better visual consistency.
- Ensured the inner div has a full height to enhance the layout structure.
2026-04-17 22:24:08 -06:00
autofix-ci[bot]
f6e2c033ba [autofix.ci] apply automated fixes 2026-04-18 04:18:44 +00:00
Mauricio Siu
5c787adae1 feat: implement homeStats query for dashboard overview
- Replace individual project and server queries with a consolidated homeStats query to streamline data retrieval for the dashboard.
- Update the ShowHome component to utilize homeStats for displaying project, environment, application, and service counts, along with their status breakdown.
- Enhance data handling for user permissions to ensure accurate statistics based on user access levels.
2026-04-17 22:18:14 -06:00
Mauricio Siu
2ba1df1eaa feat: refine home page and fix libsql in bulk actions
- Home: 4 KPI cards (projects, services, deploys/7d, status list),
  server column with icon in recent deployments, empty state with
  icon, dashboard card frame to match other pages.
- Include libsql in project services count sort.
- Fix bulk actions in environment page: libsql was missing from
  start, stop, move, delete and deploy handlers.
2026-04-17 22:11:04 -06:00
autofix-ci[bot]
e7859395b1 [autofix.ci] apply automated fixes 2026-04-18 03:37:12 +00:00
Mauricio Siu
6f0ed89ce7 feat: add dashboard home page with overview and recent deployments
Adds a new /dashboard/home landing with welcome header, KPI cards
(deploys/24h, build, CPU, memory) and a recent deployments list.

Home is now the post-login landing and the destination for permission
fallback redirects across the app. Projects remains accessible from
the sidebar.
2026-04-17 21:36:37 -06:00
Mauricio Siu
4277a509b2 Merge pull request #4241 from sancho1952007/patch-1
style: Fix typo in custom entrypoint description
2026-04-17 21:06:48 -06:00
Sancho Godinho
f7b576cbf3 Fix typo in custom entrypoint description 2026-04-18 04:23:15 +05:30
Mauricio Siu
425fef6e28 fix: remove 'v' prefix from version in synchronization workflow
Update the version retrieval command in the GitHub Actions workflow to strip the 'v' prefix from the version number in package.json. This change ensures that the version format is consistent for downstream processes.
2026-04-17 14:49:14 -06:00
Mauricio Siu
958372c5f9 chore: update paths in version synchronization workflow for MCP and CLI repositories
Modify the GitHub Actions workflow to clone the MCP and CLI repositories into temporary directories instead of the current directory. This change improves the organization of the workflow and ensures that the latest OpenAPI specification is correctly referenced during the synchronization process.
2026-04-17 14:46:20 -06:00
Mauricio Siu
e7c581476e feat: add workflow dispatch trigger to version synchronization workflow
Enhance the GitHub Actions workflow by adding a workflow_dispatch trigger, allowing manual execution of the version synchronization process. This provides greater flexibility in managing version updates for MCP and CLI repositories.
2026-04-17 14:44:04 -06:00
Mauricio Siu
0cae8330e2 chore: adjust version bump timing in synchronization workflow
Update the GitHub Actions workflow to bump the version in package.json after installing dependencies, ensuring that the version is not overwritten by pnpm install. This change enhances the reliability of version synchronization for both MCP and CLI repositories.
2026-04-17 14:42:14 -06:00
Mauricio Siu
4a271c11e7 Merge pull request #4239 from Dokploy/feat/resend-verification-email-on-signin
feat: resend verification email on sign-in and improve template
2026-04-17 14:02:01 -06:00
Mauricio Siu
fda367b2c5 fix: update logger configuration to disable in production environment
Change the logger's disabled property to be dependent on the NODE_ENV variable, ensuring logging is disabled in production for improved performance and security.
2026-04-17 14:01:46 -06:00
Mauricio Siu
ea1238b1d1 feat: resend verification email on sign-in and improve email template
- Add `sendOnSignIn: true` to emailVerification config so unverified users
  receive a new verification email when they attempt to sign in
- Create styled verification email template matching the invoice email design
- Extract `sendVerificationEmail` helper to keep auth.ts clean
- Show friendly message on login when email is not verified
2026-04-17 13:59:50 -06:00
Mauricio Siu
b060f80932 feat: add no tags message to tag selector component
Enhance the TagSelector component to display a message when no tags are created, prompting users to add tags. This improves user experience by providing clear feedback in the UI.
2026-04-16 12:21:17 -06:00
Mauricio Siu
04b9f56333 chore: enhance version synchronization workflow for MCP and CLI repositories
Update the GitHub Actions workflow to include regeneration of tools from the latest OpenAPI specification and ensure the latest openapi.json is copied to the CLI repository. This improves the consistency and accuracy of the versioning and API documentation across both repositories.
2026-04-15 20:55:37 -06:00
Mauricio Siu
599b97da51 feat: add version synchronization workflow for MCP and CLI repositories
Implement a GitHub Actions workflow to automatically sync the version from the Dokploy repository to the MCP and CLI repositories upon release. This includes cloning the repositories, updating the package.json version, and committing the changes with relevant metadata, ensuring consistent versioning across platforms.
2026-04-15 18:50:54 -06:00
Mauricio Siu
415298fddb feat: add OpenAPI sync to MCP and CLI repositories
Implement workflows to sync the OpenAPI specification to both the MCP and CLI repositories. This includes cloning the repositories, updating the openapi.json file, and committing the changes with relevant metadata. The process ensures that the OpenAPI documentation is consistently updated across multiple platforms.
2026-04-15 18:32:20 -06:00
Mauricio Siu
ddff8b9de7 feat: add container networks view to dashboard
Integrate a new component, ShowContainerNetworks, to display network details for each container in the dashboard. This includes a dialog that shows network information such as IP address, gateway, and MAC address, enhancing the container management capabilities.
2026-04-13 22:04:46 -06:00
Mauricio Siu
90f97912a4 Merge pull request #4221 from Dokploy/feat/container-view-mounts
feat: add view mounts, config, and terminal to container actions
2026-04-13 21:58:20 -06:00
Mauricio Siu
9af745ce67 feat: add view mounts, view config, and terminal to container actions
Add a new "View Mounts" action to the container dropdown that displays
volume and bind mounts in a formatted table (type, source, destination,
mode, read/write). Also add "View Config" and "Terminal" actions to the
compose containers tab which previously only had logs and lifecycle actions.
2026-04-13 21:56:53 -06:00
83 changed files with 1359 additions and 314 deletions

View File

@@ -68,3 +68,45 @@ jobs:
echo "✅ OpenAPI synced to website successfully"
- name: Sync to MCP repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
cd mcp-repo
cp -f ../openapi.json openapi.json
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add openapi.json
git commit -m "chore: sync OpenAPI specification [skip ci]" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
--allow-empty
git push
echo "✅ OpenAPI synced to MCP repository successfully"
- name: Sync to CLI repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
cd cli-repo
cp -f ../openapi.json openapi.json
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add openapi.json
git commit -m "chore: sync OpenAPI specification [skip ci]" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
--allow-empty
git push
echo "✅ OpenAPI synced to CLI repository successfully"

80
.github/workflows/sync-version.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Sync version to MCP and CLI repos
on:
release:
types: [published]
workflow_dispatch:
jobs:
sync-version:
name: Sync version to external repos
runs-on: ubuntu-latest
steps:
- name: Checkout Dokploy repository
uses: actions/checkout@v4
- name: Get version
id: get_version
run: |
VERSION=$(jq -r .version apps/dokploy/package.json | sed 's/^v//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Sync version to MCP repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo
cd /tmp/mcp-repo
# Regenerate tools from latest OpenAPI spec
npm install -g pnpm
pnpm install
pnpm run fetch-openapi
pnpm run generate
# Bump version after install so pnpm install doesn't overwrite it
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Release: ${{ github.event.release.html_url }}" \
--allow-empty
git push
- name: Sync version to CLI repository
run: |
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo
cd /tmp/cli-repo
# Copy latest openapi spec and regenerate commands
cp ${{ github.workspace }}/openapi.json ./openapi.json
npm install -g pnpm
pnpm install
pnpm run generate
# Bump version after install so pnpm install doesn't overwrite it
if [ -f package.json ]; then
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
fi
git config user.name "Dokploy Bot"
git config user.email "bot@dokploy.com"
git add -A
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
-m "Release: ${{ github.event.release.html_url }}" \
--allow-empty
git push
echo "CLI repo synced to version ${{ steps.get_version.outputs.version }}"

View File

@@ -666,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<div className="space-y-0.5">
<FormLabel>Custom Entrypoint</FormLabel>
<FormDescription>
Use custom entrypoint for domina
Use custom entrypoint for domain
<br />
"web" and/or "websecure" is used by default.
</FormDescription>

View File

@@ -241,7 +241,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -329,7 +329,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -254,7 +254,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -349,7 +349,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -229,7 +229,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -316,7 +316,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -250,7 +250,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -347,7 +347,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -511,7 +511,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -181,7 +181,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -263,7 +263,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -36,6 +36,10 @@ import {
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
const DockerLogsId = dynamic(
() =>
@@ -217,6 +221,24 @@ const ContainerRow = ({
View Logs
</DropdownMenuItem>
</DialogTrigger>
<ShowContainerConfig
containerId={container.containerId}
serverId={serverId || ""}
/>
<ShowContainerMounts
containerId={container.containerId}
serverId={serverId || ""}
/>
<ShowContainerNetworks
containerId={container.containerId}
serverId={serverId || ""}
/>
<DockerTerminalModal
containerId={container.containerId}
serverId={serverId || ""}
>
Terminal
</DockerTerminalModal>
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"

View File

@@ -243,7 +243,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -331,7 +331,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -240,7 +240,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -327,7 +327,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -230,7 +230,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -317,7 +317,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -252,7 +252,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -349,7 +349,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -409,7 +409,7 @@ export const HandleBackup = ({
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -346,7 +346,7 @@ export const RestoreBackup = ({
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
@@ -428,7 +428,7 @@ export const RestoreBackup = ({
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -0,0 +1,112 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
interface Mount {
Type: string;
Source: string;
Destination: string;
Mode: string;
RW: boolean;
Propagation: string;
Name?: string;
Driver?: string;
}
export const ShowContainerMounts = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,
},
);
const mounts: Mount[] = data?.Mounts ?? [];
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
View Mounts
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
<DialogHeader>
<DialogTitle>Container Mounts</DialogTitle>
<DialogDescription>
Volume and bind mounts for this container
</DialogDescription>
</DialogHeader>
<div className="overflow-auto max-h-[70vh]">
{mounts.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No mounts found for this container.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Type</TableHead>
<TableHead>Source</TableHead>
<TableHead>Destination</TableHead>
<TableHead>Mode</TableHead>
<TableHead>Read/Write</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mounts.map((mount, index) => (
<TableRow key={index}>
<TableCell>
<Badge variant="outline">{mount.Type}</Badge>
</TableCell>
<TableCell className="font-mono text-xs max-w-[250px] truncate">
{mount.Name || mount.Source}
</TableCell>
<TableCell className="font-mono text-xs max-w-[250px] truncate">
{mount.Destination}
</TableCell>
<TableCell className="text-xs">
{mount.Mode || "-"}
</TableCell>
<TableCell>
<Badge variant={mount.RW ? "default" : "secondary"}>
{mount.RW ? "RW" : "RO"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,119 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
interface Network {
IPAMConfig: unknown;
Links: unknown;
Aliases: string[] | null;
MacAddress: string;
NetworkID: string;
EndpointID: string;
Gateway: string;
IPAddress: string;
IPPrefixLen: number;
IPv6Gateway: string;
GlobalIPv6Address: string;
GlobalIPv6PrefixLen: number;
DriverOpts: unknown;
}
export const ShowContainerNetworks = ({ containerId, serverId }: Props) => {
const { data } = api.docker.getConfig.useQuery(
{
containerId,
serverId,
},
{
enabled: !!containerId,
},
);
const networks: Record<string, Network> =
data?.NetworkSettings?.Networks ?? {};
const entries = Object.entries(networks);
return (
<Dialog>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
View Networks
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
<DialogHeader>
<DialogTitle>Container Networks</DialogTitle>
<DialogDescription>
Networks attached to this container
</DialogDescription>
</DialogHeader>
<div className="overflow-auto max-h-[70vh]">
{entries.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
No networks found for this container.
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Network</TableHead>
<TableHead>IP Address</TableHead>
<TableHead>Gateway</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>Aliases</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{entries.map(([name, network]) => (
<TableRow key={name}>
<TableCell>
<Badge variant="outline">{name}</Badge>
</TableCell>
<TableCell className="font-mono text-xs">
{network.IPAddress
? `${network.IPAddress}/${network.IPPrefixLen}`
: "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{network.Gateway || "-"}
</TableCell>
<TableCell className="font-mono text-xs">
{network.MacAddress || "-"}
</TableCell>
<TableCell className="text-xs">
{network.Aliases?.join(", ") || "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -10,6 +10,8 @@ import {
} from "@/components/ui/dropdown-menu";
import { ShowContainerConfig } from "../config/show-container-config";
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
import { ShowContainerMounts } from "../mounts/show-container-mounts";
import { ShowContainerNetworks } from "../networks/show-container-networks";
import { RemoveContainerDialog } from "../remove/remove-container";
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
import { UploadFileModal } from "../upload/upload-file-modal";
@@ -123,6 +125,14 @@ export const columns: ColumnDef<Container>[] = [
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<ShowContainerMounts
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<ShowContainerNetworks
containerId={container.containerId}
serverId={container.serverId || ""}
/>
<DockerTerminalModal
containerId={container.containerId}
serverId={container.serverId || ""}

View File

@@ -0,0 +1,291 @@
import { formatDistanceToNow } from "date-fns";
import { ArrowRight, Rocket, Server } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
type DeploymentStatus = "idle" | "running" | "done" | "error";
const statusDotClass: Record<string, string> = {
done: "bg-emerald-500",
running: "bg-amber-500",
error: "bg-red-500",
idle: "bg-muted-foreground/40",
};
function getServiceInfo(d: any) {
const app = d.application;
const comp = d.compose;
const serverName: string =
d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy";
if (app?.environment?.project && app.environment) {
return {
name: app.name as string,
environment: app.environment.name as string,
projectName: app.environment.project.name as string,
serverName,
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
};
}
if (comp?.environment?.project && comp.environment) {
return {
name: comp.name as string,
environment: comp.environment.name as string,
projectName: comp.environment.project.name as string,
serverName,
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
};
}
return null;
}
function StatCard({
label,
value,
delta,
}: {
label: string;
value: string;
delta?: string;
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<div className="flex flex-col gap-1">
<span className="text-3xl font-semibold tracking-tight">{value}</span>
{delta && (
<span className="text-xs text-muted-foreground">{delta}</span>
)}
</div>
</div>
);
}
function StatusListCard({
label,
items,
}: {
label: string;
items: { dotClass: string; label: string; count: number }[];
}) {
return (
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3">
<span className="text-xs uppercase tracking-wider text-muted-foreground">
{label}
</span>
<ul className="flex flex-col gap-1.5">
{items.map((item) => (
<li key={item.label} className="flex items-center gap-2.5 text-sm">
<span
className={`size-2 rounded-full shrink-0 ${item.dotClass}`}
aria-hidden
/>
<span className="font-semibold tabular-nums w-8">{item.count}</span>
<span className="text-muted-foreground">{item.label}</span>
</li>
))}
</ul>
</div>
);
}
export const ShowHome = () => {
const { data: auth } = api.user.get.useQuery();
const { data: homeStats } = api.project.homeStats.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const canReadDeployments = !!permissions?.deployment.read;
const { data: deployments } = api.deployment.allCentralized.useQuery(
undefined,
{
enabled: canReadDeployments,
refetchInterval: 10000,
},
);
const firstName = auth?.user?.firstName?.trim();
const totals = homeStats ?? {
projects: 0,
environments: 0,
applications: 0,
compose: 0,
databases: 0,
services: 0,
};
const statusBreakdown = homeStats?.status ?? {
running: 0,
error: 0,
idle: 0,
};
const recentDeployments = useMemo(() => {
if (!deployments) return [];
return [...deployments]
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
.slice(0, 10);
}, [deployments]);
const deployStats = useMemo(() => {
const now = Date.now();
const weekMs = 7 * 24 * 60 * 60 * 1000;
const lastStart = now - weekMs;
const prevStart = now - 2 * weekMs;
const last: NonNullable<typeof deployments> = [];
const prev: NonNullable<typeof deployments> = [];
for (const d of deployments ?? []) {
const t = new Date(d.createdAt).getTime();
if (t >= lastStart) last.push(d);
else if (t >= prevStart) prev.push(d);
}
const lastCount = last.length;
const prevCount = prev.length;
let delta: string | undefined;
if (prevCount > 0) {
const pct = Math.round(((lastCount - prevCount) / prevCount) * 100);
delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`;
} else if (lastCount > 0) {
delta = "no prior data";
} else {
delta = "no activity yet";
}
return { value: String(lastCount), delta };
}, [deployments]);
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full">
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<h1 className="text-3xl font-semibold tracking-tight">
{firstName ? `Welcome back, ${firstName}` : "Welcome back"}
</h1>
<Button asChild variant="secondary" className="w-fit">
<Link href="/dashboard/projects">
Go to projects
<ArrowRight className="size-4" />
</Link>
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
label="Projects"
value={String(totals.projects)}
delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`}
/>
<StatCard
label="Services"
value={String(totals.services)}
delta={`${totals.applications} apps · ${totals.compose} compose · ${totals.databases} db`}
/>
<StatCard
label="Deploys / 7d"
value={deployStats.value}
delta={deployStats.delta}
/>
<StatusListCard
label="Status"
items={[
{
dotClass: "bg-emerald-500",
label: "running",
count: statusBreakdown.running,
},
{
dotClass: "bg-red-500",
label: "errored",
count: statusBreakdown.error,
},
{
dotClass: "bg-muted-foreground/40",
label: "idle",
count: statusBreakdown.idle,
},
]}
/>
</div>
<div className="rounded-xl border bg-background">
<div className="flex items-center justify-between px-5 py-4 border-b">
<div className="flex items-center gap-2">
<Rocket className="size-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Recent deployments</h2>
</div>
{canReadDeployments && (
<Link
href="/dashboard/deployments"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
view all
</Link>
)}
</div>
{!canReadDeployments ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>You do not have permission to view deployments.</span>
</div>
) : recentDeployments.length === 0 ? (
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
<Rocket className="size-8 opacity-40" />
<span>No deployments yet.</span>
</div>
) : (
<ul className="divide-y">
{recentDeployments.map((d) => {
const info = getServiceInfo(d);
if (!info) return null;
const status = (d.status ?? "idle") as DeploymentStatus;
return (
<li key={d.deploymentId}>
<Link
href={info.href}
className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors"
>
<span
className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`}
aria-hidden
/>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-sm truncate">{info.name}</span>
<span className="text-xs text-muted-foreground truncate">
{info.projectName} · {info.environment}
</span>
</div>
<span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate">
<Server className="size-3 shrink-0" />
<span className="truncate">{info.serverName}</span>
</span>
<span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline">
{status}
</span>
<span className="text-xs text-muted-foreground w-24 text-right hidden md:inline">
{formatDistanceToNow(new Date(d.createdAt), {
addSuffix: true,
})}
</span>
<span className="text-xs text-muted-foreground hover:text-foreground transition-colors">
logs
</span>
</Link>
</li>
);
})}
</ul>
)}
</div>
</div>
</Card>
</div>
);
};

View File

@@ -17,11 +17,11 @@ interface Props {
const chartConfig = {
readMb: {
label: "Read (MB)",
color: "oklch(var(--chart-1))",
color: "hsl(var(--chart-1))",
},
writeMb: {
label: "Write (MB)",
color: "oklch(var(--chart-2))",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;

View File

@@ -17,7 +17,7 @@ interface Props {
const chartConfig = {
usage: {
label: "CPU Usage",
color: "oklch(var(--chart-1))",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;

View File

@@ -16,7 +16,7 @@ interface Props {
const chartConfig = {
usedGb: {
label: "Used (GB)",
color: "oklch(var(--chart-3))",
color: "hsl(var(--chart-3))",
},
} satisfies ChartConfig;

View File

@@ -25,19 +25,19 @@ const chartConfig = {
},
images: {
label: "Images",
color: "oklch(var(--chart-1))",
color: "hsl(var(--chart-1))",
},
containers: {
label: "Containers",
color: "oklch(var(--chart-2))",
color: "hsl(var(--chart-2))",
},
volumes: {
label: "Volumes",
color: "oklch(var(--chart-3))",
color: "hsl(var(--chart-3))",
},
buildCache: {
label: "Build Cache",
color: "oklch(var(--chart-4))",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
@@ -138,7 +138,7 @@ export const DockerDiskUsageChart = () => {
innerRadius={60}
outerRadius={85}
strokeWidth={3}
stroke="oklch(var(--background))"
stroke="hsl(var(--background))"
minAngle={15}
>
{chartData.map((entry) => (

View File

@@ -19,7 +19,7 @@ interface Props {
const chartConfig = {
usage: {
label: "Memory (GB)",
color: "oklch(var(--chart-2))",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;

View File

@@ -17,11 +17,11 @@ interface Props {
const chartConfig = {
inMB: {
label: "In (MB)",
color: "oklch(var(--chart-1))",
color: "hsl(var(--chart-1))",
},
outMB: {
label: "Out (MB)",
color: "oklch(var(--chart-2))",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;

View File

@@ -27,7 +27,7 @@ interface Props {
const chartConfig = {
cpu: {
label: "CPU",
color: "oklch(var(--chart-1))",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
@@ -58,12 +58,12 @@ export const ContainerCPUChart = ({ data }: Props) => {
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-1))"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-1))"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -112,7 +112,7 @@ export const ContainerCPUChart = ({ data }: Props) => {
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="oklch(var(--chart-1))"
stroke="hsl(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -33,7 +33,7 @@ interface Props {
const chartConfig = {
memory: {
label: "Memory",
color: "oklch(var(--chart-2))",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
@@ -73,12 +73,12 @@ export const ContainerMemoryChart = ({ data }: Props) => {
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-2))"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-2))"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -133,7 +133,7 @@ export const ContainerMemoryChart = ({ data }: Props) => {
dataKey="memory"
type="monotone"
fill="url(#fillMemory)"
stroke="oklch(var(--chart-2))"
stroke="hsl(var(--chart-2))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -40,11 +40,11 @@ interface FormattedMetric {
const chartConfig = {
input: {
label: "Input",
color: "oklch(var(--chart-3))",
color: "hsl(var(--chart-3))",
},
output: {
label: "Output",
color: "oklch(var(--chart-4))",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
@@ -84,24 +84,24 @@ export const ContainerNetworkChart = ({ data }: Props) => {
<linearGradient id="fillInput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-3))"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-3))"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillOutput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-4))"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-4))"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -162,7 +162,7 @@ export const ContainerNetworkChart = ({ data }: Props) => {
dataKey="input"
type="monotone"
fill="url(#fillInput)"
stroke="oklch(var(--chart-3))"
stroke="hsl(var(--chart-3))"
strokeWidth={2}
/>
<Area
@@ -170,7 +170,7 @@ export const ContainerNetworkChart = ({ data }: Props) => {
dataKey="output"
type="monotone"
fill="url(#fillOutput)"
stroke="oklch(var(--chart-4))"
stroke="hsl(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -22,7 +22,7 @@ interface CPUChartProps {
const chartConfig = {
cpu: {
label: "CPU",
color: "oklch(var(--chart-1))",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
@@ -45,12 +45,12 @@ export function CPUChart({ data }: CPUChartProps) {
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-1))"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-1))"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -99,7 +99,7 @@ export function CPUChart({ data }: CPUChartProps) {
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="oklch(var(--chart-1))"
stroke="hsl(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -29,14 +29,14 @@ export function DiskChart({ data }: RadialChartProps) {
const chartData = [
{
disk: 25,
fill: "oklch(var(--chart-2))",
fill: "hsl(var(--chart-2))",
},
];
const chartConfig = {
disk: {
label: "Disk",
color: "oklch(var(--chart-2))",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
@@ -71,7 +71,7 @@ export function DiskChart({ data }: RadialChartProps) {
dataKey="disk"
background
cornerRadius={10}
fill="oklch(var(--chart-2))"
fill="hsl(var(--chart-2))"
/>
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label

View File

@@ -20,7 +20,7 @@ interface MemoryChartProps {
const chartConfig = {
Memory: {
label: "Memory",
color: "oklch(var(--chart-2))",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
@@ -46,12 +46,12 @@ export function MemoryChart({ data }: MemoryChartProps) {
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-2))"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-2))"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -116,7 +116,7 @@ export function MemoryChart({ data }: MemoryChartProps) {
dataKey="memUsed"
type="monotone"
fill="url(#fillMemory)"
stroke="oklch(var(--chart-2))"
stroke="hsl(var(--chart-2))"
strokeWidth={2}
name="Memory"
/>

View File

@@ -22,11 +22,11 @@ interface NetworkChartProps {
const chartConfig = {
networkIn: {
label: "Network In",
color: "oklch(var(--chart-3))",
color: "hsl(var(--chart-3))",
},
networkOut: {
label: "Network Out",
color: "oklch(var(--chart-4))",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
@@ -52,24 +52,24 @@ export function NetworkChart({ data }: NetworkChartProps) {
<linearGradient id="fillNetworkIn" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-3))"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-3))"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillNetworkOut" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="oklch(var(--chart-4))"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="oklch(var(--chart-4))"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -121,7 +121,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
dataKey="networkIn"
type="monotone"
fill="url(#fillNetworkIn)"
stroke="oklch(var(--chart-3))"
stroke="hsl(var(--chart-3))"
strokeWidth={2}
/>
<Area
@@ -129,7 +129,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
dataKey="networkOut"
type="monotone"
fill="url(#fillNetworkOut)"
stroke="oklch(var(--chart-4))"
stroke="hsl(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -236,7 +236,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
<Button
variant="outline"
className={cn(
"w-full sm:w-[200px] justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
"w-full sm:w-[200px] justify-between !bg-input",
)}
>
{isLoadingTags

View File

@@ -166,6 +166,7 @@ export const ShowProjects = () => {
return (
total +
(env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
@@ -178,6 +179,7 @@ export const ShowProjects = () => {
return (
total +
(env.applications?.length || 0) +
(env.libsql?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +

View File

@@ -27,7 +27,7 @@ const chartConfig = {
},
count: {
label: "Count",
color: "oklch(var(--chart-1))",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
@@ -101,9 +101,9 @@ export const RequestDistributionChart = ({
<Area
dataKey="count"
type="monotone"
fill="oklch(var(--chart-1))"
fill="hsl(var(--chart-1))"
fillOpacity={0.4}
stroke="oklch(var(--chart-1))"
stroke="hsl(var(--chart-1))"
/>
</AreaChart>
</ChartContainer>

View File

@@ -167,7 +167,7 @@ export const SearchCommand = () => {
<CommandGroup heading={"Application"} hidden={true}>
<CommandItem
onSelect={() => {
router.push("/dashboard/projects");
router.push("/dashboard/home");
setOpen(false);
}}
>

View File

@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
onClick={() => {
if (stepper.isLast) {
setIsOpen(false);
push("/dashboard/projects");
push("/dashboard/home");
} else {
stepper.next();
}

View File

@@ -19,6 +19,7 @@ import {
Forward,
GalleryVerticalEnd,
GitBranch,
House,
Key,
KeyRound,
Loader2,
@@ -148,6 +149,12 @@ type Menu = {
// The `isEnabled` function is called to determine if the item should be displayed
const MENU: Menu = {
home: [
{
isSingle: true,
title: "Home",
url: "/dashboard/home",
icon: House,
},
{
isSingle: true,
title: "Projects",

View File

@@ -80,7 +80,7 @@ export const UserNav = () => {
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
router.push("/dashboard/projects");
router.push("/dashboard/home");
}}
>
Projects

View File

@@ -44,7 +44,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
try {
const { data, error } = await authClient.signIn.sso({
email: values.email,
callbackURL: "/dashboard/projects",
callbackURL: "/dashboard/home",
});
if (error) {
toast.error(error.message ?? "Failed to sign in with SSO");

View File

@@ -60,99 +60,99 @@ const DEFAULT_CSS_TEMPLATE = `/* ============================================
/* ---------- Light Mode ---------- */
:root {
--background: 1 0 0;
--foreground: 0.145 0 0;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 1 0 0;
--card-foreground: 0.145 0 0;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 1 0 0;
--popover-foreground: 0.145 0 0;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 0.205 0 0;
--primary-foreground: 0.985 0 0;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 0.97 0 0;
--secondary-foreground: 0.205 0 0;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 0.97 0 0;
--muted-foreground: 0.556 0 0;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 0.97 0 0;
--accent-foreground: 0.205 0 0;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0.577 0.245 27.325;
--destructive-foreground: 0.985 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--border: 0.922 0 0;
--input: 0.922 0 0;
--ring: 0.708 0 0;
--radius: 0.625rem;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
/* Sidebar */
--sidebar: 0.985 0 0;
--sidebar-foreground: 0.145 0 0;
--sidebar-primary: 0.205 0 0;
--sidebar-primary-foreground: 0.985 0 0;
--sidebar-accent: 0.97 0 0;
--sidebar-accent-foreground: 0.205 0 0;
--sidebar-border: 0.922 0 0;
--sidebar-ring: 0.708 0 0;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
/* Charts */
--chart-1: 0.646 0.222 41.116;
--chart-2: 0.6 0.118 184.704;
--chart-3: 0.398 0.07 227.392;
--chart-4: 0.828 0.189 84.429;
--chart-5: 0.769 0.188 70.08;
--chart-1: 173 58% 39%;
--chart-2: 12 76% 61%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
/* ---------- Dark Mode ---------- */
.dark {
--background: 0.145 0 0;
--foreground: 0.985 0 0;
--background: 0 0% 0%;
--foreground: 0 0% 98%;
--card: 0.205 0 0;
--card-foreground: 0.985 0 0;
--card: 240 4% 10%;
--card-foreground: 0 0% 98%;
--popover: 0.205 0 0;
--popover-foreground: 0.985 0 0;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0.922 0 0;
--primary-foreground: 0.205 0 0;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 0.269 0 0;
--secondary-foreground: 0.985 0 0;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0.269 0 0;
--muted-foreground: 0.708 0 0;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--accent: 0.269 0 0;
--accent-foreground: 0.985 0 0;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0.704 0.191 22.216;
--destructive-foreground: 0.985 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--border: 0.371 0 0;
--input: 0.371 0 0;
--ring: 0.556 0 0;
--border: 240 3.7% 15.9%;
--input: 240 4% 10%;
--ring: 240 4.9% 83.9%;
/* Sidebar */
--sidebar: 0.205 0 0;
--sidebar-foreground: 0.985 0 0;
--sidebar-primary: 0.488 0.243 264.376;
--sidebar-primary-foreground: 0.985 0 0;
--sidebar-accent: 0.269 0 0;
--sidebar-accent-foreground: 0.985 0 0;
--sidebar-border: 0.371 0 0;
--sidebar-ring: 0.556 0 0;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
/* Charts */
--chart-1: 0.488 0.243 264.376;
--chart-2: 0.696 0.17 162.48;
--chart-3: 0.769 0.188 70.08;
--chart-4: 0.627 0.265 303.9;
--chart-5: 0.645 0.246 16.439;
--chart-1: 220 70% 50%;
--chart-2: 340 75% 55%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 160 60% 45%;
}
/* ---------- Custom Styles ---------- */

View File

@@ -116,6 +116,14 @@ export function TagSelector({
<HandleTag />
</div>
</CommandEmpty>
{tags.length === 0 && (
<div className="flex flex-col items-center gap-2 py-4">
<span className="text-sm text-muted-foreground">
No tags created yet.
</span>
<HandleTag />
</div>
)}
<CommandGroup>
{tags.map((tag) => {
const isSelected = selectedTags.includes(tag.id);

View File

@@ -13,7 +13,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground p-px",
className,
)}
{...props}
@@ -39,12 +39,12 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex h-9 items-center gap-2 border-b px-3" cmdk-input-wrapper="">
<Search className="size-4 shrink-0 opacity-50" />
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -115,7 +115,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}

View File

@@ -76,7 +76,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={inputType}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40 md:text-sm",
// bg-gray
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
className,
)}

View File

@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30 [&>span]:line-clamp-1",
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-input px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}

View File

@@ -528,7 +528,7 @@ const sidebarMenuButtonVariants = cva(
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_oklch(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_oklch(var(--sidebar-accent))]",
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",

View File

@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80",
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/80",
className,
)}
{...props}
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block size-4 rounded-full bg-background ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground",
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>

View File

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

View File

@@ -48,7 +48,7 @@ const MyApp = ({
disableTransitionOnChange
forcedTheme={Component.theme}
>
<NextTopLoader color="oklch(var(--sidebar-ring))" />
<NextTopLoader color="hsl(var(--sidebar-ring))" />
<WhitelabelingProvider />
<Toaster richColors />
<SearchCommand />

View File

@@ -53,7 +53,7 @@ export default function Custom404({ statusCode, error }: Props) {
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
<Link
href="/dashboard/projects"
href="/dashboard/home"
className={buttonVariants({
variant: "secondary",
className: "flex flex-row gap-2",

View File

@@ -102,7 +102,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -0,0 +1,53 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowHome } from "@/components/dashboard/home/show-home";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
const Home = () => {
return <ShowHome />;
};
export default Home;
Home.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { req, res } = ctx;
const { user, session } = await validateRequest(req);
if (!user) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
await helpers.user.get.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -96,7 +96,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -122,7 +122,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -509,6 +509,14 @@ const EnvironmentPage = (
deploy: api.mongo.deploy.useMutation(),
};
const libsqlActions = {
start: api.libsql.start.useMutation(),
stop: api.libsql.stop.useMutation(),
move: api.libsql.move.useMutation(),
delete: api.libsql.remove.useMutation(),
deploy: api.libsql.deploy.useMutation(),
};
const handleBulkStart = async () => {
let success = 0;
setIsBulkActionLoading(true);
@@ -541,6 +549,9 @@ const EnvironmentPage = (
case "mongo":
await mongoActions.start.mutateAsync({ mongoId: serviceId });
break;
case "libsql":
await libsqlActions.start.mutateAsync({ libsqlId: serviceId });
break;
}
success++;
} catch {
@@ -588,6 +599,9 @@ const EnvironmentPage = (
case "mongo":
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
break;
case "libsql":
await libsqlActions.stop.mutateAsync({ libsqlId: serviceId });
break;
}
success++;
} catch {
@@ -664,6 +678,12 @@ const EnvironmentPage = (
targetEnvironmentId: selectedTargetEnvironment,
});
break;
case "libsql":
await libsqlActions.move.mutateAsync({
libsqlId: serviceId,
targetEnvironmentId: selectedTargetEnvironment,
});
break;
}
await utils.environment.one.invalidate({
environmentId,
@@ -733,6 +753,11 @@ const EnvironmentPage = (
mongoId: serviceId,
});
break;
case "libsql":
await libsqlActions.delete.mutateAsync({
libsqlId: serviceId,
});
break;
}
await utils.environment.one.invalidate({
environmentId,
@@ -799,6 +824,11 @@ const EnvironmentPage = (
mongoId: serviceId,
});
break;
case "libsql":
await libsqlActions.deploy.mutateAsync({
libsqlId: serviceId,
});
break;
}
success++;
} catch (error) {
@@ -1856,7 +1886,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -490,7 +490,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -492,7 +492,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -343,7 +343,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -372,7 +372,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -376,7 +376,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -353,7 +353,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -360,7 +360,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -363,7 +363,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -18,7 +18,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -35,7 +35,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -82,6 +82,16 @@ export default function Home({ IS_CLOUD }: Props) {
});
if (error) {
const isEmailNotVerified =
error.code === "EMAIL_NOT_VERIFIED" ||
error.message?.toLowerCase().includes("email not verified");
if (isEmailNotVerified) {
const msg =
"Your email is not verified. We've sent a new verification link to your email.";
toast.info(msg);
setError(msg);
return;
}
toast.error(error.message);
setError(error.message || "An error occurred while logging in");
return;
@@ -96,7 +106,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while logging in");
} finally {
@@ -123,7 +133,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while verifying 2FA code");
} finally {
@@ -153,7 +163,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while verifying backup code");
} finally {
@@ -398,7 +408,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}
@@ -427,7 +437,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -139,7 +139,7 @@ const Invitation = ({
});
toast.success("Account created successfully");
router.push("/dashboard/projects");
router.push("/dashboard/home");
} catch {
toast.error("An error occurred while creating your account");
}

View File

@@ -303,7 +303,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
destination: "/dashboard/home",
},
};
}

View File

@@ -487,6 +487,148 @@ export const projectRouter = createTRPCRouter({
},
),
homeStats: protectedProcedure.query(async ({ ctx }) => {
const isPrivileged = ctx.user.role === "owner" || ctx.user.role === "admin";
let accessedProjects: string[] = [];
let accessedEnvironments: string[] = [];
let accessedServices: string[] = [];
if (!isPrivileged) {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
accessedProjects = member.accessedProjects;
accessedEnvironments = member.accessedEnvironments;
accessedServices = member.accessedServices;
if (accessedProjects.length === 0) {
return {
projects: 0,
environments: 0,
applications: 0,
compose: 0,
databases: 0,
services: 0,
status: { running: 0, error: 0, idle: 0 },
};
}
}
const projectIdFilter = isPrivileged
? eq(projects.organizationId, ctx.session.activeOrganizationId)
: and(
sql`${projects.projectId} IN (${sql.join(
accessedProjects.map((id) => sql`${id}`),
sql`, `,
)})`,
eq(projects.organizationId, ctx.session.activeOrganizationId),
);
const environmentFilter = isPrivileged
? undefined
: accessedEnvironments.length === 0
? sql`false`
: sql`${environments.environmentId} IN (${sql.join(
accessedEnvironments.map((envId) => sql`${envId}`),
sql`, `,
)})`;
const applyFilter = (col: AnyPgColumn) =>
isPrivileged ? undefined : buildServiceFilter(col, accessedServices);
const rows = await db.query.projects.findMany({
where: projectIdFilter,
columns: { projectId: true },
with: {
environments: {
where: environmentFilter,
columns: { environmentId: true },
with: {
applications: {
where: applyFilter(applications.applicationId),
columns: { applicationStatus: true },
},
compose: {
where: applyFilter(compose.composeId),
columns: { composeStatus: true },
},
libsql: {
where: applyFilter(libsql.libsqlId),
columns: { applicationStatus: true },
},
mariadb: {
where: applyFilter(mariadb.mariadbId),
columns: { applicationStatus: true },
},
mongo: {
where: applyFilter(mongo.mongoId),
columns: { applicationStatus: true },
},
mysql: {
where: applyFilter(mysql.mysqlId),
columns: { applicationStatus: true },
},
postgres: {
where: applyFilter(postgres.postgresId),
columns: { applicationStatus: true },
},
redis: {
where: applyFilter(redis.redisId),
columns: { applicationStatus: true },
},
},
},
},
});
let applicationsCount = 0;
let composeCount = 0;
let databasesCount = 0;
let environmentsCount = 0;
const status = { running: 0, error: 0, idle: 0 };
const bump = (s?: string | null) => {
if (s === "done") status.running++;
else if (s === "error") status.error++;
else status.idle++;
};
for (const project of rows) {
for (const env of project.environments) {
environmentsCount++;
applicationsCount += env.applications.length;
composeCount += env.compose.length;
databasesCount +=
env.libsql.length +
env.mariadb.length +
env.mongo.length +
env.mysql.length +
env.postgres.length +
env.redis.length;
for (const a of env.applications) bump(a.applicationStatus);
for (const c of env.compose) bump(c.composeStatus);
for (const s of env.libsql) bump(s.applicationStatus);
for (const s of env.mariadb) bump(s.applicationStatus);
for (const s of env.mongo) bump(s.applicationStatus);
for (const s of env.mysql) bump(s.applicationStatus);
for (const s of env.postgres) bump(s.applicationStatus);
for (const s of env.redis) bump(s.applicationStatus);
}
}
return {
projects: rows.length,
environments: environmentsCount,
applications: applicationsCount,
compose: composeCount,
databases: databasesCount,
services: applicationsCount + composeCount + databasesCount,
status,
};
}),
search: protectedProcedure
.input(
z.object({

View File

@@ -5,97 +5,97 @@
@layer base {
:root {
--terminal-paste: rgba(0, 0, 0, 0.2);
--background: 1 0 0;
--foreground: 0.145 0 0;
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 1 0 0;
--card-foreground: 0.145 0 0;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 1 0 0;
--popover-foreground: 0.145 0 0;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 0.205 0 0;
--primary-foreground: 0.985 0 0;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 0.97 0 0;
--secondary-foreground: 0.205 0 0;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 0.97 0 0;
--muted-foreground: 0.556 0 0;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 0.97 0 0;
--accent-foreground: 0.205 0 0;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0.577 0.245 27.325;
--destructive-foreground: 0.985 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--border: 0.922 0 0;
--input: 0.922 0 0;
--ring: 0.708 0 0;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.625rem;
--radius: 0.5rem;
--overlay: rgba(0, 0, 0, 0.2);
--chart-1: 0.646 0.222 41.116;
--chart-2: 0.6 0.118 184.704;
--chart-3: 0.398 0.07 227.392;
--chart-4: 0.828 0.189 84.429;
--chart-5: 0.769 0.188 70.08;
--sidebar: 0.985 0 0;
--sidebar-foreground: 0.145 0 0;
--sidebar-primary: 0.205 0 0;
--sidebar-primary-foreground: 0.985 0 0;
--sidebar-accent: 0.97 0 0;
--sidebar-accent-foreground: 0.205 0 0;
--sidebar-border: 0.922 0 0;
--sidebar-ring: 0.708 0 0;
--chart-1: 173 58% 39%;
--chart-2: 12 76% 61%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--terminal-paste: rgba(255, 255, 255, 0.2);
--background: 0.145 0 0;
--foreground: 0.985 0 0;
--background: 0 0% 0%;
--foreground: 0 0% 98%;
--card: 0.205 0 0;
--card-foreground: 0.985 0 0;
--card: 240 4% 10%;
--card-foreground: 0 0% 98%;
--popover: 0.205 0 0;
--popover-foreground: 0.985 0 0;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0.922 0 0;
--primary-foreground: 0.205 0 0;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 0.269 0 0;
--secondary-foreground: 0.985 0 0;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0.269 0 0;
--muted-foreground: 0.708 0 0;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--accent: 0.269 0 0;
--accent-foreground: 0.985 0 0;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0.704 0.191 22.216;
--destructive-foreground: 0.985 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--border: 0.371 0 0;
--input: 0.371 0 0;
--ring: 0.556 0 0;
--border: 240 3.7% 15.9%;
--input: 240 4% 10%;
--ring: 240 4.9% 83.9%;
--overlay: rgba(0, 0, 0, 0.5);
--chart-1: 0.488 0.243 264.376;
--chart-2: 0.696 0.17 162.48;
--chart-3: 0.769 0.188 70.08;
--chart-4: 0.627 0.265 303.9;
--chart-5: 0.645 0.246 16.439;
--sidebar: 0.205 0 0;
--sidebar-foreground: 0.985 0 0;
--sidebar-primary: 0.488 0.243 264.376;
--sidebar-primary-foreground: 0.985 0 0;
--sidebar-accent: 0.269 0 0;
--sidebar-accent-foreground: 0.985 0 0;
--sidebar-border: 0.371 0 0;
--sidebar-ring: 0.556 0 0;
--chart-1: 220 70% 50%;
--chart-5: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-2: 340 75% 55%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
@@ -118,13 +118,13 @@
}
::-webkit-scrollbar-thumb {
background: oklch(var(--border));
background: hsl(var(--border));
border-radius: 0.3125rem;
}
* {
scrollbar-width: thin;
scrollbar-color: oklch(var(--border)) transparent;
scrollbar-color: hsl(var(--border)) transparent;
}
}
@@ -216,7 +216,7 @@
@layer utilities {
.custom-logs-scrollbar {
scrollbar-width: thin;
scrollbar-color: oklch(var(--muted-foreground)) transparent;
scrollbar-color: hsl(var(--muted-foreground)) transparent;
}
.custom-logs-scrollbar::-webkit-scrollbar {
@@ -229,12 +229,12 @@
}
.custom-logs-scrollbar::-webkit-scrollbar-thumb {
background-color: oklch(var(--muted-foreground) / 0.3);
background-color: hsl(var(--muted-foreground) / 0.3);
border-radius: 20px;
}
.custom-logs-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: oklch(var(--muted-foreground) / 0.5);
background-color: hsl(var(--muted-foreground) / 0.5);
}
}

View File

@@ -32,48 +32,48 @@ const config = {
"10xl": "105rem",
},
colors: {
border: "oklch(var(--border) / <alpha-value>)",
input: "oklch(var(--input) / <alpha-value>)",
ring: "oklch(var(--ring) / <alpha-value>)",
background: "oklch(var(--background) / <alpha-value>)",
foreground: "oklch(var(--foreground) / <alpha-value>)",
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "oklch(var(--primary) / <alpha-value>)",
foreground: "oklch(var(--primary-foreground) / <alpha-value>)",
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "oklch(var(--secondary) / <alpha-value>)",
foreground: "oklch(var(--secondary-foreground) / <alpha-value>)",
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "oklch(var(--destructive) / <alpha-value>)",
foreground: "oklch(var(--destructive-foreground) / <alpha-value>)",
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "oklch(var(--muted) / <alpha-value>)",
foreground: "oklch(var(--muted-foreground) / <alpha-value>)",
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "oklch(var(--accent) / <alpha-value>)",
foreground: "oklch(var(--accent-foreground) / <alpha-value>)",
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "oklch(var(--popover) / <alpha-value>)",
foreground: "oklch(var(--popover-foreground) / <alpha-value>)",
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "oklch(var(--card) / <alpha-value>)",
foreground: "oklch(var(--card-foreground) / <alpha-value>)",
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
sidebar: {
DEFAULT: "oklch(var(--sidebar) / <alpha-value>)",
foreground: "oklch(var(--sidebar-foreground) / <alpha-value>)",
primary: "oklch(var(--sidebar-primary) / <alpha-value>)",
"primary-foreground": "oklch(var(--sidebar-primary-foreground) / <alpha-value>)",
accent: "oklch(var(--sidebar-accent) / <alpha-value>)",
"accent-foreground": "oklch(var(--sidebar-accent-foreground) / <alpha-value>)",
border: "oklch(var(--sidebar-border) / <alpha-value>)",
ring: "oklch(var(--sidebar-ring) / <alpha-value>)",
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
borderRadius: {

View File

@@ -1,32 +1,21 @@
{
"name": "@dokploy/server",
"version": "1.0.0",
"main": "./dist/index.js",
"main": "./src/index.ts",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
".": "./src/index.ts",
"./db": {
"import": "./dist/db/index.js",
"import": "./src/db/index.ts",
"require": "./dist/db/index.cjs.js"
},
"./*": {
"import": "./dist/*",
"require": "./dist/*.cjs"
"./setup/*": {
"import": "./src/setup/*.ts",
"require": "./dist/setup/index.cjs.js"
},
"./dist": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./dist/db": {
"import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js"
},
"./dist/db/schema": {
"import": "./dist/db/schema/index.js",
"require": "./dist/db/schema/index.cjs.js"
"./constants": {
"import": "./src/constants/index.ts",
"require": "./dist/constants.cjs.js"
}
},
"scripts": {

View File

@@ -0,0 +1,104 @@
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
export type TemplateProps = {
userName: string;
verificationUrl: string;
};
export const VerifyEmailTemplate = ({
userName = "User",
verificationUrl = "https://app.dokploy.com/verify",
}: TemplateProps) => {
const previewText = "Verify your email address to get started with Dokploy";
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: "#007291",
},
},
},
}}
>
<Body className="bg-[#f4f4f5] my-auto mx-auto font-sans">
<Container className="my-[40px] mx-auto max-w-[520px]">
{/* Header */}
<Section className="bg-[#09090b] rounded-t-xl px-[40px] py-[32px] text-center">
<Img
src="https://raw.githubusercontent.com/Dokploy/website/refs/heads/main/apps/docs/public/logo-dokploy-blackpng.png"
width="190"
height="120"
alt="Dokploy"
className="my-0 mx-auto"
/>
</Section>
{/* Body */}
<Section className="bg-white px-[40px] py-[32px]">
<Heading className="text-[#09090b] text-[22px] font-semibold m-0 mb-[8px]">
Verify Your Email
</Heading>
<Text className="text-[#71717a] text-[14px] leading-[22px] m-0 mb-[24px]">
Hello {userName}, thank you for signing up for Dokploy. Please
verify your email address to activate your account.
</Text>
{/* CTA Button */}
<Section className="text-center mb-[24px]">
<Button
href={verificationUrl}
className="bg-[#09090b] rounded-lg text-white text-[14px] font-semibold no-underline text-center px-[24px] py-[12px]"
>
Verify Email Address
</Button>
</Section>
<Text className="text-[#a1a1aa] text-[13px] leading-[20px] m-0 text-center mb-[16px]">
If the button above doesn't work, copy and paste the following
link into your browser:
</Text>
<Text className="text-[#71717a] text-[12px] leading-[18px] m-0 text-center break-all">
{verificationUrl}
</Text>
</Section>
{/* Footer */}
<Section className="bg-[#fafafa] rounded-b-xl px-[40px] py-[24px] text-center border-t border-solid border-[#e4e4e7]">
<Text className="text-[#a1a1aa] text-[12px] leading-[18px] m-0">
This is an automated email from{" "}
<Link
href="https://dokploy.com"
className="text-[#71717a] underline"
>
Dokploy Cloud
</Link>
. If you didn't create an account, you can safely ignore this
email.
</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default VerifyEmailTemplate;

View File

@@ -21,7 +21,10 @@ import {
updateWebServerSettings,
} from "../services/web-server-settings";
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
import { sendEmail } from "../verification/send-verification-email";
import {
sendEmail,
sendVerificationEmail,
} from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils";
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
@@ -106,14 +109,13 @@ const { handler, api } = betterAuth({
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: true,
sendOnSignIn: true,
sendVerificationEmail: async ({ user, url }) => {
if (IS_CLOUD) {
await sendEmail({
await sendVerificationEmail({
userName: user.name || "User",
email: user.email,
subject: "Verify your email",
text: `
<p>Click the link to verify your email: <a href="${url}">Verify Email</a></p>
`,
verificationUrl: url,
});
}
},

View File

@@ -30,13 +30,9 @@ export const findPreviewDeploymentById = async (
with: {
domain: true,
application: {
with: {
server: true,
environment: {
with: {
project: true,
},
},
columns: {
applicationId: true,
serverId: true,
},
},
},

View File

@@ -1,4 +1,7 @@
import { renderAsync } from "@react-email/components";
import VerifyEmailTemplate from "../emails/emails/verify-email";
import { sendEmailNotification } from "../utils/notifications/utils";
export const sendEmail = async ({
email,
subject,
@@ -26,3 +29,25 @@ export const sendEmail = async ({
return true;
};
export const sendVerificationEmail = async ({
userName,
email,
verificationUrl,
}: {
userName: string;
email: string;
verificationUrl: string;
}) => {
const html = await renderAsync(
VerifyEmailTemplate({
userName: userName || "User",
verificationUrl,
}),
);
await sendEmail({
email,
subject: "Verify your email",
text: html,
});
};