mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 13:45:23 +02:00
Compare commits
1 Commits
feature/ma
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bf4a97cee |
79
.github/workflows/dokploy.yml
vendored
79
.github/workflows/dokploy.yml
vendored
@@ -138,8 +138,6 @@ jobs:
|
||||
needs: [combine-manifests]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -162,80 +160,3 @@ jobs:
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
sync-version:
|
||||
needs: [generate-release]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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
|
||||
|
||||
jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
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 ${{ needs.generate-release.outputs.version }}" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
--allow-empty
|
||||
git push
|
||||
|
||||
echo "✅ MCP repo synced to version ${{ needs.generate-release.outputs.version }}"
|
||||
|
||||
- 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
|
||||
|
||||
jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
cp ${{ github.workspace }}/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 ${{ needs.generate-release.outputs.version }}" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
--allow-empty
|
||||
git push
|
||||
|
||||
echo "✅ CLI repo synced to version ${{ needs.generate-release.outputs.version }}"
|
||||
|
||||
- name: Sync version to SDK repository
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git /tmp/sdk-repo
|
||||
cd /tmp/sdk-repo
|
||||
|
||||
jq --arg v "${{ needs.generate-release.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
|
||||
cp ${{ github.workspace }}/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 ${{ needs.generate-release.outputs.version }}" \
|
||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||
--allow-empty
|
||||
git push
|
||||
|
||||
echo "✅ SDK repo synced to version ${{ needs.generate-release.outputs.version }}"
|
||||
|
||||
63
.github/workflows/sync-openapi-docs.yml
vendored
63
.github/workflows/sync-openapi-docs.yml
vendored
@@ -68,66 +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"
|
||||
|
||||
- name: Sync to SDK repository
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git sdk-repo
|
||||
|
||||
cd sdk-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 SDK repository successfully"
|
||||
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -4,8 +4,5 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \
|
||||
HEALTHCHECK --interval=10s --timeout=3s --retries=10 \
|
||||
CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1
|
||||
|
||||
CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]
|
||||
|
||||
8403
api-1.json
8403
api-1.json
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,3 @@
|
||||
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Managed Servers (Dokploy Cloud only) — API token from https://hpanel.hostinger.com/profile/api
|
||||
HOSTINGER_API_KEY=
|
||||
|
||||
@@ -494,49 +494,4 @@ describe("processTemplate", () => {
|
||||
expect(result.mounts).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isolated deployment config", () => {
|
||||
it("should default to isolated=true when not specified", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
domains: [],
|
||||
env: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(template.config.isolated).toBeUndefined();
|
||||
// undefined !== false => isolatedDeployment = true
|
||||
expect(template.config.isolated !== false).toBe(true);
|
||||
});
|
||||
|
||||
it("should be isolated when isolated=true is explicitly set", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
isolated: true,
|
||||
domains: [],
|
||||
env: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(template.config.isolated !== false).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable isolated deployment when isolated=false", () => {
|
||||
const template: CompleteTemplate = {
|
||||
metadata: {} as any,
|
||||
variables: {},
|
||||
config: {
|
||||
isolated: false,
|
||||
domains: [],
|
||||
env: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(template.config.isolated !== false).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,9 @@ describe("helpers functions", () => {
|
||||
const domain = processValue("${domain}", {}, mockSchema);
|
||||
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
|
||||
expect(
|
||||
domain.endsWith(`${mockSchema.serverIp.replaceAll(".", "-")}.sslip.io`),
|
||||
domain.endsWith(
|
||||
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,9 +21,9 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { RouterOutputs } from "@/utils/api";
|
||||
import { DnsHelperModal } from "./dns-helper-modal";
|
||||
import { AddDomain } from "./handle-domain";
|
||||
import type { ValidationStates } from "./show-domains";
|
||||
import { AddDomain } from "./handle-domain";
|
||||
import { DnsHelperModal } from "./dns-helper-modal";
|
||||
|
||||
export type Domain =
|
||||
| RouterOutputs["domain"]["byApplicationId"][0]
|
||||
@@ -168,7 +168,7 @@ export const createColumns = ({
|
||||
{domain.certificateType}
|
||||
</Badge>
|
||||
)}
|
||||
{!domain.host.includes("sslip.io") && (
|
||||
{!domain.host.includes("traefik.me") && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -256,7 +256,7 @@ export const createColumns = ({
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{!domain.host.includes("sslip.io") && (
|
||||
{!domain.host.includes("traefik.me") && (
|
||||
<DnsHelperModal
|
||||
domain={{
|
||||
host: domain.host,
|
||||
|
||||
@@ -225,7 +225,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
const https = form.watch("https");
|
||||
const domainType = form.watch("domainType");
|
||||
const host = form.watch("host");
|
||||
const isTraefikMeDomain = host?.includes("sslip.io") || false;
|
||||
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -513,7 +513,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
{!canGenerateTraefikMeDomains &&
|
||||
field.value.includes("sslip.io") && (
|
||||
field.value.includes("traefik.me") && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link
|
||||
@@ -524,12 +524,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to make your sslip.io domain work.
|
||||
to make your traefik.me domain work.
|
||||
</AlertBlock>
|
||||
)}
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> sslip.io is a public HTTP
|
||||
<strong>Note:</strong> traefik.me is a public HTTP
|
||||
service and does not support SSL/HTTPS. HTTPS and
|
||||
certificate options will not have any effect.
|
||||
</AlertBlock>
|
||||
@@ -567,7 +567,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate sslip.io domain</p>
|
||||
<p>Generate traefik.me domain</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -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>
|
||||
|
||||
@@ -425,7 +425,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{!item.host.includes("sslip.io") && (
|
||||
{!item.host.includes("traefik.me") && (
|
||||
<DnsHelperModal
|
||||
domain={{
|
||||
host: item.host,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
|
||||
slug: z.string().optional(),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
enableSubmodules: z.boolean().optional(),
|
||||
@@ -245,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",
|
||||
)}
|
||||
>
|
||||
@@ -333,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -42,10 +41,7 @@ const GitProviderSchema = z.object({
|
||||
repositoryURL: z.string().min(1, {
|
||||
message: "Repository URL is required",
|
||||
}),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch required"),
|
||||
sshKey: z.string().optional(),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
enableSubmodules: z.boolean().default(false),
|
||||
@@ -111,103 +107,110 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repositoryURL"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2 lg:col-span-3">
|
||||
<div className="flex items-center justify-between h-5">
|
||||
<FormLabel>Repository URL</FormLabel>
|
||||
{field.value?.startsWith("https://") && (
|
||||
<Link
|
||||
href={field.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<GitIcon className="h-4 w-4" />
|
||||
<span>View Repository</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder="Repository URL" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="flex items-end col-span-2 gap-4">
|
||||
<div className="grow">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="repositoryURL"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center justify-between">
|
||||
<FormLabel>Repository URL</FormLabel>
|
||||
{field.value?.startsWith("https://") && (
|
||||
<Link
|
||||
href={field.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<GitIcon className="h-4 w-4" />
|
||||
<span>View Repository</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input placeholder="Repository URL" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{sshKeys && sshKeys.length > 0 ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshKey"
|
||||
render={({ field }) => (
|
||||
<FormItem className="basis-40">
|
||||
<FormLabel className="w-full inline-flex justify-between">
|
||||
SSH Key
|
||||
<LockIcon className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
key={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{sshKeys?.map((sshKey) => (
|
||||
<SelectItem
|
||||
key={sshKey.sshKeyId}
|
||||
value={sshKey.sshKeyId}
|
||||
>
|
||||
{sshKey.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => router.push("/dashboard/settings/ssh-keys")}
|
||||
type="button"
|
||||
>
|
||||
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
{sshKeys && sshKeys.length > 0 ? (
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sshKey"
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2 lg:col-span-1">
|
||||
<FormLabel className="w-full inline-flex justify-between">
|
||||
SSH Key
|
||||
<LockIcon className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
<FormItem>
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
key={field.value}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a key" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{sshKeys?.map((sshKey) => (
|
||||
<SelectItem
|
||||
key={sshKey.sshKeyId}
|
||||
value={sshKey.sshKeyId}
|
||||
>
|
||||
{sshKey.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input placeholder="Branch" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => router.push("/dashboard/settings/ssh-keys")}
|
||||
type="button"
|
||||
className="col-span-2 lg:col-span-1 lg:mt-7"
|
||||
>
|
||||
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="branch"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormLabel>Branch</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Branch" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="buildPath"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2">
|
||||
<FormItem>
|
||||
<FormLabel>Build Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/" {...field} />
|
||||
@@ -220,7 +223,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="watchPaths"
|
||||
render={({ field }) => (
|
||||
<FormItem className="col-span-2 lg:col-span-4">
|
||||
<FormItem className="md:col-span-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -73,10 +72,7 @@ const GiteaProviderSchema = z.object({
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||
watchPaths: z.array(z.string()).default([]),
|
||||
enableSubmodules: z.boolean().optional(),
|
||||
@@ -258,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",
|
||||
)}
|
||||
>
|
||||
@@ -353,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
githubId: z.string().min(1, "Github Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||
@@ -233,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",
|
||||
)}
|
||||
>
|
||||
@@ -320,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
|
||||
id: z.number().nullable(),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
enableSubmodules: z.boolean().default(false),
|
||||
@@ -254,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",
|
||||
)}
|
||||
>
|
||||
@@ -351,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -58,7 +58,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 lg:flex lg:flex-row lg:flex-wrap gap-4">
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||
{canDeploy && (
|
||||
<DialogAction
|
||||
@@ -274,14 +274,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2 col-span-2"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
{canUpdateService && (
|
||||
<div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1">
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Autodeploy</span>
|
||||
<Switch
|
||||
aria-label="Toggle autodeploy"
|
||||
@@ -305,7 +305,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
)}
|
||||
|
||||
{canUpdateService && (
|
||||
<div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1">
|
||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||
<span className="text-sm font-medium">Clean Cache</span>
|
||||
<Switch
|
||||
aria-label="Toggle clean cache"
|
||||
|
||||
@@ -87,7 +87,7 @@ export const AddPreviewDomain = ({
|
||||
});
|
||||
|
||||
const host = form.watch("host");
|
||||
const isTraefikMeDomain = host?.includes("sslip.io") || false;
|
||||
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -162,7 +162,7 @@ export const AddPreviewDomain = ({
|
||||
<FormItem>
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> sslip.io is a public HTTP
|
||||
<strong>Note:</strong> traefik.me is a public HTTP
|
||||
service and does not support SSL/HTTPS. HTTPS and
|
||||
certificate options will not have any effect.
|
||||
</AlertBlock>
|
||||
@@ -202,7 +202,7 @@ export const AddPreviewDomain = ({
|
||||
sideOffset={5}
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate sslip.io domain</p>
|
||||
<p>Generate traefik.me domain</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -88,7 +88,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
env: "",
|
||||
wildcardDomain: "*.sslip.io",
|
||||
wildcardDomain: "*.traefik.me",
|
||||
port: 3000,
|
||||
previewLimit: 3,
|
||||
previewLabels: [],
|
||||
@@ -102,7 +102,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
|
||||
const previewHttps = form.watch("previewHttps");
|
||||
const wildcardDomain = form.watch("wildcardDomain");
|
||||
const isTraefikMeDomain = wildcardDomain?.includes("sslip.io") || false;
|
||||
const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
||||
@@ -114,7 +114,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
env: data.previewEnv || "",
|
||||
buildArgs: data.previewBuildArgs || "",
|
||||
buildSecrets: data.previewBuildSecrets || "",
|
||||
wildcardDomain: data.previewWildcard || "*.sslip.io",
|
||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||
port: data.previewPort || 3000,
|
||||
previewLabels: data.previewLabels || [],
|
||||
previewLimit: data.previewLimit || 3,
|
||||
@@ -173,7 +173,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
<div className="grid gap-4">
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> sslip.io is a public HTTP service and
|
||||
<strong>Note:</strong> traefik.me is a public HTTP service and
|
||||
does not support SSL/HTTPS. HTTPS and certificate options will
|
||||
not have any effect.
|
||||
</AlertBlock>
|
||||
@@ -192,7 +192,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Wildcard Domain</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="*.sslip.io" {...field} />
|
||||
<Input placeholder="*.traefik.me" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -80,7 +80,6 @@ export const commonCronExpressions = [
|
||||
const formSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
description: z.string().optional(),
|
||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||
shellType: z.enum(["bash", "sh"]).default("bash"),
|
||||
command: z.string(),
|
||||
@@ -225,7 +224,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
resolver: standardSchemaResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
cronExpression: "",
|
||||
shellType: "bash",
|
||||
command: "",
|
||||
@@ -265,7 +263,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
if (scheduleId && schedule) {
|
||||
form.reset({
|
||||
name: schedule.name,
|
||||
description: schedule.description || "",
|
||||
cronExpression: schedule.cronExpression,
|
||||
shellType: schedule.shellType,
|
||||
command: schedule.command,
|
||||
@@ -482,26 +479,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Backs up the database every day at midnight"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional description of what this schedule does
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ScheduleFormField
|
||||
name="cronExpression"
|
||||
formControl={form.control}
|
||||
@@ -534,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -125,11 +125,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
{schedule.enabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
{schedule.description && (
|
||||
<p className="text-xs text-muted-foreground/70 [overflow-wrap:anywhere] line-clamp-2">
|
||||
{schedule.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
|
||||
<Badge
|
||||
variant="outline"
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -2,10 +2,6 @@ import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
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";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -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"
|
||||
|
||||
@@ -49,12 +49,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
const composeFile = form.watch("composeFile");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && !composeFile) {
|
||||
form.reset({
|
||||
composeFile: data.composeFile || "",
|
||||
});
|
||||
}
|
||||
}, [form, data]);
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.composeFile !== undefined) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
|
||||
slug: z.string().optional(),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
enableSubmodules: z.boolean().default(false),
|
||||
@@ -247,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",
|
||||
)}
|
||||
>
|
||||
@@ -335,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -42,10 +41,7 @@ const GitProviderSchema = z.object({
|
||||
repositoryURL: z.string().min(1, {
|
||||
message: "Repository URL is required",
|
||||
}),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch required"),
|
||||
sshKey: z.string().optional(),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
enableSubmodules: z.boolean().default(false),
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import { CheckIcon, ChevronsUpDown, Plus, X, HelpCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -58,10 +57,7 @@ const GiteaProviderSchema = z.object({
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
enableSubmodules: z.boolean().default(false),
|
||||
@@ -244,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",
|
||||
)}
|
||||
>
|
||||
@@ -331,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
|
||||
owner: z.string().min(1, "Owner is required"),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
githubId: z.string().min(1, "Github Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||
@@ -234,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",
|
||||
)}
|
||||
>
|
||||
@@ -321,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
|
||||
gitlabPathNamespace: z.string().min(1),
|
||||
})
|
||||
.required(),
|
||||
branch: z
|
||||
.string()
|
||||
.min(1, "Branch is required")
|
||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
||||
branch: z.string().min(1, "Branch is required"),
|
||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||
watchPaths: z.array(z.string()).optional(),
|
||||
enableSubmodules: z.boolean().default(false),
|
||||
@@ -256,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",
|
||||
)}
|
||||
>
|
||||
@@ -353,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -288,6 +288,7 @@ export const RestoreBackup = ({
|
||||
toast.error("Please select a database type");
|
||||
return;
|
||||
}
|
||||
console.log({ data });
|
||||
setIsDeploying(true);
|
||||
};
|
||||
|
||||
@@ -345,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",
|
||||
)}
|
||||
>
|
||||
@@ -427,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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
"use client";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {
|
||||
Bot,
|
||||
Check,
|
||||
Copy,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
Settings,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
@@ -39,7 +30,6 @@ const MAX_LOG_LINES = 200;
|
||||
export function AnalyzeLogs({ logs, context }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [aiId, setAiId] = useState<string>("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
|
||||
enabled: open,
|
||||
});
|
||||
@@ -62,15 +52,6 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
||||
mutate({ aiId, logs: logsText, context });
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!data?.analysis) return;
|
||||
const success = copy(data.analysis);
|
||||
if (success) {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
@@ -90,7 +71,7 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
||||
disabled={logs.length === 0}
|
||||
title="Analyze logs with AI"
|
||||
>
|
||||
<Bot className="mr-2 size-4" />
|
||||
<Bot className="mr-2 h-4 w-4" />
|
||||
AI
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -187,18 +168,6 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
||||
)}
|
||||
Re-analyze
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
title="Copy analysis to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
||||
@@ -347,13 +347,11 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
title={isPaused ? "Resume logs" : "Pause logs"}
|
||||
>
|
||||
{isPaused ? (
|
||||
<Play className="size-4" />
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Pause className="size-4" />
|
||||
<Pause className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className="hidden lg:ml-2 lg:inline">
|
||||
{isPaused ? "Resume" : "Pause"}
|
||||
</span>
|
||||
{isPaused ? "Resume" : "Pause"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -364,13 +362,11 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
title="Copy logs to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-4" />
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="size-4" />
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
<span className="hidden lg:ml-2 lg:inline">
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</span>
|
||||
Copy
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -378,18 +374,17 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
className="h-9 sm:w-auto w-full"
|
||||
onClick={handleDownload}
|
||||
disabled={filteredLogs.length === 0 || !data?.Name}
|
||||
title="Download logs as text file"
|
||||
>
|
||||
<DownloadIcon className="size-4" />
|
||||
<span className="hidden lg:ml-2 lg:inline">Download logs</span>
|
||||
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||
Download logs
|
||||
</Button>
|
||||
<AnalyzeLogs logs={filteredLogs} context="runtime" />
|
||||
</div>
|
||||
</div>
|
||||
{isPaused && (
|
||||
<AlertBlock type="warning" className="items-center">
|
||||
<AlertBlock type="warning">
|
||||
<div className="flex items-center gap-2">
|
||||
<Pause className="size-4" />
|
||||
<Pause className="h-4 w-4" />
|
||||
<span>
|
||||
Logs paused
|
||||
{messageBuffer.length > 0 && (
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
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 { 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>
|
||||
);
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
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 { 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>
|
||||
);
|
||||
};
|
||||
@@ -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 || ""}
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import {
|
||||
type UploadFileToContainer,
|
||||
uploadFileToContainerSchema,
|
||||
type UploadFileToContainer,
|
||||
} from "@/utils/schema";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { toast } from "sonner";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mariadbId: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { toast } from "sonner";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mongoId: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
const chartConfig = {
|
||||
usage: {
|
||||
label: "CPU Usage",
|
||||
color: "hsl(var(--chart-1))",
|
||||
color: "oklch(var(--chart-1))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Props {
|
||||
const chartConfig = {
|
||||
usedGb: {
|
||||
label: "Used (GB)",
|
||||
color: "hsl(var(--chart-3))",
|
||||
color: "oklch(var(--chart-3))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -19,7 +19,7 @@ interface Props {
|
||||
const chartConfig = {
|
||||
usage: {
|
||||
label: "Memory (GB)",
|
||||
color: "hsl(var(--chart-2))",
|
||||
color: "oklch(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { toast } from "sonner";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
mysqlId: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { toast } from "sonner";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
postgresId: string;
|
||||
|
||||
@@ -632,6 +632,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
control={form.control}
|
||||
name="enableNamespaces"
|
||||
render={({ field }) => {
|
||||
console.log(field.value);
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Enable Namespaces</FormLabel>
|
||||
|
||||
@@ -1,494 +0,0 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { Code2, FileInput, Globe2, HardDrive, HelpCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { api } from "@/utils/api";
|
||||
import { APP_NAME_MESSAGE, APP_NAME_REGEX } from "@/utils/schema";
|
||||
|
||||
const AddImportSchema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
appName: z
|
||||
.string()
|
||||
.min(1, { message: "App name is required" })
|
||||
.regex(APP_NAME_REGEX, { message: APP_NAME_MESSAGE }),
|
||||
base64: z.string().min(1, { message: "Base64 content is required" }),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddImport = z.infer<typeof AddImportSchema>;
|
||||
|
||||
type TemplateInfo = {
|
||||
compose: string;
|
||||
template: {
|
||||
domains: Array<{
|
||||
serviceName: string;
|
||||
port: number;
|
||||
path?: string;
|
||||
host?: string;
|
||||
}>;
|
||||
envs: string[];
|
||||
mounts: Array<{ filePath: string; content: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
environmentId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddImport = ({ environmentId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [mountOpen, setMountOpen] = useState(false);
|
||||
const [selectedMount, setSelectedMount] = useState<{
|
||||
filePath: string;
|
||||
content: string;
|
||||
} | null>(null);
|
||||
const [templateInfo, setTemplateInfo] = useState<TemplateInfo | null>(null);
|
||||
|
||||
const slug = slugify(projectName);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const shouldShowServerDropdown = !!(servers && servers.length > 0);
|
||||
|
||||
const { mutateAsync: previewTemplate, isPending: isProcessing } =
|
||||
api.compose.previewTemplate.useMutation();
|
||||
const { mutateAsync: createCompose, isPending: isCreating } =
|
||||
api.compose.create.useMutation();
|
||||
const { mutateAsync: importCompose, isPending: isImporting } =
|
||||
api.compose.import.useMutation();
|
||||
|
||||
const form = useForm<AddImport>({
|
||||
defaultValues: { name: "", appName: `${slug}-`, base64: "" },
|
||||
resolver: zodResolver(AddImportSchema),
|
||||
});
|
||||
|
||||
const resetAll = () => {
|
||||
form.reset({ name: "", appName: `${slug}-`, base64: "" });
|
||||
setTemplateInfo(null);
|
||||
setPreviewOpen(false);
|
||||
setMountOpen(false);
|
||||
setSelectedMount(null);
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) resetAll();
|
||||
setVisible(open);
|
||||
};
|
||||
|
||||
const handleLoad = async (data: AddImport) => {
|
||||
try {
|
||||
const result = await previewTemplate({
|
||||
appName: data.appName,
|
||||
base64: data.base64.trim(),
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
});
|
||||
setTemplateInfo(result);
|
||||
setPreviewOpen(true);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Error processing template",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
const data = form.getValues();
|
||||
try {
|
||||
const compose = await createCompose({
|
||||
name: data.name,
|
||||
appName: data.appName,
|
||||
environmentId,
|
||||
composeType: "docker-compose",
|
||||
serverId: data.serverId === "dokploy" ? undefined : data.serverId,
|
||||
});
|
||||
await importCompose({
|
||||
composeId: compose.composeId,
|
||||
base64: data.base64.trim(),
|
||||
});
|
||||
toast.success("Compose imported successfully");
|
||||
await utils.environment.one.invalidate({ environmentId });
|
||||
resetAll();
|
||||
setVisible(false);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Error importing compose",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelPreview = () => {
|
||||
setPreviewOpen(false);
|
||||
setTemplateInfo(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={visible} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<FileInput className="size-4 text-muted-foreground" />
|
||||
<span>Import</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Import Compose</DialogTitle>
|
||||
<DialogDescription>
|
||||
Paste a base64-encoded compose export to preview and import it
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-import"
|
||||
onSubmit={form.handleSubmit(handleLoad)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="My App"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value || "";
|
||||
form.setValue(
|
||||
"appName",
|
||||
`${slug}-${slugify(val.trim())}`,
|
||||
);
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{shouldShowServerDropdown && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serverId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server {!isCloud ? "(Optional)" : ""}
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
If no server is selected, the compose will be
|
||||
deployed on the server where the user is logged
|
||||
in.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!isCloud ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
Default
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
{servers?.map((server) => (
|
||||
<SelectItem
|
||||
key={server.serverId}
|
||||
value={server.serverId}
|
||||
>
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{server.name}</span>
|
||||
<span className="text-muted-foreground text-xs self-center">
|
||||
{server.ipAddress}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers (
|
||||
{(servers?.length ?? 0) + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="my-app" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="base64"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Configuration (Base64)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Paste your base64-encoded compose export here..."
|
||||
className="font-mono resize-none h-32"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
isLoading={isCreating || isProcessing}
|
||||
>
|
||||
Load
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Preview modal */}
|
||||
<Dialog
|
||||
open={previewOpen}
|
||||
onOpenChange={(open) => !open && handleCancelPreview()}
|
||||
>
|
||||
<DialogContent className="max-w-[60vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
Template Information
|
||||
</DialogTitle>
|
||||
<DialogDescription className="space-y-2">
|
||||
<p>Review the template information before importing</p>
|
||||
<AlertBlock type="warning">
|
||||
Warning: This will remove all existing environment variables,
|
||||
mounts, and domains from this service.
|
||||
</AlertBlock>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code2 className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Docker Compose</h3>
|
||||
</div>
|
||||
<CodeEditor
|
||||
language="yaml"
|
||||
value={templateInfo?.compose || ""}
|
||||
className="font-mono"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
{templateInfo?.template.domains &&
|
||||
templateInfo.template.domains.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe2 className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Domains</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{templateInfo.template.domains.map((domain, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm"
|
||||
>
|
||||
<div className="font-medium">
|
||||
{domain.serviceName}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div>Port: {domain.port}</div>
|
||||
{domain.host && <div>Host: {domain.host}</div>}
|
||||
{domain.path && <div>Path: {domain.path}</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{templateInfo?.template.envs &&
|
||||
templateInfo.template.envs.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code2 className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">
|
||||
Environment Variables
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{templateInfo.template.envs.map((env, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
|
||||
>
|
||||
{env}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{templateInfo?.template.mounts &&
|
||||
templateInfo.template.mounts.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Mounts</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{templateInfo.template.mounts.map((mount, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-card p-2 font-mono text-sm hover:bg-accent cursor-pointer transition-colors"
|
||||
onClick={() => {
|
||||
setSelectedMount(mount);
|
||||
setMountOpen(true);
|
||||
}}
|
||||
>
|
||||
{mount.filePath}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button variant="outline" onClick={handleCancelPreview}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button isLoading={isImporting} onClick={handleImport}>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Mount content modal */}
|
||||
<Dialog open={mountOpen} onOpenChange={setMountOpen}>
|
||||
<DialogContent className="max-w-[50vw]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
{selectedMount?.filePath}
|
||||
</DialogTitle>
|
||||
<DialogDescription>Mount File Content</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="h-[45vh] pr-4">
|
||||
<CodeEditor
|
||||
language="yaml"
|
||||
value={selectedMount?.content || ""}
|
||||
className="font-mono"
|
||||
readOnly
|
||||
/>
|
||||
</ScrollArea>
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button onClick={() => setMountOpen(false)}>Close</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
Bookmark,
|
||||
BookText,
|
||||
Bookmark,
|
||||
CheckIcon,
|
||||
ChevronsUpDown,
|
||||
Globe,
|
||||
@@ -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
|
||||
|
||||
@@ -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) +
|
||||
@@ -344,7 +342,7 @@ export const ShowProjects = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border flex flex-col">
|
||||
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
|
||||
<span className="flex flex-col gap-1.5 ">
|
||||
@@ -491,7 +489,7 @@ export const ShowProjects = () => {
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter className="pt-4 mt-auto">
|
||||
<CardFooter className="pt-4">
|
||||
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||
<DateTooltip date={project.createdAt}>
|
||||
Created
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { toast } from "sonner";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
redisId: string;
|
||||
|
||||
@@ -79,11 +79,8 @@ export const columns: ColumnDef<LogEntry>[] = [
|
||||
: log.RequestPath}
|
||||
</div>
|
||||
<div className="flex flex-row gap-3 w-full">
|
||||
<Badge
|
||||
variant={getStatusColor(log.OriginStatus || log.DownstreamStatus)}
|
||||
>
|
||||
Status:{" "}
|
||||
{formatStatusLabel(log.OriginStatus || log.DownstreamStatus)}
|
||||
<Badge variant={getStatusColor(log.OriginStatus)}>
|
||||
Status: {formatStatusLabel(log.OriginStatus)}
|
||||
</Badge>
|
||||
<Badge variant={"secondary"}>
|
||||
Exec Time: {formatDuration(log.Duration)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -185,7 +185,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||
<Input
|
||||
placeholder="Filter by hostname..."
|
||||
placeholder="Filter by name..."
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
className="md:max-w-sm"
|
||||
|
||||
@@ -167,7 +167,7 @@ export const SearchCommand = () => {
|
||||
<CommandGroup heading={"Application"} hidden={true}>
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
router.push("/dashboard/home");
|
||||
router.push("/dashboard/projects");
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreditCard, FileText, Server } from "lucide-react";
|
||||
import { CreditCard, FileText } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
@@ -17,11 +17,6 @@ const navigationItems = [
|
||||
href: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "Managed Servers",
|
||||
href: "/dashboard/settings/managed-servers",
|
||||
icon: Server,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/settings/invoices",
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Loader2,
|
||||
MinusIcon,
|
||||
PlusIcon,
|
||||
Server,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { NumberInput } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -83,11 +82,6 @@ const navigationItems = [
|
||||
href: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "Managed Servers",
|
||||
href: "/dashboard/settings/managed-servers",
|
||||
icon: Server,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/settings/invoices",
|
||||
|
||||
@@ -1,493 +0,0 @@
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
CreditCard,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Loader2,
|
||||
Plus,
|
||||
Server,
|
||||
Trash2,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Subscription",
|
||||
href: "/dashboard/settings/billing",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "Managed Servers",
|
||||
href: "/dashboard/settings/managed-servers",
|
||||
icon: Server,
|
||||
},
|
||||
{
|
||||
name: "Invoices",
|
||||
href: "/dashboard/settings/invoices",
|
||||
icon: FileText,
|
||||
},
|
||||
];
|
||||
|
||||
const STATUS_MAP: Record<
|
||||
string,
|
||||
{
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
variant: "default" | "secondary" | "destructive" | "outline";
|
||||
}
|
||||
> = {
|
||||
pending: {
|
||||
label: "Pending",
|
||||
icon: <Clock className="size-3" />,
|
||||
variant: "secondary",
|
||||
},
|
||||
provisioning: {
|
||||
label: "Provisioning",
|
||||
icon: <Loader2 className="size-3 animate-spin" />,
|
||||
variant: "secondary",
|
||||
},
|
||||
configuring: {
|
||||
label: "Installing Dokploy",
|
||||
icon: <Loader2 className="size-3 animate-spin" />,
|
||||
variant: "secondary",
|
||||
},
|
||||
ready: {
|
||||
label: "Ready",
|
||||
icon: <CheckCircle2 className="size-3" />,
|
||||
variant: "default",
|
||||
},
|
||||
error: {
|
||||
label: "Error",
|
||||
icon: <XCircle className="size-3" />,
|
||||
variant: "destructive",
|
||||
},
|
||||
terminating: {
|
||||
label: "Terminating",
|
||||
icon: <Loader2 className="size-3 animate-spin" />,
|
||||
variant: "secondary",
|
||||
},
|
||||
terminated: {
|
||||
label: "Terminated",
|
||||
icon: <AlertCircle className="size-3" />,
|
||||
variant: "outline",
|
||||
},
|
||||
};
|
||||
|
||||
function formatSpecs(cpus: number, memoryMb: number, diskMb: number, bandwidthMb: number) {
|
||||
const bandwidthTb = bandwidthMb / 1024 / 1024;
|
||||
const bandwidthLabel = bandwidthTb >= 1 ? `${bandwidthTb.toFixed(0)} TB` : `${Math.round(bandwidthMb / 1024)} GB`;
|
||||
return `${cpus} vCPU · ${Math.round(memoryMb / 1024)} GB RAM · ${Math.round(diskMb / 1024)} GB NVMe · ${bandwidthLabel} bandwidth`;
|
||||
}
|
||||
|
||||
function centsToDisplay(cents: number) {
|
||||
return (cents / 100).toFixed(2).replace(/\.00$/, "");
|
||||
}
|
||||
|
||||
function OrderServerDialog({ onSuccess }: { onSuccess: () => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedPlan, setSelectedPlan] = useState<string>("");
|
||||
const [selectedDc, setSelectedDc] = useState<string>("");
|
||||
const [isAnnual, setIsAnnual] = useState(false);
|
||||
|
||||
const { data: plans, isLoading: loadingPlans } =
|
||||
api.managedServer.getPlans.useQuery(undefined, { enabled: open });
|
||||
const { data: dataCenters, isLoading: loadingDcs } =
|
||||
api.managedServer.getDataCenters.useQuery(undefined, { enabled: open });
|
||||
|
||||
const isLoadingOptions = loadingPlans || loadingDcs;
|
||||
|
||||
const purchase = api.managedServer.purchase.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Server order placed! Provisioning will take ~5 minutes.");
|
||||
setOpen(false);
|
||||
onSuccess();
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
const plan = plans?.find((p) => p.id === selectedPlan);
|
||||
|
||||
const displayPrice = (p: NonNullable<typeof plan>) =>
|
||||
isAnnual
|
||||
? `$${centsToDisplay(p.dokployPriceCentsAnnual)}/yr`
|
||||
: `$${centsToDisplay(p.dokployPriceCentsMonthly)}/mo`;
|
||||
|
||||
const displayPriceSmall = (p: NonNullable<typeof plan>) =>
|
||||
isAnnual
|
||||
? `$${centsToDisplay(Math.round(p.dokployPriceCentsAnnual / 12))}/mo billed annually`
|
||||
: `$${centsToDisplay(p.dokployPriceCentsAnnual)}/yr if annual`;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="size-4 mr-2" />
|
||||
Order Server
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Order a Managed Server</DialogTitle>
|
||||
<DialogDescription>
|
||||
We'll provision and configure a server for you automatically. Ready
|
||||
in ~5 minutes.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 pt-2">
|
||||
{isLoadingOptions ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-3 text-muted-foreground">
|
||||
<Loader2 className="size-6 animate-spin" />
|
||||
<p className="text-sm">Loading available plans...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Billing period toggle */}
|
||||
<div className="flex items-center gap-1 rounded-lg border p-1 bg-muted/40 w-fit">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAnnual(false)}
|
||||
className={cn(
|
||||
"px-4 py-1.5 rounded-md text-sm font-medium transition-colors",
|
||||
!isAnnual
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsAnnual(true)}
|
||||
className={cn(
|
||||
"px-4 py-1.5 rounded-md text-sm font-medium transition-colors flex items-center gap-1.5",
|
||||
isAnnual
|
||||
? "bg-background shadow-sm text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
Annual
|
||||
<span className="text-xs bg-green-500/15 text-green-600 dark:text-green-400 px-1.5 py-0.5 rounded font-semibold">
|
||||
Save ~20%
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Plan selector */}
|
||||
<div className="space-y-2">
|
||||
<Label>Plan</Label>
|
||||
<div className="grid gap-2">
|
||||
{plans?.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedPlan(p.id)}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-lg border p-3 text-left transition-colors",
|
||||
selectedPlan === p.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-sm">{p.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatSpecs(p.cpus, p.memoryMb, p.diskMb, p.bandwidthMb)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-semibold text-sm">
|
||||
{displayPrice(p)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{displayPriceSmall(p)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data center selector */}
|
||||
<div className="space-y-2">
|
||||
<Label>Data Center</Label>
|
||||
<Select value={selectedDc} onValueChange={setSelectedDc}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a location..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" side="bottom" sideOffset={4} className="max-h-56 overflow-y-auto">
|
||||
{dataCenters?.map((dc) => (
|
||||
<SelectItem key={dc.id} value={String(dc.id)}>
|
||||
{dc.city} — {dc.continent}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{plan && selectedDc && (
|
||||
<div className="rounded-lg bg-muted p-3 text-sm space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Plan</span>
|
||||
<span className="font-medium">{plan.name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Billing</span>
|
||||
<span className="font-medium">{isAnnual ? "Annual" : "Monthly"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total</span>
|
||||
<span className="font-semibold">{displayPrice(plan)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!selectedPlan || !selectedDc || purchase.isPending}
|
||||
onClick={() => {
|
||||
if (!selectedPlan || !selectedDc) return;
|
||||
purchase.mutate({
|
||||
plan: selectedPlan,
|
||||
dataCenterId: Number(selectedDc),
|
||||
isAnnual,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{purchase.isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 mr-2 animate-spin" />
|
||||
Placing order...
|
||||
</>
|
||||
) : (
|
||||
"Order Server"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export const ShowManagedServers = () => {
|
||||
const router = useRouter();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data: servers, isLoading } = api.managedServer.list.useQuery();
|
||||
|
||||
const syncStatus = api.managedServer.syncStatus.useMutation({
|
||||
onSuccess: () => utils.managedServer.list.invalidate(),
|
||||
});
|
||||
|
||||
const deleteServer = api.managedServer.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Server terminated.");
|
||||
utils.managedServer.list.invalidate();
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<Server className="size-6 text-muted-foreground self-center" />
|
||||
Billing
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your subscription and servers
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 py-4 border-t">
|
||||
<nav className="flex space-x-2 border-b">
|
||||
{navigationItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = router.pathname === item.href;
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
|
||||
isActive
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-base">Managed Servers</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Servers provisioned and managed by Dokploy Cloud
|
||||
</p>
|
||||
</div>
|
||||
<OrderServerDialog
|
||||
onSuccess={() => utils.managedServer.list.invalidate()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : servers?.length === 0 ? (
|
||||
<div className="text-center py-12 border rounded-lg border-dashed">
|
||||
<Server className="size-10 mx-auto text-muted-foreground mb-3" />
|
||||
<p className="text-sm font-medium">No managed servers yet</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Order a server and we'll provision and configure it for you
|
||||
automatically.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{servers?.map((s) => {
|
||||
const status =
|
||||
STATUS_MAP[s.status] ?? STATUS_MAP.error!;
|
||||
const isProvisioning = [
|
||||
"pending",
|
||||
"provisioning",
|
||||
"configuring",
|
||||
].includes(s.status);
|
||||
const planLabel = s.plan
|
||||
.split("-")
|
||||
.slice(-2)
|
||||
.join(" ")
|
||||
.toUpperCase();
|
||||
|
||||
return (
|
||||
<div
|
||||
key={s.managedServerId}
|
||||
className="flex items-center justify-between rounded-lg border p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="size-5 text-muted-foreground shrink-0" />
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">
|
||||
{planLabel}
|
||||
</span>
|
||||
<Badge
|
||||
variant={status?.variant}
|
||||
className="flex items-center gap-1 text-xs h-5"
|
||||
>
|
||||
{status?.icon}
|
||||
{status?.label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{s.hostname ?? ""}
|
||||
{s.ipAddress ? ` · ${s.ipAddress}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isProvisioning && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
syncStatus.mutate({
|
||||
managedServerId: s.managedServerId,
|
||||
})
|
||||
}
|
||||
disabled={syncStatus.isPending}
|
||||
>
|
||||
<Loader2
|
||||
className={cn(
|
||||
"size-4",
|
||||
syncStatus.isPending && "animate-spin",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{s.status === "ready" && s.server && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link
|
||||
href={`/dashboard/settings/server?serverId=${s.serverId}`}
|
||||
>
|
||||
<ExternalLink className="size-3.5 mr-1.5" />
|
||||
Open
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<DialogAction
|
||||
title="Terminate Server"
|
||||
description="This will permanently destroy the server and all data on it. This action cannot be undone."
|
||||
type="destructive"
|
||||
onClick={() =>
|
||||
deleteServer.mutate({
|
||||
managedServerId: s.managedServerId,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,3 @@
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
@@ -9,6 +7,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
|
||||
@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
|
||||
onClick={() => {
|
||||
if (stepper.isLast) {
|
||||
setIsOpen(false);
|
||||
push("/dashboard/home");
|
||||
push("/dashboard/projects");
|
||||
} else {
|
||||
stepper.next();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
|
||||
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
||||
|
||||
@@ -141,14 +141,14 @@ export const WebDomain = () => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4 grid-cols-2"
|
||||
className="grid w-full gap-4 md:grid-cols-2"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="domain"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="col-span-2 md:col-span-1">
|
||||
<FormItem>
|
||||
<FormLabel>Domain</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -168,7 +168,7 @@ export const WebDomain = () => {
|
||||
name="letsEncryptEmail"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="col-span-2 md:col-span-1">
|
||||
<FormItem>
|
||||
<FormLabel>Let's Encrypt Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -209,7 +209,7 @@ export const WebDomain = () => {
|
||||
name="certificateType"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="col-span-2">
|
||||
<FormItem className="md:col-span-2">
|
||||
<FormLabel>Certificate Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CopyIcon, ServerIcon } from "lucide-react";
|
||||
import { ServerIcon } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { toast } from "sonner";
|
||||
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
|
||||
@@ -51,17 +49,8 @@ export const WebServer = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-1.5">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Server IP: {webServerSettings?.serverIp}
|
||||
{webServerSettings?.serverIp && (
|
||||
<CopyIcon
|
||||
className="size-3.5 cursor-pointer hover:text-foreground transition-colors"
|
||||
onClick={() => {
|
||||
copy(webServerSettings.serverIp ?? "");
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Version: {dokployVersion}
|
||||
|
||||
@@ -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",
|
||||
@@ -868,19 +861,6 @@ function SidebarLogo() {
|
||||
);
|
||||
}
|
||||
|
||||
function MobileCloser() {
|
||||
const pathname = usePathname();
|
||||
const { setOpenMobile, isMobile } = useSidebar();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setOpenMobile(false);
|
||||
}
|
||||
}, [pathname, isMobile, setOpenMobile]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Page({ children }: Props) {
|
||||
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
|
||||
undefined,
|
||||
@@ -946,7 +926,6 @@ export default function Page({ children }: Props) {
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<MobileCloser />
|
||||
<Sidebar collapsible="icon" variant="floating">
|
||||
<SidebarHeader>
|
||||
{/* <SidebarMenuButton
|
||||
|
||||
@@ -80,7 +80,7 @@ export const UserNav = () => {
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/dashboard/home");
|
||||
router.push("/dashboard/projects");
|
||||
}}
|
||||
>
|
||||
Projects
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 ---------- */
|
||||
|
||||
@@ -342,7 +342,7 @@ export const AdvanceBreadcrumb = () => {
|
||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||
>
|
||||
<FolderInput className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
|
||||
<span className="font-medium max-w-[150px] truncate">
|
||||
{currentProject?.name || "Select Project"}
|
||||
</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
@@ -478,7 +478,7 @@ export const AdvanceBreadcrumb = () => {
|
||||
aria-expanded={environmentOpen}
|
||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||
>
|
||||
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
|
||||
<span className="font-medium max-w-[150px] truncate">
|
||||
{currentEnvironment?.name || "production"}
|
||||
</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
@@ -533,7 +533,7 @@ export const AdvanceBreadcrumb = () => {
|
||||
)}
|
||||
|
||||
{projectEnvironments && projectEnvironments.length === 1 && (
|
||||
<p className="text-sm font-normal ml-1 max-w-[50px] md:max-w-[150px] truncate">
|
||||
<p className="text-sm font-normal ml-1">
|
||||
{currentEnvironment?.name || "production"}
|
||||
</p>
|
||||
)}
|
||||
@@ -551,7 +551,7 @@ export const AdvanceBreadcrumb = () => {
|
||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||
>
|
||||
{getServiceIcon(currentService.type)}
|
||||
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
|
||||
<span className="font-medium max-w-[150px] truncate">
|
||||
{currentService.name}
|
||||
</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
@@ -617,7 +617,7 @@ export const AdvanceBreadcrumb = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 ml-1 hidden md:flex"
|
||||
className="size-7 ml-1"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -63,7 +63,6 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
className={cn(
|
||||
buttonVariants({ variant, size, className }),
|
||||
"flex gap-2",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "schedule" ADD COLUMN "description" text;
|
||||
@@ -1,22 +0,0 @@
|
||||
CREATE TYPE "public"."managedServerStatus" AS ENUM('pending', 'provisioning', 'configuring', 'ready', 'error', 'terminating', 'terminated');--> statement-breakpoint
|
||||
CREATE TABLE "managed_server" (
|
||||
"managedServerId" text PRIMARY KEY NOT NULL,
|
||||
"organizationId" text NOT NULL,
|
||||
"serverId" text,
|
||||
"plan" text NOT NULL,
|
||||
"status" "managedServerStatus" DEFAULT 'pending' NOT NULL,
|
||||
"hostingerVmId" integer,
|
||||
"hostingerSubscriptionId" text,
|
||||
"dataCenterId" integer NOT NULL,
|
||||
"ipAddress" text,
|
||||
"hostname" text,
|
||||
"stripeSubscriptionId" text,
|
||||
"stripePriceId" text,
|
||||
"rootPassword" text,
|
||||
"errorMessage" text,
|
||||
"createdAt" text NOT NULL,
|
||||
"updatedAt" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "managed_server" ADD CONSTRAINT "managed_server_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "managed_server" ADD CONSTRAINT "managed_server_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE set null ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1163,20 +1163,6 @@
|
||||
"when": 1775845419261,
|
||||
"tag": "0165_abnormal_greymalkin",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 166,
|
||||
"version": "7",
|
||||
"when": 1778303519111,
|
||||
"tag": "0166_nosy_slapstick",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 167,
|
||||
"version": "7",
|
||||
"when": 1778657133470,
|
||||
"tag": "0167_dizzy_solo",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -28,7 +28,6 @@ try {
|
||||
"wait-for-postgres": "wait-for-postgres.ts",
|
||||
"reset-password": "reset-password.ts",
|
||||
"reset-2fa": "reset-2fa.ts",
|
||||
"migrate-auth-secret": "scripts/migrate-auth-secret.ts",
|
||||
},
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.29.4",
|
||||
"version": "v0.29.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -14,7 +14,6 @@
|
||||
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
|
||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
|
||||
"migrate-auth-secret": "node -r dotenv/config dist/migrate-auth-secret.mjs",
|
||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||
@@ -127,7 +126,7 @@
|
||||
"next-themes": "^0.2.1",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-os-utils": "2.0.1",
|
||||
"node-pty": "1.1.0",
|
||||
"node-pty": "1.0.0",
|
||||
"node-schedule": "2.1.1",
|
||||
"nodemailer": "6.9.14",
|
||||
"octokit": "3.1.2",
|
||||
@@ -148,7 +147,7 @@
|
||||
"shell-quote": "^1.8.1",
|
||||
"slugify": "^1.6.6",
|
||||
"sonner": "^1.7.4",
|
||||
"ssh2": "~1.16.0",
|
||||
"ssh2": "1.15.0",
|
||||
"stripe": "17.2.0",
|
||||
"superjson": "^2.2.2",
|
||||
"swagger-ui-react": "^5.31.2",
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -12,15 +12,6 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
|
||||
/**
|
||||
* Log a webhook handler error server-side without leaking its shape to the HTTP
|
||||
* response. Drizzle errors carry the raw SQL query, column list and parameters,
|
||||
* so we never forward the error object to the client.
|
||||
*/
|
||||
export const logWebhookError = (context: string, error: unknown) => {
|
||||
console.error(context, error);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get package_version from registry_package events
|
||||
*/
|
||||
@@ -271,15 +262,14 @@ export default async function handler(
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Application:", error);
|
||||
res.status(400).json({ message: "Error deploying Application" });
|
||||
res.status(400).json({ message: "Error deploying Application", error });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ message: "Application deployed successfully" });
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Application:", error);
|
||||
res.status(400).json({ message: "Error deploying Application" });
|
||||
console.log(error);
|
||||
res.status(400).json({ message: "Error deploying Application", error });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
extractCommittedPaths,
|
||||
extractHash,
|
||||
getProviderByHeader,
|
||||
logWebhookError,
|
||||
} from "../[refreshToken]";
|
||||
|
||||
export default async function handler(
|
||||
@@ -196,14 +195,13 @@ export default async function handler(
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Compose:", error);
|
||||
res.status(400).json({ message: "Error deploying Compose" });
|
||||
res.status(400).json({ message: "Error deploying Compose", error });
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({ message: "Compose deployed successfully" });
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Compose:", error);
|
||||
res.status(400).json({ message: "Error deploying Compose" });
|
||||
console.log(error);
|
||||
res.status(400).json({ message: "Error deploying Compose", error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,13 @@ import { applications, compose, github } from "@/server/db/schema";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
import { myQueue } from "@/server/queues/queueSetup";
|
||||
import { deploy } from "@/server/utils/deploy";
|
||||
import {
|
||||
extractCommitMessage,
|
||||
extractHash,
|
||||
logWebhookError,
|
||||
} from "./[refreshToken]";
|
||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const signature = req.headers["x-hub-signature-256"];
|
||||
if (!signature) {
|
||||
res.status(401).json({ message: "Missing signature header" });
|
||||
return;
|
||||
}
|
||||
|
||||
const githubBody = req.body;
|
||||
|
||||
if (!githubBody?.installation?.id) {
|
||||
@@ -206,8 +197,10 @@ export default async function handler(
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying applications on tag:", error);
|
||||
res.status(400).json({ message: "Error deploying applications on tag" });
|
||||
console.error("Error deploying applications on tag:", error);
|
||||
res
|
||||
.status(400)
|
||||
.json({ message: "Error deploying applications on tag", error });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -329,8 +322,7 @@ export default async function handler(
|
||||
}
|
||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||
} catch (error) {
|
||||
logWebhookError("Error deploying Application:", error);
|
||||
res.status(400).json({ message: "Error deploying Application" });
|
||||
res.status(400).json({ message: "Error deploying Application", error });
|
||||
}
|
||||
} else if (req.headers["x-github-event"] === "pull_request") {
|
||||
const prId = githubBody?.pull_request?.id;
|
||||
|
||||
@@ -84,7 +84,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
@@ -102,7 +102,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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: false,
|
||||
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(),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -95,8 +95,8 @@ export async function getServerSideProps(
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
permanent: true,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export async function getServerSideProps(
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
@@ -122,7 +122,7 @@ export async function getServerSideProps(
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/home",
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user