mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-13 18:05:31 +02:00
Compare commits
2 Commits
fix/env-ed
...
feat/scim-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dbd1039e8 | ||
|
|
f06c9deddf |
79
.github/workflows/dokploy.yml
vendored
79
.github/workflows/dokploy.yml
vendored
@@ -138,8 +138,6 @@ jobs:
|
|||||||
needs: [combine-manifests]
|
needs: [combine-manifests]
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
version: ${{ steps.get_version.outputs.version }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -162,80 +160,3 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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 }}"
|
|
||||||
|
|||||||
21
.github/workflows/sync-openapi-docs.yml
vendored
21
.github/workflows/sync-openapi-docs.yml
vendored
@@ -110,24 +110,3 @@ jobs:
|
|||||||
|
|
||||||
echo "✅ OpenAPI synced to CLI repository successfully"
|
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"
|
|
||||||
|
|
||||||
|
|||||||
80
.github/workflows/sync-version.yml
vendored
Normal file
80
.github/workflows/sync-version.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: Sync version to MCP and CLI repos
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync-version:
|
||||||
|
name: Sync version to external repos
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Dokploy repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
VERSION=$(jq -r .version apps/dokploy/package.json | sed 's/^v//')
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
|
- name: Sync version to MCP repository
|
||||||
|
run: |
|
||||||
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo
|
||||||
|
cd /tmp/mcp-repo
|
||||||
|
|
||||||
|
# Regenerate tools from latest OpenAPI spec
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm run fetch-openapi
|
||||||
|
pnpm run generate
|
||||||
|
|
||||||
|
# Bump version after install so pnpm install doesn't overwrite it
|
||||||
|
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
|
||||||
|
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||||
|
-m "Release: ${{ github.event.release.html_url }}" \
|
||||||
|
--allow-empty
|
||||||
|
|
||||||
|
git push
|
||||||
|
|
||||||
|
|
||||||
|
- name: Sync version to CLI repository
|
||||||
|
run: |
|
||||||
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo
|
||||||
|
|
||||||
|
cd /tmp/cli-repo
|
||||||
|
|
||||||
|
# Copy latest openapi spec and regenerate commands
|
||||||
|
cp ${{ github.workspace }}/openapi.json ./openapi.json
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm run generate
|
||||||
|
|
||||||
|
# Bump version after install so pnpm install doesn't overwrite it
|
||||||
|
if [ -f package.json ]; then
|
||||||
|
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.name "Dokploy Bot"
|
||||||
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: bump version to ${{ steps.get_version.outputs.version }}" \
|
||||||
|
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
||||||
|
-m "Release: ${{ github.event.release.html_url }}" \
|
||||||
|
--allow-empty
|
||||||
|
|
||||||
|
git push
|
||||||
|
|
||||||
|
echo "CLI repo synced to version ${{ steps.get_version.outputs.version }}"
|
||||||
|
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -4,8 +4,5 @@
|
|||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.biome": "explicit",
|
"source.fixAll.biome": "explicit",
|
||||||
"source.organizeImports.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
|
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 curl -fs http://localhost:3000/api/trpc/settings.health || exit 1
|
||||||
|
|
||||||
CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]
|
CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { shouldDeploy } from "@dokploy/server";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
describe("shouldDeploy", () => {
|
|
||||||
it("should deploy when no watch paths are configured", () => {
|
|
||||||
expect(shouldDeploy(null, ["src/index.ts"])).toBe(true);
|
|
||||||
expect(shouldDeploy([], ["src/index.ts"])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should deploy when watch paths match modified files", () => {
|
|
||||||
expect(shouldDeploy(["src/**"], ["src/index.ts"])).toBe(true);
|
|
||||||
expect(shouldDeploy(["apps/web/**"], ["apps/web/page.tsx"])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not deploy when watch paths do not match", () => {
|
|
||||||
expect(shouldDeploy(["src/**"], ["docs/readme.md"])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not throw when modified files contain non-string values", () => {
|
|
||||||
expect(() =>
|
|
||||||
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
|
|
||||||
).not.toThrow();
|
|
||||||
expect(
|
|
||||||
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not throw when modified files are undefined or null", () => {
|
|
||||||
expect(() => shouldDeploy(["src/**"], undefined)).not.toThrow();
|
|
||||||
expect(() => shouldDeploy(["src/**"], null)).not.toThrow();
|
|
||||||
expect(shouldDeploy(["src/**"], undefined)).toBe(false);
|
|
||||||
expect(shouldDeploy(["src/**"], null)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not throw when every modified file is non-string", () => {
|
|
||||||
expect(() =>
|
|
||||||
shouldDeploy(["src/**"], [undefined, undefined] as any),
|
|
||||||
).not.toThrow();
|
|
||||||
expect(shouldDeploy(["src/**"], [undefined, undefined] as any)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -494,49 +494,4 @@ describe("processTemplate", () => {
|
|||||||
expect(result.mounts).toHaveLength(1);
|
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);
|
const domain = processValue("${domain}", {}, mockSchema);
|
||||||
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
|
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
domain.endsWith(`${mockSchema.serverIp.replaceAll(".", "-")}.sslip.io`),
|
domain.endsWith(
|
||||||
|
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
|
||||||
|
),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -78,20 +78,4 @@ describe("readValidDirectory (path traversal)", () => {
|
|||||||
it("returns false for empty string (resolves to cwd)", () => {
|
it("returns false for empty string (resolves to cwd)", () => {
|
||||||
expect(readValidDirectory("")).toBe(false);
|
expect(readValidDirectory("")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns true for Next.js dynamic route paths with square brackets", () => {
|
|
||||||
expect(
|
|
||||||
readValidDirectory(
|
|
||||||
`${BASE}/applications/myapp/code/app/api/[id]/route.ts`,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
readValidDirectory(`${BASE}/applications/myapp/code/pages/[slug].tsx`),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
|
||||||
readValidDirectory(
|
|
||||||
`${BASE}/applications/myapp/code/app/[...catch]/page.tsx`,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import type { RouterOutputs } from "@/utils/api";
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
import { DnsHelperModal } from "./dns-helper-modal";
|
|
||||||
import { AddDomain } from "./handle-domain";
|
|
||||||
import type { ValidationStates } from "./show-domains";
|
import type { ValidationStates } from "./show-domains";
|
||||||
|
import { AddDomain } from "./handle-domain";
|
||||||
|
import { DnsHelperModal } from "./dns-helper-modal";
|
||||||
|
|
||||||
export type Domain =
|
export type Domain =
|
||||||
| RouterOutputs["domain"]["byApplicationId"][0]
|
| RouterOutputs["domain"]["byApplicationId"][0]
|
||||||
@@ -168,7 +168,7 @@ export const createColumns = ({
|
|||||||
{domain.certificateType}
|
{domain.certificateType}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!domain.host.includes("sslip.io") && (
|
{!domain.host.includes("traefik.me") && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -256,7 +256,7 @@ export const createColumns = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{!domain.host.includes("sslip.io") && (
|
{!domain.host.includes("traefik.me") && (
|
||||||
<DnsHelperModal
|
<DnsHelperModal
|
||||||
domain={{
|
domain={{
|
||||||
host: domain.host,
|
host: domain.host,
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
const https = form.watch("https");
|
const https = form.watch("https");
|
||||||
const domainType = form.watch("domainType");
|
const domainType = form.watch("domainType");
|
||||||
const host = form.watch("host");
|
const host = form.watch("host");
|
||||||
const isTraefikMeDomain = host?.includes("sslip.io") || false;
|
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -513,7 +513,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
{!canGenerateTraefikMeDomains &&
|
{!canGenerateTraefikMeDomains &&
|
||||||
field.value.includes("sslip.io") && (
|
field.value.includes("traefik.me") && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You need to set an IP address in your{" "}
|
You need to set an IP address in your{" "}
|
||||||
<Link
|
<Link
|
||||||
@@ -524,12 +524,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
to make your sslip.io domain work.
|
to make your traefik.me domain work.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
{isTraefikMeDomain && (
|
{isTraefikMeDomain && (
|
||||||
<AlertBlock type="info">
|
<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
|
service and does not support SSL/HTTPS. HTTPS and
|
||||||
certificate options will not have any effect.
|
certificate options will not have any effect.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
@@ -567,7 +567,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
className="max-w-[10rem]"
|
className="max-w-[10rem]"
|
||||||
>
|
>
|
||||||
<p>Generate sslip.io domain</p>
|
<p>Generate traefik.me domain</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{!item.host.includes("sslip.io") && (
|
{!item.host.includes("traefik.me") && (
|
||||||
<DnsHelperModal
|
<DnsHelperModal
|
||||||
domain={{
|
domain={{
|
||||||
host: item.host,
|
host: item.host,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
|
|||||||
slug: z.string().optional(),
|
slug: z.string().optional(),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
enableSubmodules: z.boolean().optional(),
|
enableSubmodules: z.boolean().optional(),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -42,10 +41,7 @@ const GitProviderSchema = z.object({
|
|||||||
repositoryURL: z.string().min(1, {
|
repositoryURL: z.string().min(1, {
|
||||||
message: "Repository URL is required",
|
message: "Repository URL is required",
|
||||||
}),
|
}),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
sshKey: z.string().optional(),
|
sshKey: z.string().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
enableSubmodules: z.boolean().default(false),
|
enableSubmodules: z.boolean().default(false),
|
||||||
@@ -111,103 +107,110 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 items-start">
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
<FormField
|
className="flex flex-col gap-4"
|
||||||
control={form.control}
|
>
|
||||||
name="repositoryURL"
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
render={({ field }) => (
|
<div className="flex items-end col-span-2 gap-4">
|
||||||
<FormItem className="col-span-2 lg:col-span-3">
|
<div className="grow">
|
||||||
<div className="flex items-center justify-between h-5">
|
<FormField
|
||||||
<FormLabel>Repository URL</FormLabel>
|
control={form.control}
|
||||||
{field.value?.startsWith("https://") && (
|
name="repositoryURL"
|
||||||
<Link
|
render={({ field }) => (
|
||||||
href={field.value}
|
<FormItem>
|
||||||
target="_blank"
|
<div className="flex items-center justify-between">
|
||||||
rel="noopener noreferrer"
|
<FormLabel>Repository URL</FormLabel>
|
||||||
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
{field.value?.startsWith("https://") && (
|
||||||
>
|
<Link
|
||||||
<GitIcon className="h-4 w-4" />
|
href={field.value}
|
||||||
<span>View Repository</span>
|
target="_blank"
|
||||||
</Link>
|
rel="noopener noreferrer"
|
||||||
)}
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
|
||||||
</div>
|
>
|
||||||
<FormControl>
|
<GitIcon className="h-4 w-4" />
|
||||||
<Input placeholder="Repository URL" {...field} />
|
<span>View Repository</span>
|
||||||
</FormControl>
|
</Link>
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
</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>
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
{sshKeys && sshKeys.length > 0 ? (
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="sshKey"
|
name="branch"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2 lg:col-span-1">
|
<FormItem>
|
||||||
<FormLabel className="w-full inline-flex justify-between">
|
<FormLabel>Branch</FormLabel>
|
||||||
SSH Key
|
|
||||||
<LockIcon className="size-4 text-muted-foreground" />
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Input placeholder="Branch" {...field} />
|
||||||
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>
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="buildPath"
|
name="buildPath"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem>
|
||||||
<FormLabel>Build Path</FormLabel>
|
<FormLabel>Build Path</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="/" {...field} />
|
<Input placeholder="/" {...field} />
|
||||||
@@ -220,7 +223,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="watchPaths"
|
name="watchPaths"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2 lg:col-span-4">
|
<FormItem className="md:col-span-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -73,10 +72,7 @@ const GiteaProviderSchema = z.object({
|
|||||||
owner: z.string().min(1, "Owner is required"),
|
owner: z.string().min(1, "Owner is required"),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||||
watchPaths: z.array(z.string()).default([]),
|
watchPaths: z.array(z.string()).default([]),
|
||||||
enableSubmodules: z.boolean().optional(),
|
enableSubmodules: z.boolean().optional(),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
|
|||||||
owner: z.string().min(1, "Owner is required"),
|
owner: z.string().min(1, "Owner is required"),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
githubId: z.string().min(1, "Github Provider is required"),
|
githubId: z.string().min(1, "Github Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
|
|||||||
id: z.number().nullable(),
|
id: z.number().nullable(),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
enableSubmodules: z.boolean().default(false),
|
enableSubmodules: z.boolean().default(false),
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</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}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
{canDeploy && (
|
{canDeploy && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
@@ -274,14 +274,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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" />
|
<Terminal className="size-4 mr-1" />
|
||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
{canUpdateService && (
|
{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>
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle autodeploy"
|
aria-label="Toggle autodeploy"
|
||||||
@@ -305,7 +305,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{canUpdateService && (
|
{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>
|
<span className="text-sm font-medium">Clean Cache</span>
|
||||||
<Switch
|
<Switch
|
||||||
aria-label="Toggle clean cache"
|
aria-label="Toggle clean cache"
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const AddPreviewDomain = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const host = form.watch("host");
|
const host = form.watch("host");
|
||||||
const isTraefikMeDomain = host?.includes("sslip.io") || false;
|
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -162,7 +162,7 @@ export const AddPreviewDomain = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
{isTraefikMeDomain && (
|
{isTraefikMeDomain && (
|
||||||
<AlertBlock type="info">
|
<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
|
service and does not support SSL/HTTPS. HTTPS and
|
||||||
certificate options will not have any effect.
|
certificate options will not have any effect.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
@@ -202,7 +202,7 @@ export const AddPreviewDomain = ({
|
|||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
className="max-w-[10rem]"
|
className="max-w-[10rem]"
|
||||||
>
|
>
|
||||||
<p>Generate sslip.io domain</p>
|
<p>Generate traefik.me domain</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
env: "",
|
env: "",
|
||||||
wildcardDomain: "*.sslip.io",
|
wildcardDomain: "*.traefik.me",
|
||||||
port: 3000,
|
port: 3000,
|
||||||
previewLimit: 3,
|
previewLimit: 3,
|
||||||
previewLabels: [],
|
previewLabels: [],
|
||||||
@@ -102,7 +102,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
const previewHttps = form.watch("previewHttps");
|
const previewHttps = form.watch("previewHttps");
|
||||||
const wildcardDomain = form.watch("wildcardDomain");
|
const wildcardDomain = form.watch("wildcardDomain");
|
||||||
const isTraefikMeDomain = wildcardDomain?.includes("sslip.io") || false;
|
const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
||||||
@@ -114,7 +114,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
env: data.previewEnv || "",
|
env: data.previewEnv || "",
|
||||||
buildArgs: data.previewBuildArgs || "",
|
buildArgs: data.previewBuildArgs || "",
|
||||||
buildSecrets: data.previewBuildSecrets || "",
|
buildSecrets: data.previewBuildSecrets || "",
|
||||||
wildcardDomain: data.previewWildcard || "*.sslip.io",
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
port: data.previewPort || 3000,
|
port: data.previewPort || 3000,
|
||||||
previewLabels: data.previewLabels || [],
|
previewLabels: data.previewLabels || [],
|
||||||
previewLimit: data.previewLimit || 3,
|
previewLimit: data.previewLimit || 3,
|
||||||
@@ -173,7 +173,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{isTraefikMeDomain && (
|
{isTraefikMeDomain && (
|
||||||
<AlertBlock type="info">
|
<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
|
does not support SSL/HTTPS. HTTPS and certificate options will
|
||||||
not have any effect.
|
not have any effect.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
@@ -192,7 +192,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Wildcard Domain</FormLabel>
|
<FormLabel>Wildcard Domain</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="*.sslip.io" {...field} />
|
<Input placeholder="*.traefik.me" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ export const commonCronExpressions = [
|
|||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
description: z.string().optional(),
|
|
||||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||||
shellType: z.enum(["bash", "sh"]).default("bash"),
|
shellType: z.enum(["bash", "sh"]).default("bash"),
|
||||||
command: z.string(),
|
command: z.string(),
|
||||||
@@ -225,7 +224,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
resolver: standardSchemaResolver(formSchema),
|
resolver: standardSchemaResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
|
||||||
cronExpression: "",
|
cronExpression: "",
|
||||||
shellType: "bash",
|
shellType: "bash",
|
||||||
command: "",
|
command: "",
|
||||||
@@ -265,7 +263,6 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
if (scheduleId && schedule) {
|
if (scheduleId && schedule) {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: schedule.name,
|
name: schedule.name,
|
||||||
description: schedule.description || "",
|
|
||||||
cronExpression: schedule.cronExpression,
|
cronExpression: schedule.cronExpression,
|
||||||
shellType: schedule.shellType,
|
shellType: schedule.shellType,
|
||||||
command: schedule.command,
|
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
|
<ScheduleFormField
|
||||||
name="cronExpression"
|
name="cronExpression"
|
||||||
formControl={form.control}
|
formControl={form.control}
|
||||||
|
|||||||
@@ -125,11 +125,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
{schedule.enabled ? "Enabled" : "Disabled"}
|
{schedule.enabled ? "Enabled" : "Disabled"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +36,10 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
|
||||||
|
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
|
||||||
|
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
|
||||||
|
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
|
||||||
|
|
||||||
const DockerLogsId = dynamic(
|
const DockerLogsId = dynamic(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -49,12 +49,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
const composeFile = form.watch("composeFile");
|
const composeFile = form.watch("composeFile");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && !composeFile) {
|
||||||
form.reset({
|
form.reset({
|
||||||
composeFile: data.composeFile || "",
|
composeFile: data.composeFile || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.composeFile !== undefined) {
|
if (data?.composeFile !== undefined) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -58,10 +57,7 @@ const BitbucketProviderSchema = z.object({
|
|||||||
slug: z.string().optional(),
|
slug: z.string().optional(),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
enableSubmodules: z.boolean().default(false),
|
enableSubmodules: z.boolean().default(false),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -42,10 +41,7 @@ const GitProviderSchema = z.object({
|
|||||||
repositoryURL: z.string().min(1, {
|
repositoryURL: z.string().min(1, {
|
||||||
message: "Repository URL is required",
|
message: "Repository URL is required",
|
||||||
}),
|
}),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
sshKey: z.string().optional(),
|
sshKey: z.string().optional(),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
enableSubmodules: z.boolean().default(false),
|
enableSubmodules: z.boolean().default(false),
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
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 Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -58,10 +57,7 @@ const GiteaProviderSchema = z.object({
|
|||||||
owner: z.string().min(1, "Owner is required"),
|
owner: z.string().min(1, "Owner is required"),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
giteaId: z.string().min(1, "Gitea Provider is required"),
|
giteaId: z.string().min(1, "Gitea Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
enableSubmodules: z.boolean().default(false),
|
enableSubmodules: z.boolean().default(false),
|
||||||
|
|||||||
@@ -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 { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -56,10 +55,7 @@ const GithubProviderSchema = z.object({
|
|||||||
owner: z.string().min(1, "Owner is required"),
|
owner: z.string().min(1, "Owner is required"),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
githubId: z.string().min(1, "Github Provider is required"),
|
githubId: z.string().min(1, "Github Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
triggerType: z.enum(["push", "tag"]).default("push"),
|
triggerType: z.enum(["push", "tag"]).default("push"),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useEffect, useMemo } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
|
||||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -59,10 +58,7 @@ const GitlabProviderSchema = z.object({
|
|||||||
gitlabPathNamespace: z.string().min(1),
|
gitlabPathNamespace: z.string().min(1),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
branch: z
|
branch: z.string().min(1, "Branch is required"),
|
||||||
.string()
|
|
||||||
.min(1, "Branch is required")
|
|
||||||
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
|
|
||||||
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
gitlabId: z.string().min(1, "Gitlab Provider is required"),
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
enableSubmodules: z.boolean().default(false),
|
enableSubmodules: z.boolean().default(false),
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ export const RestoreBackup = ({
|
|||||||
toast.error("Please select a database type");
|
toast.error("Please select a database type");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log({ data });
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import copy from "copy-to-clipboard";
|
import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react";
|
||||||
import {
|
|
||||||
Bot,
|
|
||||||
Check,
|
|
||||||
Copy,
|
|
||||||
Loader2,
|
|
||||||
RotateCcw,
|
|
||||||
Settings,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
@@ -39,7 +30,6 @@ const MAX_LOG_LINES = 200;
|
|||||||
export function AnalyzeLogs({ logs, context }: Props) {
|
export function AnalyzeLogs({ logs, context }: Props) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [aiId, setAiId] = useState<string>("");
|
const [aiId, setAiId] = useState<string>("");
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
|
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
@@ -62,15 +52,6 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
|||||||
mutate({ aiId, logs: logsText, context });
|
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 (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
open={open}
|
open={open}
|
||||||
@@ -90,7 +71,7 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
|||||||
disabled={logs.length === 0}
|
disabled={logs.length === 0}
|
||||||
title="Analyze logs with AI"
|
title="Analyze logs with AI"
|
||||||
>
|
>
|
||||||
<Bot className="mr-2 size-4" />
|
<Bot className="mr-2 h-4 w-4" />
|
||||||
AI
|
AI
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -187,18 +168,6 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
|||||||
)}
|
)}
|
||||||
Re-analyze
|
Re-analyze
|
||||||
</Button>
|
</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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -347,13 +347,11 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
title={isPaused ? "Resume logs" : "Pause logs"}
|
title={isPaused ? "Resume logs" : "Pause logs"}
|
||||||
>
|
>
|
||||||
{isPaused ? (
|
{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"}
|
||||||
{isPaused ? "Resume" : "Pause"}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -364,13 +362,11 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
title="Copy logs to clipboard"
|
title="Copy logs to clipboard"
|
||||||
>
|
>
|
||||||
{copied ? (
|
{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">
|
Copy
|
||||||
{copied ? "Copied" : "Copy"}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -378,18 +374,17 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
className="h-9 sm:w-auto w-full"
|
className="h-9 sm:w-auto w-full"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={filteredLogs.length === 0 || !data?.Name}
|
disabled={filteredLogs.length === 0 || !data?.Name}
|
||||||
title="Download logs as text file"
|
|
||||||
>
|
>
|
||||||
<DownloadIcon className="size-4" />
|
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||||
<span className="hidden lg:ml-2 lg:inline">Download logs</span>
|
Download logs
|
||||||
</Button>
|
</Button>
|
||||||
<AnalyzeLogs logs={filteredLogs} context="runtime" />
|
<AnalyzeLogs logs={filteredLogs} context="runtime" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isPaused && (
|
{isPaused && (
|
||||||
<AlertBlock type="warning" className="items-center">
|
<AlertBlock type="warning">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Pause className="size-4" />
|
<Pause className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
Logs paused
|
Logs paused
|
||||||
{messageBuffer.length > 0 && (
|
{messageBuffer.length > 0 && (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
type UploadFileToContainer,
|
|
||||||
uploadFileToContainerSchema,
|
uploadFileToContainerSchema,
|
||||||
|
type UploadFileToContainer,
|
||||||
} from "@/utils/schema";
|
} from "@/utils/schema";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
|
|||||||
@@ -632,6 +632,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableNamespaces"
|
name="enableNamespaces"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
|
console.log(field.value);
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Enable Namespaces</FormLabel>
|
<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 {
|
import {
|
||||||
Bookmark,
|
|
||||||
BookText,
|
BookText,
|
||||||
|
Bookmark,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
Globe,
|
Globe,
|
||||||
|
|||||||
@@ -344,7 +344,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>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
|
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
|
||||||
<span className="flex flex-col gap-1.5 ">
|
<span className="flex flex-col gap-1.5 ">
|
||||||
@@ -491,7 +491,7 @@ export const ShowProjects = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</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">
|
<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}>
|
<DateTooltip date={project.createdAt}>
|
||||||
Created
|
Created
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||||
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
|
|||||||
@@ -79,11 +79,8 @@ export const columns: ColumnDef<LogEntry>[] = [
|
|||||||
: log.RequestPath}
|
: log.RequestPath}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-3 w-full">
|
<div className="flex flex-row gap-3 w-full">
|
||||||
<Badge
|
<Badge variant={getStatusColor(log.OriginStatus)}>
|
||||||
variant={getStatusColor(log.OriginStatus || log.DownstreamStatus)}
|
Status: {formatStatusLabel(log.OriginStatus)}
|
||||||
>
|
|
||||||
Status:{" "}
|
|
||||||
{formatStatusLabel(log.OriginStatus || log.DownstreamStatus)}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant={"secondary"}>
|
<Badge variant={"secondary"}>
|
||||||
Exec Time: {formatDuration(log.Duration)}
|
Exec Time: {formatDuration(log.Duration)}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
|||||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||||
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter by hostname..."
|
placeholder="Filter by name..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
className="md:max-w-sm"
|
className="md:max-w-sm"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { NumberInput } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -33,7 +34,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { NumberInput } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import {
|
import {
|
||||||
@@ -9,6 +7,8 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
|
||||||
import { api, type RouterOutputs } from "@/utils/api";
|
import { api, type RouterOutputs } from "@/utils/api";
|
||||||
|
|
||||||
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
||||||
|
|||||||
@@ -141,14 +141,14 @@ export const WebDomain = () => {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 grid-cols-2"
|
className="grid w-full gap-4 md:grid-cols-2"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="domain"
|
name="domain"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className="col-span-2 md:col-span-1">
|
<FormItem>
|
||||||
<FormLabel>Domain</FormLabel>
|
<FormLabel>Domain</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -168,7 +168,7 @@ export const WebDomain = () => {
|
|||||||
name="letsEncryptEmail"
|
name="letsEncryptEmail"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className="col-span-2 md:col-span-1">
|
<FormItem>
|
||||||
<FormLabel>Let's Encrypt Email</FormLabel>
|
<FormLabel>Let's Encrypt Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -209,7 +209,7 @@ export const WebDomain = () => {
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="md:col-span-2">
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CopyIcon, ServerIcon } from "lucide-react";
|
import { ServerIcon } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
|
||||||
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
|
||||||
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
|
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
|
||||||
@@ -51,17 +49,8 @@ export const WebServer = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
<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}
|
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>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version: {dokployVersion}
|
Version: {dokployVersion}
|
||||||
|
|||||||
@@ -868,19 +868,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) {
|
export default function Page({ children }: Props) {
|
||||||
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
|
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
@@ -946,7 +933,6 @@ export default function Page({ children }: Props) {
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MobileCloser />
|
|
||||||
<Sidebar collapsible="icon" variant="floating">
|
<Sidebar collapsible="icon" variant="floating">
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
{/* <SidebarMenuButton
|
{/* <SidebarMenuButton
|
||||||
|
|||||||
236
apps/dokploy/components/proprietary/sso/scim-dialog.tsx
Normal file
236
apps/dokploy/components/proprietary/sso/scim-dialog.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Copy, KeyRound, Loader2, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { type ReactNode, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScimDialog = ({ children }: Props) => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const baseURL = useUrl();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [newProviderId, setNewProviderId] = useState("");
|
||||||
|
const [justCreatedToken, setJustCreatedToken] = useState<{
|
||||||
|
providerId: string;
|
||||||
|
token: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const { data: providers = [], isPending } = api.scim.listProviders.useQuery(
|
||||||
|
undefined,
|
||||||
|
{ enabled: open },
|
||||||
|
);
|
||||||
|
const { mutateAsync: generateToken, isPending: isGenerating } =
|
||||||
|
api.scim.generateToken.useMutation();
|
||||||
|
const { mutateAsync: deleteProvider, isPending: isDeleting } =
|
||||||
|
api.scim.deleteProvider.useMutation();
|
||||||
|
|
||||||
|
const scimUrl = `${baseURL || "{baseURL}"}/api/auth/scim/v2`;
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
const providerId = newProviderId.trim().toLowerCase();
|
||||||
|
if (!providerId) return;
|
||||||
|
try {
|
||||||
|
const result = await generateToken({ providerId });
|
||||||
|
setJustCreatedToken({
|
||||||
|
providerId: result.providerId,
|
||||||
|
token: result.scimToken,
|
||||||
|
});
|
||||||
|
setNewProviderId("");
|
||||||
|
await utils.scim.listProviders.invalidate();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : "Failed to generate SCIM token",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (providerId: string) => {
|
||||||
|
try {
|
||||||
|
await deleteProvider({ providerId });
|
||||||
|
toast.success("SCIM provider removed");
|
||||||
|
await utils.scim.listProviders.invalidate();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(
|
||||||
|
err instanceof Error ? err.message : "Failed to delete SCIM provider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async (value: string, label: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
toast.success(`${label} copied`);
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to copy");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (next: boolean) => {
|
||||||
|
setOpen(next);
|
||||||
|
if (!next) setJustCreatedToken(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[560px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<KeyRound className="size-5" />
|
||||||
|
SCIM provisioning
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Automatically provision, update, and deactivate users from your
|
||||||
|
identity provider (Okta, Entra ID, etc.). Configure the SCIM endpoint
|
||||||
|
below in your IdP.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label className="text-xs font-medium text-muted-foreground">
|
||||||
|
SCIM 2.0 endpoint URL
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="flex-1 break-all rounded-md bg-muted px-2 py-1.5 font-mono text-xs">
|
||||||
|
{scimUrl}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 shrink-0"
|
||||||
|
onClick={() => handleCopy(scimUrl, "Endpoint URL")}
|
||||||
|
disabled={!baseURL}
|
||||||
|
>
|
||||||
|
<Copy className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{justCreatedToken && (
|
||||||
|
<div className="rounded-md border border-amber-500/40 bg-amber-500/10 p-3">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Bearer token for {justCreatedToken.providerId}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">
|
||||||
|
Copy this token now — it will not be shown again. Paste it into
|
||||||
|
your IdP's SCIM configuration.
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<p className="flex-1 break-all rounded-md bg-background px-2 py-1.5 font-mono text-xs">
|
||||||
|
{justCreatedToken.token}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 shrink-0"
|
||||||
|
onClick={() =>
|
||||||
|
handleCopy(justCreatedToken.token, "Bearer token")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Copy className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
Generate token for a new provider
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={newProviderId}
|
||||||
|
onChange={(e) => setNewProviderId(e.target.value)}
|
||||||
|
placeholder="okta, entra, jumpcloud..."
|
||||||
|
className="font-mono text-sm"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleGenerate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={!newProviderId.trim() || isGenerating}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Choose a unique identifier for this IdP connection (lowercase,
|
||||||
|
alphanumeric, dashes).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">Existing providers</Label>
|
||||||
|
{isPending ? (
|
||||||
|
<div className="flex items-center gap-2 justify-center py-4">
|
||||||
|
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">Loading...</span>
|
||||||
|
</div>
|
||||||
|
) : providers.length === 0 ? (
|
||||||
|
<p className="rounded-md border border-dashed bg-muted/30 px-3 py-4 text-center text-sm text-muted-foreground">
|
||||||
|
No SCIM providers configured yet.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="flex flex-col gap-2">
|
||||||
|
{providers.map((provider) => (
|
||||||
|
<li
|
||||||
|
key={provider.id}
|
||||||
|
className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2"
|
||||||
|
>
|
||||||
|
<span className="flex-1 font-mono text-sm">
|
||||||
|
{provider.providerId}
|
||||||
|
</span>
|
||||||
|
<DialogAction
|
||||||
|
title="Remove SCIM provider"
|
||||||
|
description={`Remove "${provider.providerId}"? Existing provisioned users will stay but the IdP will no longer be able to sync.`}
|
||||||
|
type="destructive"
|
||||||
|
onClick={() => handleDelete(provider.providerId)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 shrink-0 text-destructive hover:text-destructive"
|
||||||
|
disabled={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => handleOpenChange(false)}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Eye,
|
Eye,
|
||||||
|
KeyRound,
|
||||||
Loader2,
|
Loader2,
|
||||||
LogIn,
|
LogIn,
|
||||||
Pencil,
|
Pencil,
|
||||||
@@ -34,6 +35,7 @@ import { api } from "@/utils/api";
|
|||||||
import { useUrl } from "@/utils/hooks/use-url";
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
import { RegisterOidcDialog } from "./register-oidc-dialog";
|
import { RegisterOidcDialog } from "./register-oidc-dialog";
|
||||||
import { RegisterSamlDialog } from "./register-saml-dialog";
|
import { RegisterSamlDialog } from "./register-saml-dialog";
|
||||||
|
import { ScimDialog } from "./scim-dialog";
|
||||||
|
|
||||||
type ProviderForDetails = {
|
type ProviderForDetails = {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
@@ -169,15 +171,22 @@ export const SSOSettings = () => {
|
|||||||
Users can sign in with their organization's IdP.
|
Users can sign in with their organization's IdP.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="flex flex-wrap gap-2 shrink-0">
|
||||||
variant="outline"
|
<Button
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => setManageOriginsOpen(true)}
|
size="sm"
|
||||||
className="shrink-0"
|
onClick={() => setManageOriginsOpen(true)}
|
||||||
>
|
>
|
||||||
<Shield className="mr-2 size-4" />
|
<Shield className="mr-2 size-4" />
|
||||||
Manage origins
|
Manage origins
|
||||||
</Button>
|
</Button>
|
||||||
|
<ScimDialog>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<KeyRound className="mr-2 size-4" />
|
||||||
|
Manage SCIM
|
||||||
|
</Button>
|
||||||
|
</ScimDialog>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export const AdvanceBreadcrumb = () => {
|
|||||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||||
>
|
>
|
||||||
<FolderInput className="size-4 text-muted-foreground" />
|
<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"}
|
{currentProject?.name || "Select Project"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="size-4 text-muted-foreground" />
|
<ChevronDown className="size-4 text-muted-foreground" />
|
||||||
@@ -478,7 +478,7 @@ export const AdvanceBreadcrumb = () => {
|
|||||||
aria-expanded={environmentOpen}
|
aria-expanded={environmentOpen}
|
||||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
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"}
|
{currentEnvironment?.name || "production"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="size-4 text-muted-foreground" />
|
<ChevronDown className="size-4 text-muted-foreground" />
|
||||||
@@ -533,7 +533,7 @@ export const AdvanceBreadcrumb = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{projectEnvironments && projectEnvironments.length === 1 && (
|
{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"}
|
{currentEnvironment?.name || "production"}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -551,7 +551,7 @@ export const AdvanceBreadcrumb = () => {
|
|||||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||||
>
|
>
|
||||||
{getServiceIcon(currentService.type)}
|
{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}
|
{currentService.name}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="size-4 text-muted-foreground" />
|
<ChevronDown className="size-4 text-muted-foreground" />
|
||||||
@@ -617,7 +617,7 @@ export const AdvanceBreadcrumb = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-7 ml-1 hidden md:flex"
|
className="size-7 ml-1"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(
|
router.push(
|
||||||
`/dashboard/project/${projectId}/environment/${environmentId}`,
|
`/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||||
|
|||||||
@@ -167,13 +167,7 @@ export const CodeEditor = ({
|
|||||||
? css()
|
? css()
|
||||||
: language === "shell"
|
: language === "shell"
|
||||||
? StreamLanguage.define(shell)
|
? StreamLanguage.define(shell)
|
||||||
: StreamLanguage.define({
|
: StreamLanguage.define(properties),
|
||||||
...properties,
|
|
||||||
// The legacy properties mode lacks comment metadata, so
|
|
||||||
// CodeMirror's toggle-comment shortcut (Mod-/) has no comment
|
|
||||||
// token to use. Declare `#` as the line comment for env editors.
|
|
||||||
languageData: { commentTokens: { line: "#" } },
|
|
||||||
}),
|
|
||||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||||
language === "yaml"
|
language === "yaml"
|
||||||
? autocompletion({
|
? autocompletion({
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant, size, className }),
|
buttonVariants({ variant, size, className }),
|
||||||
"flex gap-2",
|
"flex gap-2",
|
||||||
className,
|
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "schedule" ADD COLUMN "description" text;
|
|
||||||
11
apps/dokploy/drizzle/0166_overjoyed_big_bertha.sql
Normal file
11
apps/dokploy/drizzle/0166_overjoyed_big_bertha.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE "scim_provider" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"provider_id" text NOT NULL,
|
||||||
|
"scim_token" text NOT NULL,
|
||||||
|
"organization_id" text,
|
||||||
|
CONSTRAINT "scim_provider_provider_id_unique" UNIQUE("provider_id"),
|
||||||
|
CONSTRAINT "scim_provider_scim_token_unique" UNIQUE("scim_token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "two_factor" ADD COLUMN "verified" boolean DEFAULT true NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "scim_provider" ADD CONSTRAINT "scim_provider_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "887c0c81-4af9-477a-ab29-b3ad16f08451",
|
"id": "0e5a80ea-2d66-4bd2-a41d-b92c419e0116",
|
||||||
"prevId": "ef484e78-f78d-4c3f-ae4b-aa123dc77e61",
|
"prevId": "ef484e78-f78d-4c3f-ae4b-aa123dc77e61",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -789,6 +789,13 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
|
},
|
||||||
|
"verified": {
|
||||||
|
"name": "verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -6714,12 +6721,6 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"cronExpression": {
|
"cronExpression": {
|
||||||
"name": "cronExpression",
|
"name": "cronExpression",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -6871,6 +6872,72 @@
|
|||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
|
"public.scim_provider": {
|
||||||
|
"name": "scim_provider",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"scim_token": {
|
||||||
|
"name": "scim_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"organization_id": {
|
||||||
|
"name": "organization_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"scim_provider_organization_id_organization_id_fk": {
|
||||||
|
"name": "scim_provider_organization_id_organization_id_fk",
|
||||||
|
"tableFrom": "scim_provider",
|
||||||
|
"tableTo": "organization",
|
||||||
|
"columnsFrom": [
|
||||||
|
"organization_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"scim_provider_provider_id_unique": {
|
||||||
|
"name": "scim_provider_provider_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"provider_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scim_provider_scim_token_unique": {
|
||||||
|
"name": "scim_provider_scim_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"scim_token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
"public.security": {
|
"public.security": {
|
||||||
"name": "security",
|
"name": "security",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
|
|||||||
@@ -1167,8 +1167,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 166,
|
"idx": 166,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1778303519111,
|
"when": 1776576422440,
|
||||||
"tag": "0166_nosy_slapstick",
|
"tag": "0166_overjoyed_big_bertha",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ try {
|
|||||||
"wait-for-postgres": "wait-for-postgres.ts",
|
"wait-for-postgres": "wait-for-postgres.ts",
|
||||||
"reset-password": "reset-password.ts",
|
"reset-password": "reset-password.ts",
|
||||||
"reset-2fa": "reset-2fa.ts",
|
"reset-2fa": "reset-2fa.ts",
|
||||||
"migrate-auth-secret": "scripts/migrate-auth-secret.ts",
|
|
||||||
},
|
},
|
||||||
bundle: true,
|
bundle: true,
|
||||||
platform: "node",
|
platform: "node",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.29.4",
|
"version": "v0.29.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -14,7 +14,6 @@
|
|||||||
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
|
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
|
||||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||||
"reset-2fa": "node -r dotenv/config dist/reset-2fa.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 ",
|
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||||
@@ -47,8 +46,8 @@
|
|||||||
"@ai-sdk/mistral": "^3.0.20",
|
"@ai-sdk/mistral": "^3.0.20",
|
||||||
"@ai-sdk/openai": "^3.0.29",
|
"@ai-sdk/openai": "^3.0.29",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||||
"@better-auth/api-key": "1.5.4",
|
"@better-auth/api-key": "1.6.5",
|
||||||
"@better-auth/sso": "1.5.4",
|
"@better-auth/sso": "1.6.5",
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
@@ -102,7 +101,7 @@
|
|||||||
"ai": "^6.0.86",
|
"ai": "^6.0.86",
|
||||||
"ai-sdk-ollama": "^3.7.0",
|
"ai-sdk-ollama": "^3.7.0",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "1.5.4",
|
"better-auth": "1.6.5",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.67.3",
|
"bullmq": "5.67.3",
|
||||||
@@ -114,7 +113,7 @@
|
|||||||
"dockerode": "4.0.2",
|
"dockerode": "4.0.2",
|
||||||
"dompurify": "^3.3.3",
|
"dompurify": "^3.3.3",
|
||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.2",
|
||||||
"drizzle-zod": "0.8.3",
|
"drizzle-zod": "0.8.3",
|
||||||
"fancy-ansi": "^0.1.3",
|
"fancy-ansi": "^0.1.3",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
@@ -127,7 +126,7 @@
|
|||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "^3.9.17",
|
||||||
"node-os-utils": "2.0.1",
|
"node-os-utils": "2.0.1",
|
||||||
"node-pty": "1.1.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
"nodemailer": "6.9.14",
|
"nodemailer": "6.9.14",
|
||||||
"octokit": "3.1.2",
|
"octokit": "3.1.2",
|
||||||
@@ -148,7 +147,7 @@
|
|||||||
"shell-quote": "^1.8.1",
|
"shell-quote": "^1.8.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"ssh2": "~1.16.0",
|
"ssh2": "1.15.0",
|
||||||
"stripe": "17.2.0",
|
"stripe": "17.2.0",
|
||||||
"superjson": "^2.2.2",
|
"superjson": "^2.2.2",
|
||||||
"swagger-ui-react": "^5.31.2",
|
"swagger-ui-react": "^5.31.2",
|
||||||
|
|||||||
@@ -12,15 +12,6 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
|||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
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
|
* Helper function to get package_version from registry_package events
|
||||||
*/
|
*/
|
||||||
@@ -271,15 +262,14 @@ export default async function handler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWebhookError("Error deploying Application:", error);
|
res.status(400).json({ message: "Error deploying Application", error });
|
||||||
res.status(400).json({ message: "Error deploying Application" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ message: "Application deployed successfully" });
|
res.status(200).json({ message: "Application deployed successfully" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWebhookError("Error deploying Application:", error);
|
console.log(error);
|
||||||
res.status(400).json({ message: "Error deploying Application" });
|
res.status(400).json({ message: "Error deploying Application", error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
extractCommittedPaths,
|
extractCommittedPaths,
|
||||||
extractHash,
|
extractHash,
|
||||||
getProviderByHeader,
|
getProviderByHeader,
|
||||||
logWebhookError,
|
|
||||||
} from "../[refreshToken]";
|
} from "../[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
@@ -196,14 +195,13 @@ export default async function handler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWebhookError("Error deploying Compose:", error);
|
res.status(400).json({ message: "Error deploying Compose", error });
|
||||||
res.status(400).json({ message: "Error deploying Compose" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ message: "Compose deployed successfully" });
|
res.status(200).json({ message: "Compose deployed successfully" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWebhookError("Error deploying Compose:", error);
|
console.log(error);
|
||||||
res.status(400).json({ message: "Error deploying Compose" });
|
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 type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import {
|
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||||
extractCommitMessage,
|
|
||||||
extractHash,
|
|
||||||
logWebhookError,
|
|
||||||
} from "./[refreshToken]";
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const signature = req.headers["x-hub-signature-256"];
|
const signature = req.headers["x-hub-signature-256"];
|
||||||
if (!signature) {
|
|
||||||
res.status(401).json({ message: "Missing signature header" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const githubBody = req.body;
|
const githubBody = req.body;
|
||||||
|
|
||||||
if (!githubBody?.installation?.id) {
|
if (!githubBody?.installation?.id) {
|
||||||
@@ -206,8 +197,10 @@ export default async function handler(
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWebhookError("Error deploying applications on tag:", error);
|
console.error("Error deploying applications on tag:", error);
|
||||||
res.status(400).json({ message: "Error deploying applications on tag" });
|
res
|
||||||
|
.status(400)
|
||||||
|
.json({ message: "Error deploying applications on tag", error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,8 +322,7 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logWebhookError("Error deploying Application:", error);
|
res.status(400).json({ message: "Error deploying Application", error });
|
||||||
res.status(400).json({ message: "Error deploying Application" });
|
|
||||||
}
|
}
|
||||||
} else if (req.headers["x-github-event"] === "pull_request") {
|
} else if (req.headers["x-github-event"] === "pull_request") {
|
||||||
const prId = githubBody?.pull_request?.id;
|
const prId = githubBody?.pull_request?.id;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export async function getServerSideProps(
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -104,7 +104,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant"
|
|||||||
import { AddApplication } from "@/components/dashboard/project/add-application";
|
import { AddApplication } from "@/components/dashboard/project/add-application";
|
||||||
import { AddCompose } from "@/components/dashboard/project/add-compose";
|
import { AddCompose } from "@/components/dashboard/project/add-compose";
|
||||||
import { AddDatabase } from "@/components/dashboard/project/add-database";
|
import { AddDatabase } from "@/components/dashboard/project/add-database";
|
||||||
import { AddImport } from "@/components/dashboard/project/add-import";
|
|
||||||
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
||||||
import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector";
|
import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector";
|
||||||
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||||
@@ -1092,10 +1091,6 @@ const EnvironmentPage = (
|
|||||||
projectName={projectData?.name}
|
projectName={projectData?.name}
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
/>
|
/>
|
||||||
<AddImport
|
|
||||||
projectName={projectData?.name}
|
|
||||||
environmentId={environmentId}
|
|
||||||
/>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
@@ -1104,7 +1099,7 @@ const EnvironmentPage = (
|
|||||||
</div>
|
</div>
|
||||||
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-4 2xl:flex-row 2xl:items-center 2xl:justify-between">
|
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -1625,9 +1620,9 @@ const EnvironmentPage = (
|
|||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<Link
|
<Link
|
||||||
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
|
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
|
||||||
className="block h-full"
|
className="block"
|
||||||
>
|
>
|
||||||
<Card className="flex flex-col h-full group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
|
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
|
||||||
{service.serverId && (
|
{service.serverId && (
|
||||||
<div className="absolute -left-1 -top-2">
|
<div className="absolute -left-1 -top-2">
|
||||||
<ServerIcon className="size-4 text-muted-foreground" />
|
<ServerIcon className="size-4 text-muted-foreground" />
|
||||||
@@ -1832,7 +1827,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ const Service = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
@@ -148,9 +147,8 @@ const Service = (
|
|||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
if (data?.server?.ipAddress) {
|
||||||
if (ip) {
|
copy(data.server.ipAddress);
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
toast.success("IP Address Copied!");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -453,7 +451,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ const Service = (
|
|||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
@@ -135,9 +134,8 @@ const Service = (
|
|||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
if (data?.server?.ipAddress) {
|
||||||
if (ip) {
|
copy(data.server.ipAddress);
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
toast.success("IP Address Copied!");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -457,7 +455,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
@@ -11,7 +10,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -63,7 +61,6 @@ const Libsql = (
|
|||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -102,14 +99,6 @@ const Libsql = (
|
|||||||
<div className="flex flex-col h-fit w-fit gap-2">
|
<div className="flex flex-col h-fit w-fit gap-2">
|
||||||
<div className="flex flex-row h-fit w-fit gap-2">
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
|
||||||
if (ip) {
|
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={
|
variant={
|
||||||
!data?.serverId
|
!data?.serverId
|
||||||
? "default"
|
? "default"
|
||||||
@@ -318,7 +307,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
@@ -11,7 +10,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -65,7 +63,6 @@ const Mariadb = (
|
|||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
@@ -114,14 +111,6 @@ const Mariadb = (
|
|||||||
<div className="flex flex-col h-fit w-fit gap-2">
|
<div className="flex flex-col h-fit w-fit gap-2">
|
||||||
<div className="flex flex-row h-fit w-fit gap-2">
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
|
||||||
if (ip) {
|
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={
|
variant={
|
||||||
!data?.serverId
|
!data?.serverId
|
||||||
? "default"
|
? "default"
|
||||||
@@ -347,7 +336,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
@@ -11,7 +10,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -65,7 +63,6 @@ const Mongo = (
|
|||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
@@ -113,14 +110,6 @@ const Mongo = (
|
|||||||
<div className="flex flex-col h-fit w-fit gap-2">
|
<div className="flex flex-col h-fit w-fit gap-2">
|
||||||
<div className="flex flex-row h-fit w-fit gap-2">
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
|
||||||
if (ip) {
|
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={
|
variant={
|
||||||
!data?.serverId
|
!data?.serverId
|
||||||
? "default"
|
? "default"
|
||||||
@@ -351,7 +340,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
@@ -11,7 +10,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -64,7 +62,6 @@ const MySql = (
|
|||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
@@ -113,14 +110,6 @@ const MySql = (
|
|||||||
<div className="flex flex-col h-fit w-fit gap-2">
|
<div className="flex flex-col h-fit w-fit gap-2">
|
||||||
<div className="flex flex-row h-fit w-fit gap-2">
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
|
||||||
if (ip) {
|
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={
|
variant={
|
||||||
!data?.serverId
|
!data?.serverId
|
||||||
? "default"
|
? "default"
|
||||||
@@ -329,7 +318,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
@@ -11,7 +10,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -64,7 +62,6 @@ const Postgresql = (
|
|||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
@@ -112,14 +109,6 @@ const Postgresql = (
|
|||||||
<div className="flex flex-col h-fit w-fit gap-2">
|
<div className="flex flex-col h-fit w-fit gap-2">
|
||||||
<div className="flex flex-row h-fit w-fit gap-2">
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
|
||||||
if (ip) {
|
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={
|
variant={
|
||||||
!data?.serverId
|
!data?.serverId
|
||||||
? "default"
|
? "default"
|
||||||
@@ -335,7 +324,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { HelpCircle, ServerOff } from "lucide-react";
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
@@ -11,7 +10,6 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { type ReactElement, useState } from "react";
|
import { type ReactElement, useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
@@ -64,7 +62,6 @@ const Redis = (
|
|||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: serverIp } = api.settings.getIp.useQuery();
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
@@ -112,14 +109,6 @@ const Redis = (
|
|||||||
<div className="flex flex-col h-fit w-fit gap-2">
|
<div className="flex flex-col h-fit w-fit gap-2">
|
||||||
<div className="flex flex-row h-fit w-fit gap-2">
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge
|
<Badge
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
const ip = data?.server?.ipAddress || serverIp;
|
|
||||||
if (ip) {
|
|
||||||
copy(ip);
|
|
||||||
toast.success("IP Address Copied!");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={
|
variant={
|
||||||
!data?.serverId
|
!data?.serverId
|
||||||
? "default"
|
? "default"
|
||||||
@@ -340,7 +329,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function getServerSideProps(
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -26,7 +26,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function getServerSideProps(
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -43,7 +43,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || (user.role !== "owner" && user.role !== "admin")) {
|
if (!user || (user.role !== "owner" && user.role !== "admin")) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role === "member") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: { destination: "/", permanent: false },
|
redirect: { destination: "/", permanent: true },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function getServerSideProps(
|
|||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -33,7 +33,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role !== "owner") {
|
if (!user || user.role !== "owner") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role === "member") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role === "member") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role === "member") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -53,7 +53,7 @@ export async function getServerSideProps(
|
|||||||
if (!userPermissions?.gitProviders.read) {
|
if (!userPermissions?.gitProviders.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function getServerSideProps(
|
|||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -33,7 +33,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role !== "owner") {
|
if (!user || user.role !== "owner") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ export async function getServerSideProps(
|
|||||||
if (user.role !== "owner") {
|
if (user.role !== "owner") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/settings/profile",
|
destination: "/dashboard/settings/profile",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role === "member") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
if (!user || user.role === "member") {
|
if (!user || user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export async function getServerSideProps(
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -53,7 +53,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -61,7 +61,7 @@ export async function getServerSideProps(
|
|||||||
if (user.role === "member") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/settings/profile",
|
destination: "/dashboard/settings/profile",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -36,7 +36,7 @@ export async function getServerSideProps(
|
|||||||
if (user.role === "member") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/settings/profile",
|
destination: "/dashboard/settings/profile",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export async function getServerSideProps(
|
|||||||
if (!userPermissions?.sshKeys.read) {
|
if (!userPermissions?.sshKeys.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
if (user.role === "member") {
|
if (user.role === "member") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/settings/profile",
|
destination: "/dashboard/settings/profile",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export async function getServerSideProps(
|
|||||||
if (!userPermissions?.tag.read) {
|
if (!userPermissions?.tag.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -66,7 +66,7 @@ export async function getServerSideProps(
|
|||||||
if (!userPermissions?.member.read) {
|
if (!userPermissions?.member.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
if (user.role !== "owner") {
|
if (user.role !== "owner") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/settings/profile",
|
destination: "/dashboard/settings/profile",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user