Compare commits

..

1 Commits

Author SHA1 Message Date
Mauricio Siu
9bf4a97cee refactor: update color system to use oklch color format
- Changed color definitions in tailwind.config.ts and various components to use oklch format for improved color manipulation.
- Updated button backgrounds in multiple components to enhance visibility and consistency across light and dark themes.
- Adjusted chart color configurations to align with the new color system.
- Refined global CSS variables for better color management in light and dark modes.
2026-04-13 21:34:26 -06:00
83 changed files with 314 additions and 1359 deletions

View File

@@ -68,45 +68,3 @@ 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"

View File

@@ -1,80 +0,0 @@
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 domain
Use custom entrypoint for domina
<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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -329,7 +329,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -349,7 +349,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -316,7 +316,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -347,7 +347,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

@@ -36,10 +36,6 @@ 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(
() =>
@@ -221,24 +217,6 @@ 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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -331,7 +331,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -327,7 +327,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -317,7 +317,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!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-input",
"w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>
@@ -349,7 +349,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<Button
variant="outline"
className={cn(
" w-full justify-between !bg-input",
" w-full justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
!field.value && "text-muted-foreground",
)}
>

View File

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

View File

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

View File

@@ -1,112 +0,0 @@
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

@@ -1,119 +0,0 @@
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,8 +10,6 @@ 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";
@@ -125,14 +123,6 @@ 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

@@ -1,291 +0,0 @@
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: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
writeMb: {
label: "Write (MB)",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ interface Props {
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
color: "oklch(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="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -112,7 +112,7 @@ export const ContainerCPUChart = ({ data }: Props) => {
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
stroke="oklch(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -33,7 +33,7 @@ interface Props {
const chartConfig = {
memory: {
label: "Memory",
color: "hsl(var(--chart-2))",
color: "oklch(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="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -133,7 +133,7 @@ export const ContainerMemoryChart = ({ data }: Props) => {
dataKey="memory"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
stroke="oklch(var(--chart-2))"
strokeWidth={2}
/>
<ChartLegend

View File

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

View File

@@ -22,7 +22,7 @@ interface CPUChartProps {
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
color: "oklch(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="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopColor="oklch(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -99,7 +99,7 @@ export function CPUChart({ data }: CPUChartProps) {
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
stroke="oklch(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend

View File

@@ -29,14 +29,14 @@ export function DiskChart({ data }: RadialChartProps) {
const chartData = [
{
disk: 25,
fill: "hsl(var(--chart-2))",
fill: "oklch(var(--chart-2))",
},
];
const chartConfig = {
disk: {
label: "Disk",
color: "hsl(var(--chart-2))",
color: "oklch(var(--chart-2))",
},
} satisfies ChartConfig;
@@ -71,7 +71,7 @@ export function DiskChart({ data }: RadialChartProps) {
dataKey="disk"
background
cornerRadius={10}
fill="hsl(var(--chart-2))"
fill="oklch(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: "hsl(var(--chart-2))",
color: "oklch(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="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopColor="oklch(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -116,7 +116,7 @@ export function MemoryChart({ data }: MemoryChartProps) {
dataKey="memUsed"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
stroke="oklch(var(--chart-2))"
strokeWidth={2}
name="Memory"
/>

View File

@@ -22,11 +22,11 @@ interface NetworkChartProps {
const chartConfig = {
networkIn: {
label: "Network In",
color: "hsl(var(--chart-3))",
color: "oklch(var(--chart-3))",
},
networkOut: {
label: "Network Out",
color: "hsl(var(--chart-4))",
color: "oklch(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="hsl(var(--chart-3))"
stopColor="oklch(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-3))"
stopColor="oklch(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillNetworkOut" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-4))"
stopColor="oklch(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-4))"
stopColor="oklch(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
@@ -121,7 +121,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
dataKey="networkIn"
type="monotone"
fill="url(#fillNetworkIn)"
stroke="hsl(var(--chart-3))"
stroke="oklch(var(--chart-3))"
strokeWidth={2}
/>
<Area
@@ -129,7 +129,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
dataKey="networkOut"
type="monotone"
fill="url(#fillNetworkOut)"
stroke="hsl(var(--chart-4))"
stroke="oklch(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-input",
"w-full sm:w-[200px] justify-between !bg-transparent dark:!bg-input/30 !border !border-input !shadow-xs",
)}
>
{isLoadingTags

View File

@@ -166,7 +166,6 @@ 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) +
@@ -179,7 +178,6 @@ 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: "hsl(var(--chart-1))",
color: "oklch(var(--chart-1))",
},
} satisfies ChartConfig;
@@ -101,9 +101,9 @@ export const RequestDistributionChart = ({
<Area
dataKey="count"
type="monotone"
fill="hsl(var(--chart-1))"
fill="oklch(var(--chart-1))"
fillOpacity={0.4}
stroke="hsl(var(--chart-1))"
stroke="oklch(var(--chart-1))"
/>
</AreaChart>
</ChartContainer>

View File

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

View File

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

View File

@@ -19,7 +19,6 @@ import {
Forward,
GalleryVerticalEnd,
GitBranch,
House,
Key,
KeyRound,
Loader2,
@@ -149,12 +148,6 @@ 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/home");
router.push("/dashboard/projects");
}}
>
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/home",
callbackURL: "/dashboard/projects",
});
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: 0 0% 100%;
--foreground: 240 10% 3.9%;
--background: 1 0 0;
--foreground: 0.145 0 0;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--card: 1 0 0;
--card-foreground: 0.145 0 0;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--popover: 1 0 0;
--popover-foreground: 0.145 0 0;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--primary: 0.205 0 0;
--primary-foreground: 0.985 0 0;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--secondary: 0.97 0 0;
--secondary-foreground: 0.205 0 0;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--muted: 0.97 0 0;
--muted-foreground: 0.556 0 0;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--accent: 0.97 0 0;
--accent-foreground: 0.205 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--destructive: 0.577 0.245 27.325;
--destructive-foreground: 0.985 0 0;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--radius: 0.5rem;
--border: 0.922 0 0;
--input: 0.922 0 0;
--ring: 0.708 0 0;
--radius: 0.625rem;
/* Sidebar */
--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%;
--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;
/* Charts */
--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%;
--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;
}
/* ---------- Dark Mode ---------- */
.dark {
--background: 0 0% 0%;
--foreground: 0 0% 98%;
--background: 0.145 0 0;
--foreground: 0.985 0 0;
--card: 240 4% 10%;
--card-foreground: 0 0% 98%;
--card: 0.205 0 0;
--card-foreground: 0.985 0 0;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--popover: 0.205 0 0;
--popover-foreground: 0.985 0 0;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--primary: 0.922 0 0;
--primary-foreground: 0.205 0 0;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--secondary: 0.269 0 0;
--secondary-foreground: 0.985 0 0;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--muted: 0.269 0 0;
--muted-foreground: 0.708 0 0;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--accent: 0.269 0 0;
--accent-foreground: 0.985 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--destructive: 0.704 0.191 22.216;
--destructive-foreground: 0.985 0 0;
--border: 240 3.7% 15.9%;
--input: 240 4% 10%;
--ring: 240 4.9% 83.9%;
--border: 0.371 0 0;
--input: 0.371 0 0;
--ring: 0.556 0 0;
/* Sidebar */
--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%;
--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;
/* Charts */
--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%;
--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;
}
/* ---------- Custom Styles ---------- */

View File

@@ -116,14 +116,6 @@ 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 p-px",
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
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 items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<div className="flex h-9 items-center gap-2 border-b px-3" cmdk-input-wrapper="">
<Search className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"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",
"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",
className,
)}
{...props}
@@ -115,7 +115,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"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",
"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",
className,
)}
{...props}

View File

@@ -76,8 +76,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={inputType}
className={cn(
// 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",
"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",
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-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",
"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",
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_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
"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))]",
},
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-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",
"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",
className,
)}
{...props}
@@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"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",
"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",
)}
/>
</SwitchPrimitives.Root>

