Compare commits

..

10 Commits

Author SHA1 Message Date
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
48 changed files with 795 additions and 272 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"

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

@@ -0,0 +1,79 @@
name: Sync version to MCP and CLI repos
on:
release:
types: [published]
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)
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 mcp-repo
cd mcp-repo
# Bump version
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
mv package.json.tmp package.json
# Regenerate tools from latest OpenAPI spec
npm install -g pnpm
pnpm install
pnpm run fetch-openapi
pnpm run generate
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 cli-repo
cd cli-repo
# Bump version
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
# Copy latest openapi spec and regenerate commands
cp ../openapi.json ./openapi.json
npm install -g pnpm
pnpm install
pnpm run generate
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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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;

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

@@ -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,
});
};