View File

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

View File

@@ -48,7 +48,7 @@ const MyApp = ({
disableTransitionOnChange
forcedTheme={Component.theme}
>
<NextTopLoader color="hsl(var(--sidebar-ring))" />
<NextTopLoader color="oklch(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/home"
href="/dashboard/projects"
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/home",
destination: "/dashboard/projects",
},
};
}

View File

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

View File

@@ -1,53 +0,0 @@
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/home",
destination: "/dashboard/projects",
},
};
}
@@ -122,7 +122,7 @@ export async function getServerSideProps(
return {
redirect: {
permanent: false,
destination: "/dashboard/home",
destination: "/dashboard/projects",
},
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,16 +82,6 @@ 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;
@@ -106,7 +96,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/home");
router.push("/dashboard/projects");
} catch {
toast.error("An error occurred while logging in");
} finally {
@@ -133,7 +123,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/home");
router.push("/dashboard/projects");
} catch {
toast.error("An error occurred while verifying 2FA code");
} finally {
@@ -163,7 +153,7 @@ export default function Home({ IS_CLOUD }: Props) {
}
toast.success("Logged in successfully");
router.push("/dashboard/home");
router.push("/dashboard/projects");
} catch {
toast.error("An error occurred while verifying backup code");
} finally {
@@ -408,7 +398,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/home",
destination: "/dashboard/projects",
},
};
}
@@ -437,7 +427,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
redirect: {
permanent: true,
destination: "/dashboard/home",
destination: "/dashboard/projects",
},
};
}

View File

@@ -139,7 +139,7 @@ const Invitation = ({
});
toast.success("Account created successfully");
router.push("/dashboard/home");
router.push("/dashboard/projects");
} 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/home",
destination: "/dashboard/projects",
},
};
}

View File

@@ -487,148 +487,6 @@ 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: 0 0% 100%;
--foreground: 240 10% 3.9%;
--background: 1 0 0;
--foreground: 0.145 0 0;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--card: 1 0 0;
--card-foreground: 0.145 0 0;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--popover: 1 0 0;
--popover-foreground: 0.145 0 0;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--primary: 0.205 0 0;
--primary-foreground: 0.985 0 0;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--secondary: 0.97 0 0;
--secondary-foreground: 0.205 0 0;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--muted: 0.97 0 0;
--muted-foreground: 0.556 0 0;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--accent: 0.97 0 0;
--accent-foreground: 0.205 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--destructive: 0.577 0.245 27.325;
--destructive-foreground: 0.985 0 0;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--border: 0.922 0 0;
--input: 0.922 0 0;
--ring: 0.708 0 0;
--radius: 0.5rem;
--radius: 0.625rem;
--overlay: rgba(0, 0, 0, 0.2);
--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%;
--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;
}
.dark {
--terminal-paste: rgba(255, 255, 255, 0.2);
--background: 0 0% 0%;
--foreground: 0 0% 98%;
--background: 0.145 0 0;
--foreground: 0.985 0 0;
--card: 240 4% 10%;
--card-foreground: 0 0% 98%;
--card: 0.205 0 0;
--card-foreground: 0.985 0 0;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--popover: 0.205 0 0;
--popover-foreground: 0.985 0 0;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--primary: 0.922 0 0;
--primary-foreground: 0.205 0 0;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--secondary: 0.269 0 0;
--secondary-foreground: 0.985 0 0;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--muted: 0.269 0 0;
--muted-foreground: 0.708 0 0;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--accent: 0.269 0 0;
--accent-foreground: 0.985 0 0;
--destructive: 0 84.2% 50.2%;
--destructive-foreground: 0 0% 98%;
--destructive: 0.704 0.191 22.216;
--destructive-foreground: 0.985 0 0;
--border: 240 3.7% 15.9%;
--input: 240 4% 10%;
--ring: 240 4.9% 83.9%;
--border: 0.371 0 0;
--input: 0.371 0 0;
--ring: 0.556 0 0;
--overlay: rgba(0, 0, 0, 0.5);
--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%;
--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;
}
}
@@ -118,13 +118,13 @@
}
::-webkit-scrollbar-thumb {
background: hsl(var(--border));
background: oklch(var(--border));
border-radius: 0.3125rem;
}
* {
scrollbar-width: thin;
scrollbar-color: hsl(var(--border)) transparent;
scrollbar-color: oklch(var(--border)) transparent;
}
}
@@ -216,7 +216,7 @@
@layer utilities {
.custom-logs-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--muted-foreground)) transparent;
scrollbar-color: oklch(var(--muted-foreground)) transparent;
}
.custom-logs-scrollbar::-webkit-scrollbar {
@@ -229,12 +229,12 @@
}
.custom-logs-scrollbar::-webkit-scrollbar-thumb {
background-color: hsl(var(--muted-foreground) / 0.3);
background-color: oklch(var(--muted-foreground) / 0.3);
border-radius: 20px;
}
.custom-logs-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: hsl(var(--muted-foreground) / 0.5);
background-color: oklch(var(--muted-foreground) / 0.5);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,4 @@
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,
@@ -29,25 +26,3 @@ 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,
});
};