mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-05 14:05:30 +02:00
Compare commits
4 Commits
v0.29.6
...
fix/webhoo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e2eb7213d | ||
|
|
dcb95374da | ||
|
|
36e131cf12 | ||
|
|
17b4c0fc58 |
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 }}"
|
|
||||||
|
|||||||
63
.github/workflows/sync-openapi-docs.yml
vendored
63
.github/workflows/sync-openapi-docs.yml
vendored
@@ -68,66 +68,3 @@ jobs:
|
|||||||
|
|
||||||
echo "✅ OpenAPI synced to website successfully"
|
echo "✅ OpenAPI synced to website successfully"
|
||||||
|
|
||||||
- name: Sync to MCP repository
|
|
||||||
run: |
|
|
||||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
|
|
||||||
|
|
||||||
cd mcp-repo
|
|
||||||
|
|
||||||
cp -f ../openapi.json openapi.json
|
|
||||||
|
|
||||||
git config user.name "Dokploy Bot"
|
|
||||||
git config user.email "bot@dokploy.com"
|
|
||||||
|
|
||||||
git add openapi.json
|
|
||||||
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
|
||||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
|
||||||
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
|
||||||
--allow-empty
|
|
||||||
|
|
||||||
git push
|
|
||||||
|
|
||||||
echo "✅ OpenAPI synced to MCP repository successfully"
|
|
||||||
|
|
||||||
- name: Sync to CLI repository
|
|
||||||
run: |
|
|
||||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
|
|
||||||
|
|
||||||
cd cli-repo
|
|
||||||
|
|
||||||
cp -f ../openapi.json openapi.json
|
|
||||||
|
|
||||||
git config user.name "Dokploy Bot"
|
|
||||||
git config user.email "bot@dokploy.com"
|
|
||||||
|
|
||||||
git add openapi.json
|
|
||||||
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
|
||||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
|
||||||
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
|
||||||
--allow-empty
|
|
||||||
|
|
||||||
git push
|
|
||||||
|
|
||||||
echo "✅ OpenAPI synced to CLI repository successfully"
|
|
||||||
|
|
||||||
- name: Sync to SDK repository
|
|
||||||
run: |
|
|
||||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/sdk.git sdk-repo
|
|
||||||
|
|
||||||
cd sdk-repo
|
|
||||||
|
|
||||||
cp -f ../openapi.json openapi.json
|
|
||||||
|
|
||||||
git config user.name "Dokploy Bot"
|
|
||||||
git config user.email "bot@dokploy.com"
|
|
||||||
|
|
||||||
git add openapi.json
|
|
||||||
git commit -m "chore: sync OpenAPI specification [skip ci]" \
|
|
||||||
-m "Source: ${{ github.repository }}@${{ github.sha }}" \
|
|
||||||
-m "Updated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" \
|
|
||||||
--allow-empty
|
|
||||||
|
|
||||||
git push
|
|
||||||
|
|
||||||
echo "✅ OpenAPI synced to SDK repository successfully"
|
|
||||||
|
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -4,8 +4,5 @@
|
|||||||
"editor.codeActionsOnSave": {
|
"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,52 +0,0 @@
|
|||||||
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
// Isolate the command builder from the compose-file I/O performed by
|
|
||||||
// writeDomainsToCompose; we only care about the docker invocation it emits.
|
|
||||||
vi.mock("@dokploy/server/utils/docker/domain", () => ({
|
|
||||||
writeDomainsToCompose: vi.fn().mockResolvedValue(""),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const baseCompose = {
|
|
||||||
appName: "my-app",
|
|
||||||
sourceType: "raw",
|
|
||||||
command: "",
|
|
||||||
composePath: "docker-compose.yml",
|
|
||||||
composeType: "stack",
|
|
||||||
isolatedDeployment: false,
|
|
||||||
randomize: false,
|
|
||||||
suffix: "",
|
|
||||||
serverId: null,
|
|
||||||
env: "",
|
|
||||||
mounts: [],
|
|
||||||
domains: [],
|
|
||||||
environment: { project: { env: "" }, env: "" },
|
|
||||||
} as unknown as Parameters<typeof getBuildComposeCommand>[0];
|
|
||||||
|
|
||||||
// Regression coverage for #4401: the deploy command runs under `env -i`, which
|
|
||||||
// clears the environment except for the vars listed explicitly. HOME must be
|
|
||||||
// preserved so docker can resolve ~/.docker/config.json — otherwise
|
|
||||||
// `docker stack deploy --with-registry-auth` ships no credentials to the swarm
|
|
||||||
// and private-registry images fail to pull.
|
|
||||||
describe("getBuildComposeCommand registry auth (#4401)", () => {
|
|
||||||
it("preserves HOME for swarm stack deploys", async () => {
|
|
||||||
const command = await getBuildComposeCommand({
|
|
||||||
...baseCompose,
|
|
||||||
composeType: "stack",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(command).toContain("stack deploy");
|
|
||||||
expect(command).toContain("--with-registry-auth");
|
|
||||||
expect(command).toContain('env -i PATH="$PATH" HOME="$HOME"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves HOME for docker compose deploys", async () => {
|
|
||||||
const command = await getBuildComposeCommand({
|
|
||||||
...baseCompose,
|
|
||||||
composeType: "docker-compose",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(command).toContain("compose -p my-app");
|
|
||||||
expect(command).toContain('env -i PATH="$PATH" HOME="$HOME"');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -103,51 +103,6 @@ describe("createDomainLabels", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add tls=true for certificateType none on websecure entrypoint", async () => {
|
|
||||||
const noneDomain = {
|
|
||||||
...baseDomain,
|
|
||||||
https: true,
|
|
||||||
certificateType: "none" as const,
|
|
||||||
};
|
|
||||||
const labels = await createDomainLabels(appName, noneDomain, "websecure");
|
|
||||||
expect(labels).toContain(
|
|
||||||
"traefik.http.routers.test-app-1-websecure.tls=true",
|
|
||||||
);
|
|
||||||
// no cert resolver should be set when relying on a default/custom cert
|
|
||||||
expect(labels).not.toContain(
|
|
||||||
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not add tls=true for certificateType none on web entrypoint", async () => {
|
|
||||||
const noneDomain = {
|
|
||||||
...baseDomain,
|
|
||||||
https: true,
|
|
||||||
certificateType: "none" as const,
|
|
||||||
};
|
|
||||||
const labels = await createDomainLabels(appName, noneDomain, "web");
|
|
||||||
expect(labels).not.toContain(
|
|
||||||
"traefik.http.routers.test-app-1-web.tls=true",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add tls=true for certificateType none on a custom https entrypoint", async () => {
|
|
||||||
const noneDomain = {
|
|
||||||
...baseDomain,
|
|
||||||
https: true,
|
|
||||||
customEntrypoint: "websecure-custom",
|
|
||||||
certificateType: "none" as const,
|
|
||||||
};
|
|
||||||
const labels = await createDomainLabels(
|
|
||||||
appName,
|
|
||||||
noneDomain,
|
|
||||||
"websecure-custom",
|
|
||||||
);
|
|
||||||
expect(labels).toContain(
|
|
||||||
"traefik.http.routers.test-app-1-websecure-custom.tls=true",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle different ports correctly", async () => {
|
it("should handle different ports correctly", async () => {
|
||||||
const customPortDomain = { ...baseDomain, port: 3000 };
|
const customPortDomain = { ...baseDomain, port: 3000 };
|
||||||
const labels = await createDomainLabels(appName, customPortDomain, "web");
|
const labels = await createDomainLabels(appName, customPortDomain, "web");
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,8 +65,6 @@ const baseSettings: WebServerSettings = {
|
|||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
remoteServersOnly: false,
|
|
||||||
enforceSSO: false,
|
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -424,26 +424,6 @@ test("Custom entrypoint with internalPath adds addprefix middleware", async () =
|
|||||||
expect(router.entryPoints).toEqual(["custom"]);
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("stripPath and internalPath together: stripprefix must come before addprefix", async () => {
|
|
||||||
const router = await createRouterConfig(
|
|
||||||
baseApp,
|
|
||||||
{
|
|
||||||
...baseDomain,
|
|
||||||
path: "/public",
|
|
||||||
stripPath: true,
|
|
||||||
internalPath: "/app/v2",
|
|
||||||
},
|
|
||||||
"web",
|
|
||||||
);
|
|
||||||
|
|
||||||
const stripIndex = router.middlewares?.indexOf("stripprefix--1") ?? -1;
|
|
||||||
const addIndex = router.middlewares?.indexOf("addprefix--1") ?? -1;
|
|
||||||
|
|
||||||
expect(stripIndex).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(addIndex).toBeGreaterThanOrEqual(0);
|
|
||||||
expect(stripIndex).toBeLessThan(addIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Custom entrypoint with https and custom cert resolver", async () => {
|
test("Custom entrypoint with https and custom cert resolver", async () => {
|
||||||
const router = await createRouterConfig(
|
const router = await createRouterConfig(
|
||||||
baseApp,
|
baseApp,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<FormLabel>Memory Limit</FormLabel>
|
<FormLabel>Memory Limit</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -263,7 +263,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<FormLabel>Memory Reservation</FormLabel>
|
<FormLabel>Memory Reservation</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -303,7 +303,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<FormLabel>CPU Limit</FormLabel>
|
<FormLabel>CPU Limit</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -343,7 +343,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<FormLabel>CPU Reservation</FormLabel>
|
<FormLabel>CPU Reservation</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -379,7 +379,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<FormLabel className="text-base">Ulimits</FormLabel>
|
<FormLabel className="text-base">Ulimits</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { Check, Copy, Loader2 } from "lucide-react";
|
import { Check, Copy, Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { AnalyzeLogs } from "@/components/dashboard/docker/logs/analyze-logs";
|
|
||||||
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 { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -166,7 +165,6 @@ export const ShowDeployment = ({
|
|||||||
<Copy className="h-3.5 w-3.5" />
|
<Copy className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<AnalyzeLogs logs={filteredLogs} context="build" />
|
|
||||||
|
|
||||||
{serverId && (
|
{serverId && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -666,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Custom Entrypoint</FormLabel>
|
<FormLabel>Custom Entrypoint</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Use custom entrypoint for domain
|
Use custom entrypoint for domina
|
||||||
<br />
|
<br />
|
||||||
"web" and/or "websecure" is used by default.
|
"web" and/or "websecure" is used by default.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
@@ -806,7 +806,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
<FormLabel>Middlewares</FormLabel>
|
<FormLabel>Middlewares</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
?
|
?
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -56,17 +56,17 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
|
|
||||||
const mutationMap = {
|
const mutationMap = {
|
||||||
compose: () => api.compose.saveEnvironment.useMutation(),
|
compose: () => api.compose.update.useMutation(),
|
||||||
libsql: () => api.libsql.saveEnvironment.useMutation(),
|
libsql: () => api.libsql.update.useMutation(),
|
||||||
mariadb: () => api.mariadb.saveEnvironment.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
mongo: () => api.mongo.saveEnvironment.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
mysql: () => api.mysql.saveEnvironment.useMutation(),
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
postgres: () => api.postgres.saveEnvironment.useMutation(),
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
redis: () => api.redis.saveEnvironment.useMutation(),
|
redis: () => api.redis.update.useMutation(),
|
||||||
};
|
};
|
||||||
const { mutateAsync, isPending } = mutationMap[type]
|
const { mutateAsync, isPending } = mutationMap[type]
|
||||||
? mutationMap[type]()
|
? mutationMap[type]()
|
||||||
: api.mongo.saveEnvironment.useMutation();
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
const form = useForm<EnvironmentSchema>({
|
const form = useForm<EnvironmentSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -59,7 +55,7 @@ interface Props {
|
|||||||
|
|
||||||
export const SaveGitProvider = ({ applicationId }: Props) => {
|
export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||||
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
const { data, refetch } = api.application.one.useQuery({ applicationId });
|
||||||
const { data: sshKeys } = api.sshKey.allForApps.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutateAsync, isPending } =
|
const { mutateAsync, isPending } =
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,290 +0,0 @@
|
|||||||
import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
|
|
||||||
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
|
|
||||||
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
|
|
||||||
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const DockerLogsId = dynamic(
|
|
||||||
() =>
|
|
||||||
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
|
||||||
(e) => e.DockerLogsId,
|
|
||||||
),
|
|
||||||
{
|
|
||||||
ssr: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
appName: string;
|
|
||||||
serverId?: string;
|
|
||||||
appType: "stack" | "docker-compose";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowComposeContainers = ({
|
|
||||||
appName,
|
|
||||||
appType,
|
|
||||||
serverId,
|
|
||||||
}: Props) => {
|
|
||||||
const { data, isPending, refetch } =
|
|
||||||
api.docker.getContainersByAppNameMatch.useQuery(
|
|
||||||
{
|
|
||||||
appName,
|
|
||||||
appType,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!appName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-xl">Containers</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Inspect each container in this compose and run basic lifecycle
|
|
||||||
actions.
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
<RefreshCw className={`h-4 w-4 ${isPending ? "animate-spin" : ""}`} />
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{isPending ? (
|
|
||||||
<div className="flex items-center justify-center h-[20vh]">
|
|
||||||
<Loader2 className="animate-spin h-6 w-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
) : !data || data.length === 0 ? (
|
|
||||||
<div className="flex items-center justify-center h-[20vh]">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
No containers found. Deploy the compose to see containers here.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-md border">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Name</TableHead>
|
|
||||||
<TableHead>State</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead>Container ID</TableHead>
|
|
||||||
<TableHead className="text-right" />
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{data.map((container) => (
|
|
||||||
<ContainerRow
|
|
||||||
key={container.containerId}
|
|
||||||
container={container}
|
|
||||||
serverId={serverId}
|
|
||||||
onActionComplete={() => refetch()}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ContainerRowProps {
|
|
||||||
container: {
|
|
||||||
containerId: string;
|
|
||||||
name: string;
|
|
||||||
state: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
serverId?: string;
|
|
||||||
onActionComplete: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContainerRow = ({
|
|
||||||
container,
|
|
||||||
serverId,
|
|
||||||
onActionComplete,
|
|
||||||
}: ContainerRowProps) => {
|
|
||||||
const [logsOpen, setLogsOpen] = useState(false);
|
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const restartMutation = api.docker.restartContainer.useMutation();
|
|
||||||
const startMutation = api.docker.startContainer.useMutation();
|
|
||||||
const stopMutation = api.docker.stopContainer.useMutation();
|
|
||||||
const killMutation = api.docker.killContainer.useMutation();
|
|
||||||
|
|
||||||
const handleAction = async (
|
|
||||||
action: string,
|
|
||||||
mutationFn: typeof restartMutation,
|
|
||||||
) => {
|
|
||||||
setActionLoading(action);
|
|
||||||
try {
|
|
||||||
await mutationFn.mutateAsync({
|
|
||||||
containerId: container.containerId,
|
|
||||||
serverId,
|
|
||||||
});
|
|
||||||
toast.success(`Container ${action} successfully`);
|
|
||||||
onActionComplete();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
`Failed to ${action} container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setActionLoading(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell className="font-medium">{container.name}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
container.state === "running"
|
|
||||||
? "default"
|
|
||||||
: container.state === "exited"
|
|
||||||
? "secondary"
|
|
||||||
: "destructive"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{container.state}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{container.status}</TableCell>
|
|
||||||
<TableCell className="font-mono text-sm text-muted-foreground">
|
|
||||||
{container.containerId}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Dialog open={logsOpen} onOpenChange={setLogsOpen}>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
||||||
{actionLoading ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
View Logs
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<ShowContainerConfig
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={serverId || ""}
|
|
||||||
/>
|
|
||||||
<ShowContainerMounts
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={serverId || ""}
|
|
||||||
/>
|
|
||||||
<ShowContainerNetworks
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={serverId || ""}
|
|
||||||
/>
|
|
||||||
<DockerTerminalModal
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={serverId || ""}
|
|
||||||
>
|
|
||||||
Terminal
|
|
||||||
</DockerTerminalModal>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
disabled={actionLoading !== null}
|
|
||||||
onClick={() => handleAction("restart", restartMutation)}
|
|
||||||
>
|
|
||||||
Restart
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
disabled={actionLoading !== null}
|
|
||||||
onClick={() => handleAction("start", startMutation)}
|
|
||||||
>
|
|
||||||
Start
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
disabled={actionLoading !== null}
|
|
||||||
onClick={() => handleAction("stop", stopMutation)}
|
|
||||||
>
|
|
||||||
Stop
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer text-red-500 focus:text-red-600"
|
|
||||||
disabled={actionLoading !== null}
|
|
||||||
onClick={() => handleAction("kill", killMutation)}
|
|
||||||
>
|
|
||||||
Kill
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<DialogContent className="sm:max-w-7xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>View Logs</DialogTitle>
|
|
||||||
<DialogDescription>Logs for {container.name}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<DockerLogsId
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={serverId}
|
|
||||||
runType="native"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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),
|
||||||
@@ -422,7 +418,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
?
|
?
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -59,7 +55,7 @@ interface Props {
|
|||||||
|
|
||||||
export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||||
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
const { data, refetch } = api.compose.one.useQuery({ composeId });
|
||||||
const { data: sshKeys } = api.sshKey.allForApps.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutateAsync, isPending } = api.compose.update.useMutation();
|
const { mutateAsync, isPending } = api.compose.update.useMutation();
|
||||||
|
|||||||
@@ -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"),
|
||||||
@@ -449,7 +445,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
?
|
?
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -440,7 +436,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger type="button">
|
<TooltipTrigger>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
?
|
?
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,220 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import {
|
|
||||||
Bot,
|
|
||||||
Check,
|
|
||||||
Copy,
|
|
||||||
Loader2,
|
|
||||||
RotateCcw,
|
|
||||||
Settings,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import type { LogLine } from "./utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
logs: LogLine[];
|
|
||||||
context: "build" | "runtime";
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_LOG_LINES = 200;
|
|
||||||
|
|
||||||
export function AnalyzeLogs({ logs, context }: Props) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [aiId, setAiId] = useState<string>("");
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
|
|
||||||
enabled: open,
|
|
||||||
});
|
|
||||||
const { mutate, isPending, data, reset } = api.ai.analyzeLogs.useMutation({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Analysis failed", {
|
|
||||||
description: error.message,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleAnalyze = () => {
|
|
||||||
if (!aiId || logs.length === 0) return;
|
|
||||||
|
|
||||||
const logsText = logs
|
|
||||||
.slice(-MAX_LOG_LINES)
|
|
||||||
.map((l) => l.message)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
mutate({ aiId, logs: logsText, context });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = () => {
|
|
||||||
if (!data?.analysis) return;
|
|
||||||
const success = copy(data.analysis);
|
|
||||||
if (success) {
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
setOpen(isOpen);
|
|
||||||
if (!isOpen) {
|
|
||||||
reset();
|
|
||||||
setAiId("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-9"
|
|
||||||
disabled={logs.length === 0}
|
|
||||||
title="Analyze logs with AI"
|
|
||||||
>
|
|
||||||
<Bot className="mr-2 size-4" />
|
|
||||||
AI
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[550px] p-0" align="end">
|
|
||||||
<div className="flex items-center justify-between border-b px-4 py-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Bot className="h-4 w-4" />
|
|
||||||
<span className="text-sm font-medium">Log Analysis</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-6 w-6"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 space-y-3">
|
|
||||||
{!data?.analysis ? (
|
|
||||||
providers && providers.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center gap-3 py-2 text-center">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No AI providers configured. Set up a provider to start
|
|
||||||
analyzing logs.
|
|
||||||
</p>
|
|
||||||
<Button size="sm" variant="outline" asChild>
|
|
||||||
<Link href="/dashboard/settings/ai">
|
|
||||||
<Settings className="mr-2 h-3.5 w-3.5" />
|
|
||||||
Configure AI Provider
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Select value={aiId} onValueChange={setAiId}>
|
|
||||||
<SelectTrigger className="h-9 text-sm">
|
|
||||||
<SelectValue placeholder="Select AI provider..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{providers?.map((p) => (
|
|
||||||
<SelectItem key={p.aiId} value={p.aiId}>
|
|
||||||
{p.name} ({p.model})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="w-full"
|
|
||||||
disabled={!aiId || isPending || logs.length === 0}
|
|
||||||
onClick={handleAnalyze}
|
|
||||||
>
|
|
||||||
{isPending ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
|
||||||
Analyzing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Bot className="mr-2 h-3.5 w-3.5" />
|
|
||||||
Analyze{" "}
|
|
||||||
{logs.length > MAX_LOG_LINES
|
|
||||||
? `last ${MAX_LOG_LINES}`
|
|
||||||
: logs.length}{" "}
|
|
||||||
lines
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="max-h-[400px] overflow-y-auto">
|
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none text-sm break-words">
|
|
||||||
<ReactMarkdown>{data.analysis}</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => {
|
|
||||||
reset();
|
|
||||||
handleAnalyze();
|
|
||||||
}}
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
{isPending ? (
|
|
||||||
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<RotateCcw className="mr-2 h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
Re-analyze
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCopy}
|
|
||||||
title="Copy analysis to clipboard"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="h-3.5 w-3.5" />
|
|
||||||
) : (
|
|
||||||
<Copy className="h-3.5 w-3.5" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
reset();
|
|
||||||
setAiId("");
|
|
||||||
}}
|
|
||||||
title="Change provider"
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AnalyzeLogs } from "./analyze-logs";
|
|
||||||
import { LineCountFilter } from "./line-count-filter";
|
import { LineCountFilter } from "./line-count-filter";
|
||||||
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
|
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
|
||||||
import { StatusLogsFilter } from "./status-logs-filter";
|
import { StatusLogsFilter } from "./status-logs-filter";
|
||||||
@@ -347,13 +346,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 +361,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 +373,16 @@ 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" />
|
|
||||||
</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,112 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
containerId: string;
|
|
||||||
serverId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Mount {
|
|
||||||
Type: string;
|
|
||||||
Source: string;
|
|
||||||
Destination: string;
|
|
||||||
Mode: string;
|
|
||||||
RW: boolean;
|
|
||||||
Propagation: string;
|
|
||||||
Name?: string;
|
|
||||||
Driver?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowContainerMounts = ({ containerId, serverId }: Props) => {
|
|
||||||
const { data } = api.docker.getConfig.useQuery(
|
|
||||||
{
|
|
||||||
containerId,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!containerId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const mounts: Mount[] = data?.Mounts ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
View Mounts
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Container Mounts</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Volume and bind mounts for this container
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="overflow-auto max-h-[70vh]">
|
|
||||||
{mounts.length === 0 ? (
|
|
||||||
<div className="text-center text-muted-foreground py-8">
|
|
||||||
No mounts found for this container.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Type</TableHead>
|
|
||||||
<TableHead>Source</TableHead>
|
|
||||||
<TableHead>Destination</TableHead>
|
|
||||||
<TableHead>Mode</TableHead>
|
|
||||||
<TableHead>Read/Write</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{mounts.map((mount, index) => (
|
|
||||||
<TableRow key={index}>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline">{mount.Type}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs max-w-[250px] truncate">
|
|
||||||
{mount.Name || mount.Source}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs max-w-[250px] truncate">
|
|
||||||
{mount.Destination}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs">
|
|
||||||
{mount.Mode || "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={mount.RW ? "default" : "secondary"}>
|
|
||||||
{mount.RW ? "RW" : "RO"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
containerId: string;
|
|
||||||
serverId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Network {
|
|
||||||
IPAMConfig: unknown;
|
|
||||||
Links: unknown;
|
|
||||||
Aliases: string[] | null;
|
|
||||||
MacAddress: string;
|
|
||||||
NetworkID: string;
|
|
||||||
EndpointID: string;
|
|
||||||
Gateway: string;
|
|
||||||
IPAddress: string;
|
|
||||||
IPPrefixLen: number;
|
|
||||||
IPv6Gateway: string;
|
|
||||||
GlobalIPv6Address: string;
|
|
||||||
GlobalIPv6PrefixLen: number;
|
|
||||||
DriverOpts: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowContainerNetworks = ({ containerId, serverId }: Props) => {
|
|
||||||
const { data } = api.docker.getConfig.useQuery(
|
|
||||||
{
|
|
||||||
containerId,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!containerId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const networks: Record<string, Network> =
|
|
||||||
data?.NetworkSettings?.Networks ?? {};
|
|
||||||
const entries = Object.entries(networks);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
View Networks
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="w-full md:w-[70vw] min-w-[70vw]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Container Networks</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Networks attached to this container
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="overflow-auto max-h-[70vh]">
|
|
||||||
{entries.length === 0 ? (
|
|
||||||
<div className="text-center text-muted-foreground py-8">
|
|
||||||
No networks found for this container.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Network</TableHead>
|
|
||||||
<TableHead>IP Address</TableHead>
|
|
||||||
<TableHead>Gateway</TableHead>
|
|
||||||
<TableHead>MAC Address</TableHead>
|
|
||||||
<TableHead>Aliases</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{entries.map(([name, network]) => (
|
|
||||||
<TableRow key={name}>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant="outline">{name}</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs">
|
|
||||||
{network.IPAddress
|
|
||||||
? `${network.IPAddress}/${network.IPPrefixLen}`
|
|
||||||
: "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs">
|
|
||||||
{network.Gateway || "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-mono text-xs">
|
|
||||||
{network.MacAddress || "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs">
|
|
||||||
{network.Aliases?.join(", ") || "-"}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -10,8 +10,6 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ShowContainerConfig } from "../config/show-container-config";
|
import { ShowContainerConfig } from "../config/show-container-config";
|
||||||
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
||||||
import { ShowContainerMounts } from "../mounts/show-container-mounts";
|
|
||||||
import { ShowContainerNetworks } from "../networks/show-container-networks";
|
|
||||||
import { RemoveContainerDialog } from "../remove/remove-container";
|
import { RemoveContainerDialog } from "../remove/remove-container";
|
||||||
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
||||||
import { UploadFileModal } from "../upload/upload-file-modal";
|
import { UploadFileModal } from "../upload/upload-file-modal";
|
||||||
@@ -125,14 +123,6 @@ export const columns: ColumnDef<Container>[] = [
|
|||||||
containerId={container.containerId}
|
containerId={container.containerId}
|
||||||
serverId={container.serverId || ""}
|
serverId={container.serverId || ""}
|
||||||
/>
|
/>
|
||||||
<ShowContainerMounts
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={container.serverId || ""}
|
|
||||||
/>
|
|
||||||
<ShowContainerNetworks
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={container.serverId || ""}
|
|
||||||
/>
|
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
containerId={container.containerId}
|
containerId={container.containerId}
|
||||||
serverId={container.serverId || ""}
|
serverId={container.serverId || ""}
|
||||||
|
|||||||
@@ -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,291 +0,0 @@
|
|||||||
import { formatDistanceToNow } from "date-fns";
|
|
||||||
import { ArrowRight, Rocket, Server } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
type DeploymentStatus = "idle" | "running" | "done" | "error";
|
|
||||||
|
|
||||||
const statusDotClass: Record<string, string> = {
|
|
||||||
done: "bg-emerald-500",
|
|
||||||
running: "bg-amber-500",
|
|
||||||
error: "bg-red-500",
|
|
||||||
idle: "bg-muted-foreground/40",
|
|
||||||
};
|
|
||||||
|
|
||||||
function getServiceInfo(d: any) {
|
|
||||||
const app = d.application;
|
|
||||||
const comp = d.compose;
|
|
||||||
const serverName: string =
|
|
||||||
d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy";
|
|
||||||
if (app?.environment?.project && app.environment) {
|
|
||||||
return {
|
|
||||||
name: app.name as string,
|
|
||||||
environment: app.environment.name as string,
|
|
||||||
projectName: app.environment.project.name as string,
|
|
||||||
serverName,
|
|
||||||
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (comp?.environment?.project && comp.environment) {
|
|
||||||
return {
|
|
||||||
name: comp.name as string,
|
|
||||||
environment: comp.environment.name as string,
|
|
||||||
projectName: comp.environment.project.name as string,
|
|
||||||
serverName,
|
|
||||||
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatCard({
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
delta,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
delta?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between">
|
|
||||||
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="text-3xl font-semibold tracking-tight">{value}</span>
|
|
||||||
{delta && (
|
|
||||||
<span className="text-xs text-muted-foreground">{delta}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusListCard({
|
|
||||||
label,
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
items: { dotClass: string; label: string; count: number }[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3">
|
|
||||||
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
<ul className="flex flex-col gap-1.5">
|
|
||||||
{items.map((item) => (
|
|
||||||
<li key={item.label} className="flex items-center gap-2.5 text-sm">
|
|
||||||
<span
|
|
||||||
className={`size-2 rounded-full shrink-0 ${item.dotClass}`}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<span className="font-semibold tabular-nums w-8">{item.count}</span>
|
|
||||||
<span className="text-muted-foreground">{item.label}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowHome = () => {
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
|
||||||
const { data: homeStats } = api.project.homeStats.useQuery();
|
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canReadDeployments = !!permissions?.deployment.read;
|
|
||||||
const { data: deployments } = api.deployment.allCentralized.useQuery(
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
enabled: canReadDeployments,
|
|
||||||
refetchInterval: 10000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstName = auth?.user?.firstName?.trim();
|
|
||||||
|
|
||||||
const totals = homeStats ?? {
|
|
||||||
projects: 0,
|
|
||||||
environments: 0,
|
|
||||||
applications: 0,
|
|
||||||
compose: 0,
|
|
||||||
databases: 0,
|
|
||||||
services: 0,
|
|
||||||
};
|
|
||||||
const statusBreakdown = homeStats?.status ?? {
|
|
||||||
running: 0,
|
|
||||||
error: 0,
|
|
||||||
idle: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const recentDeployments = useMemo(() => {
|
|
||||||
if (!deployments) return [];
|
|
||||||
return [...deployments]
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
|
||||||
)
|
|
||||||
.slice(0, 10);
|
|
||||||
}, [deployments]);
|
|
||||||
|
|
||||||
const deployStats = useMemo(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
const weekMs = 7 * 24 * 60 * 60 * 1000;
|
|
||||||
const lastStart = now - weekMs;
|
|
||||||
const prevStart = now - 2 * weekMs;
|
|
||||||
|
|
||||||
const last: NonNullable<typeof deployments> = [];
|
|
||||||
const prev: NonNullable<typeof deployments> = [];
|
|
||||||
for (const d of deployments ?? []) {
|
|
||||||
const t = new Date(d.createdAt).getTime();
|
|
||||||
if (t >= lastStart) last.push(d);
|
|
||||||
else if (t >= prevStart) prev.push(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastCount = last.length;
|
|
||||||
const prevCount = prev.length;
|
|
||||||
let delta: string | undefined;
|
|
||||||
if (prevCount > 0) {
|
|
||||||
const pct = Math.round(((lastCount - prevCount) / prevCount) * 100);
|
|
||||||
delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`;
|
|
||||||
} else if (lastCount > 0) {
|
|
||||||
delta = "no prior data";
|
|
||||||
} else {
|
|
||||||
delta = "no activity yet";
|
|
||||||
}
|
|
||||||
|
|
||||||
return { value: String(lastCount), delta };
|
|
||||||
}, [deployments]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]">
|
|
||||||
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full">
|
|
||||||
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
|
|
||||||
<h1 className="text-3xl font-semibold tracking-tight">
|
|
||||||
{firstName ? `Welcome back, ${firstName}` : "Welcome back"}
|
|
||||||
</h1>
|
|
||||||
<Button asChild variant="secondary" className="w-fit">
|
|
||||||
<Link href="/dashboard/projects">
|
|
||||||
Go to projects
|
|
||||||
<ArrowRight className="size-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
<StatCard
|
|
||||||
label="Projects"
|
|
||||||
value={String(totals.projects)}
|
|
||||||
delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Services"
|
|
||||||
value={String(totals.services)}
|
|
||||||
delta={`${totals.applications} apps · ${totals.compose} compose · ${totals.databases} db`}
|
|
||||||
/>
|
|
||||||
<StatCard
|
|
||||||
label="Deploys / 7d"
|
|
||||||
value={deployStats.value}
|
|
||||||
delta={deployStats.delta}
|
|
||||||
/>
|
|
||||||
<StatusListCard
|
|
||||||
label="Status"
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
dotClass: "bg-emerald-500",
|
|
||||||
label: "running",
|
|
||||||
count: statusBreakdown.running,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dotClass: "bg-red-500",
|
|
||||||
label: "errored",
|
|
||||||
count: statusBreakdown.error,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dotClass: "bg-muted-foreground/40",
|
|
||||||
label: "idle",
|
|
||||||
count: statusBreakdown.idle,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-xl border bg-background">
|
|
||||||
<div className="flex items-center justify-between px-5 py-4 border-b">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Rocket className="size-4 text-muted-foreground" />
|
|
||||||
<h2 className="text-sm font-semibold">Recent deployments</h2>
|
|
||||||
</div>
|
|
||||||
{canReadDeployments && (
|
|
||||||
<Link
|
|
||||||
href="/dashboard/deployments"
|
|
||||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
view all →
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!canReadDeployments ? (
|
|
||||||
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
|
|
||||||
<Rocket className="size-8 opacity-40" />
|
|
||||||
<span>You do not have permission to view deployments.</span>
|
|
||||||
</div>
|
|
||||||
) : recentDeployments.length === 0 ? (
|
|
||||||
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
|
|
||||||
<Rocket className="size-8 opacity-40" />
|
|
||||||
<span>No deployments yet.</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ul className="divide-y">
|
|
||||||
{recentDeployments.map((d) => {
|
|
||||||
const info = getServiceInfo(d);
|
|
||||||
if (!info) return null;
|
|
||||||
const status = (d.status ?? "idle") as DeploymentStatus;
|
|
||||||
return (
|
|
||||||
<li key={d.deploymentId}>
|
|
||||||
<Link
|
|
||||||
href={info.href}
|
|
||||||
className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col min-w-0 flex-1">
|
|
||||||
<span className="text-sm truncate">{info.name}</span>
|
|
||||||
<span className="text-xs text-muted-foreground truncate">
|
|
||||||
{info.projectName} · {info.environment}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate">
|
|
||||||
<Server className="size-3 shrink-0" />
|
|
||||||
<span className="truncate">{info.serverName}</span>
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline">
|
|
||||||
{status}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground w-24 text-right hidden md:inline">
|
|
||||||
{formatDistanceToNow(new Date(d.createdAt), {
|
|
||||||
addSuffix: true,
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
|
||||||
logs →
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
import { 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;
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
const params = `authSource=admin${data?.replicaSets ? "" : "&directConnection=true"}`;
|
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
return `mongodb://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}/?${params}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setConnectionUrl(buildConnectionUrl());
|
setConnectionUrl(buildConnectionUrl());
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -62,7 +62,7 @@ export const ShowInternalMongoCredentials = ({ mongoId }: Props) => {
|
|||||||
<Label>Internal Connection URL </Label>
|
<Label>Internal Connection URL </Label>
|
||||||
<ToggleVisibilityInput
|
<ToggleVisibilityInput
|
||||||
disabled
|
disabled
|
||||||
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017/?authSource=admin${data?.replicaSets ? "" : "&directConnection=true"}`}
|
value={`mongodb://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:27017`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -220,11 +220,11 @@ export const ContainerFreeMonitoring = ({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Used: {String(currentData.cpu.value ?? "0%")}
|
Used: {currentData.cpu.value}
|
||||||
</span>
|
</span>
|
||||||
<Progress
|
<Progress
|
||||||
value={Number.parseInt(
|
value={Number.parseInt(
|
||||||
String(currentData.cpu.value ?? "0%").replace("%", ""),
|
currentData.cpu.value.replace("%", ""),
|
||||||
10,
|
10,
|
||||||
)}
|
)}
|
||||||
className="w-[100%]"
|
className="w-[100%]"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ interface Props {
|
|||||||
export const AddApplication = ({ environmentId, projectName }: Props) => {
|
export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: webServerSettings } =
|
|
||||||
api.settings.getWebServerSettings.useQuery();
|
|
||||||
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
@@ -174,8 +171,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
Select a Server{" "}
|
Select a Server {!isCloud ? "(Optional)" : ""}
|
||||||
{showLocalOption ? "(Optional)" : ""}
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -195,19 +191,17 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
field.value || (showLocalOption ? "dokploy" : undefined)
|
field.value || (!isCloud ? "dokploy" : undefined)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||||
showLocalOption ? "Dokploy" : "Select a Server"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{showLocalOption && (
|
{!isCloud && (
|
||||||
<SelectItem value="dokploy">
|
<SelectItem value="dokploy">
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<span className="flex items-center gap-2 justify-between w-full">
|
||||||
<span>Dokploy</span>
|
<span>Dokploy</span>
|
||||||
@@ -231,8 +225,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>
|
||||||
Servers (
|
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||||
{servers?.length + (showLocalOption ? 1 : 0)})
|
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: webServerSettings } =
|
|
||||||
api.settings.getWebServerSettings.useQuery();
|
|
||||||
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
|
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const { mutateAsync, isPending, error, isError } =
|
const { mutateAsync, isPending, error, isError } =
|
||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
@@ -185,8 +182,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
Select a Server{" "}
|
Select a Server {!isCloud ? "(Optional)" : ""}
|
||||||
{showLocalOption ? "(Optional)" : ""}
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -206,19 +202,17 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
field.value || (showLocalOption ? "dokploy" : undefined)
|
field.value || (!isCloud ? "dokploy" : undefined)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||||
showLocalOption ? "Dokploy" : "Select a Server"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{showLocalOption && (
|
{!isCloud && (
|
||||||
<SelectItem value="dokploy">
|
<SelectItem value="dokploy">
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<span className="flex items-center gap-2 justify-between w-full">
|
||||||
<span>Dokploy</span>
|
<span>Dokploy</span>
|
||||||
@@ -242,8 +236,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>
|
||||||
Servers (
|
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||||
{servers?.length + (showLocalOption ? 1 : 0)})
|
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|||||||
@@ -219,9 +219,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: webServerSettings } =
|
|
||||||
api.settings.getWebServerSettings.useQuery();
|
|
||||||
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
|
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const libsqlMutation = api.libsql.create.useMutation();
|
const libsqlMutation = api.libsql.create.useMutation();
|
||||||
const mariadbMutation = api.mariadb.create.useMutation();
|
const mariadbMutation = api.mariadb.create.useMutation();
|
||||||
@@ -473,20 +470,19 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
field.value ||
|
field.value || (!isCloud ? "dokploy" : undefined)
|
||||||
(showLocalOption ? "dokploy" : undefined)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={
|
placeholder={
|
||||||
showLocalOption ? "Dokploy" : "Select a Server"
|
!isCloud ? "Dokploy" : "Select a Server"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{showLocalOption && (
|
{!isCloud && (
|
||||||
<SelectItem value="dokploy">
|
<SelectItem value="dokploy">
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<span className="flex items-center gap-2 justify-between w-full">
|
||||||
<span>Dokploy</span>
|
<span>Dokploy</span>
|
||||||
@@ -505,8 +501,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>
|
<SelectLabel>
|
||||||
Servers (
|
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||||
{servers?.length + (showLocalOption ? 1 : 0)})
|
|
||||||
</SelectLabel>
|
</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -637,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,
|
||||||
|
|||||||
@@ -298,19 +298,7 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
|
|||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex items-center gap-2 w-full justify-end">
|
<div className="flex items-center gap-2 w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={stepper.prev}
|
||||||
if (
|
|
||||||
stepper.current.id === "variant" &&
|
|
||||||
templateInfo.details
|
|
||||||
) {
|
|
||||||
setTemplateInfo((prev) => ({
|
|
||||||
...prev,
|
|
||||||
details: null,
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stepper.prev();
|
|
||||||
}}
|
|
||||||
disabled={stepper.isFirst}
|
disabled={stepper.isFirst}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -166,7 +166,6 @@ export const ShowProjects = () => {
|
|||||||
return (
|
return (
|
||||||
total +
|
total +
|
||||||
(env.applications?.length || 0) +
|
(env.applications?.length || 0) +
|
||||||
(env.libsql?.length || 0) +
|
|
||||||
(env.mariadb?.length || 0) +
|
(env.mariadb?.length || 0) +
|
||||||
(env.mongo?.length || 0) +
|
(env.mongo?.length || 0) +
|
||||||
(env.mysql?.length || 0) +
|
(env.mysql?.length || 0) +
|
||||||
@@ -179,7 +178,6 @@ export const ShowProjects = () => {
|
|||||||
return (
|
return (
|
||||||
total +
|
total +
|
||||||
(env.applications?.length || 0) +
|
(env.applications?.length || 0) +
|
||||||
(env.libsql?.length || 0) +
|
|
||||||
(env.mariadb?.length || 0) +
|
(env.mariadb?.length || 0) +
|
||||||
(env.mongo?.length || 0) +
|
(env.mongo?.length || 0) +
|
||||||
(env.mysql?.length || 0) +
|
(env.mysql?.length || 0) +
|
||||||
@@ -344,7 +342,7 @@ export const ShowProjects = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border flex flex-col">
|
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
|
||||||
<CardHeader>
|
<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 +489,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"
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export const SearchCommand = () => {
|
|||||||
<CommandGroup heading={"Application"} hidden={true}>
|
<CommandGroup heading={"Application"} hidden={true}>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/home");
|
router.push("/dashboard/projects");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { loadStripe } from "@stripe/stripe-js";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Bell,
|
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -25,18 +24,8 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { NumberInput } from "@/components/ui/input";
|
import { NumberInput } from "@/components/ui/input";
|
||||||
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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -101,8 +90,6 @@ export const ShowBilling = () => {
|
|||||||
api.stripe.createCustomerPortalSession.useMutation();
|
api.stripe.createCustomerPortalSession.useMutation();
|
||||||
const { mutateAsync: upgradeSubscription, isPending: isUpgrading } =
|
const { mutateAsync: upgradeSubscription, isPending: isUpgrading } =
|
||||||
api.stripe.upgradeSubscription.useMutation();
|
api.stripe.upgradeSubscription.useMutation();
|
||||||
const { mutateAsync: updateInvoiceNotifications } =
|
|
||||||
api.stripe.updateInvoiceNotifications.useMutation();
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
||||||
@@ -164,66 +151,14 @@ export const ShowBilling = () => {
|
|||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="bg-sidebar p-2.5 rounded-xl max-w-6xl mx-auto">
|
<Card className="bg-sidebar p-2.5 rounded-xl max-w-6xl mx-auto">
|
||||||
<div className="rounded-xl bg-background shadow-md">
|
<div className="rounded-xl bg-background shadow-md">
|
||||||
<CardHeader className="flex flex-row items-start justify-between">
|
<CardHeader>
|
||||||
<div>
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
<CreditCard className="size-6 text-muted-foreground self-center" />
|
||||||
<CreditCard className="size-6 text-muted-foreground self-center" />
|
Billing
|
||||||
Billing
|
</CardTitle>
|
||||||
</CardTitle>
|
<CardDescription>
|
||||||
<CardDescription>
|
Manage your subscription and invoices
|
||||||
Manage your subscription and invoices
|
</CardDescription>
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
{(admin?.user.stripeSubscriptionId || isEnterpriseCloud) && (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<Bell className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Notification Settings</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Configure your billing email notifications.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="invoice-notifications">
|
|
||||||
Invoice Notifications
|
|
||||||
</Label>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Receive email notifications for payments and failed
|
|
||||||
charges.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="invoice-notifications"
|
|
||||||
checked={admin?.user.sendInvoiceNotifications ?? false}
|
|
||||||
onCheckedChange={async (checked) => {
|
|
||||||
await updateInvoiceNotifications({
|
|
||||||
enabled: checked,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
utils.user.get.invalidate();
|
|
||||||
toast.success(
|
|
||||||
checked
|
|
||||||
? "Invoice notifications enabled"
|
|
||||||
: "Invoice notifications disabled",
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
"Failed to update invoice notifications",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4 py-4 border-t">
|
<CardContent className="space-y-4 py-4 border-t">
|
||||||
<nav className="flex space-x-2 border-b">
|
<nav className="flex space-x-2 border-b">
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import {
|
import { Check, ChevronDown, PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
Check,
|
|
||||||
ChevronDown,
|
|
||||||
Loader2,
|
|
||||||
PenBoxIcon,
|
|
||||||
Plug,
|
|
||||||
PlusIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
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";
|
||||||
@@ -44,34 +37,10 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const AI_PROVIDERS = [
|
|
||||||
{ name: "OpenAI", apiUrl: "https://api.openai.com/v1" },
|
|
||||||
{ name: "Anthropic", apiUrl: "https://api.anthropic.com/v1" },
|
|
||||||
{
|
|
||||||
name: "Google Gemini",
|
|
||||||
apiUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
||||||
},
|
|
||||||
{ name: "Mistral", apiUrl: "https://api.mistral.ai/v1" },
|
|
||||||
{ name: "Cohere", apiUrl: "https://api.cohere.ai/v2" },
|
|
||||||
{ name: "Perplexity", apiUrl: "https://api.perplexity.ai" },
|
|
||||||
{ name: "DeepInfra", apiUrl: "https://api.deepinfra.com/v1/openai" },
|
|
||||||
{ name: "Ollama", apiUrl: "http://localhost:11434" },
|
|
||||||
{ name: "OpenRouter", apiUrl: "https://openrouter.ai/api/v1" },
|
|
||||||
{ name: "Z.AI", apiUrl: "https://api.z.ai/api/paas/v4" },
|
|
||||||
{ name: "MiniMax", apiUrl: "https://api.minimax.io/v1" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
name: z.string().min(1, { message: "Name is required" }),
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
||||||
@@ -134,7 +103,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
|
const isOllama = apiUrl.includes(":11434") || apiUrl.includes("ollama");
|
||||||
const {
|
const {
|
||||||
data: models,
|
data: models,
|
||||||
isFetching: isLoadingServerModels,
|
isPending: isLoadingServerModels,
|
||||||
error: modelsError,
|
error: modelsError,
|
||||||
} = api.ai.getModels.useQuery(
|
} = api.ai.getModels.useQuery(
|
||||||
{
|
{
|
||||||
@@ -203,34 +172,6 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
<AlertBlock type="error">{modelsError.message}</AlertBlock>
|
<AlertBlock type="error">{modelsError.message}</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||||
<div className="space-y-1">
|
|
||||||
<FormLabel>Provider</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const provider = AI_PROVIDERS.find((p) => p.apiUrl === value);
|
|
||||||
if (provider) {
|
|
||||||
form.setValue("name", provider.name);
|
|
||||||
form.setValue("apiUrl", provider.apiUrl);
|
|
||||||
form.setValue("model", "");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a provider preset..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{AI_PROVIDERS.map((provider) => (
|
|
||||||
<SelectItem key={provider.apiUrl} value={provider.apiUrl}>
|
|
||||||
{provider.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-[0.8rem] text-muted-foreground">
|
|
||||||
Quick-fill provider name and URL, or configure manually below
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -312,129 +253,101 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormField
|
{!isLoadingServerModels && !models?.length && (
|
||||||
control={form.control}
|
<span className="text-sm text-muted-foreground">
|
||||||
name="model"
|
No models available
|
||||||
render={({ field }) => {
|
</span>
|
||||||
const hasModels =
|
)}
|
||||||
!isLoadingServerModels && models && models.length > 0;
|
|
||||||
const selectedModel = models?.find((m) => m.id === field.value);
|
|
||||||
const filteredModels = (models ?? []).filter((model) =>
|
|
||||||
model.id.toLowerCase().includes(modelSearch.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayModels =
|
{!isLoadingServerModels && models && models.length > 0 && (
|
||||||
field.value &&
|
<FormField
|
||||||
!filteredModels.find((m) => m.id === field.value) &&
|
control={form.control}
|
||||||
selectedModel
|
name="model"
|
||||||
? [selectedModel, ...filteredModels]
|
render={({ field }) => {
|
||||||
: filteredModels;
|
const selectedModel = models.find(
|
||||||
|
(m) => m.id === field.value,
|
||||||
|
);
|
||||||
|
const filteredModels = models.filter((model) =>
|
||||||
|
model.id.toLowerCase().includes(modelSearch.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
// Ensure selected model is always in the filtered list
|
||||||
<FormItem>
|
const displayModels =
|
||||||
<FormLabel>Model</FormLabel>
|
field.value &&
|
||||||
<div className="flex gap-2">
|
!filteredModels.find((m) => m.id === field.value) &&
|
||||||
<div className="flex-1">
|
selectedModel
|
||||||
{hasModels ? (
|
? [selectedModel, ...filteredModels]
|
||||||
<Popover
|
: filteredModels;
|
||||||
open={modelPopoverOpen}
|
|
||||||
onOpenChange={setModelPopoverOpen}
|
return (
|
||||||
>
|
<FormItem>
|
||||||
<PopoverTrigger asChild>
|
<FormLabel>Model</FormLabel>
|
||||||
<FormControl>
|
<Popover
|
||||||
<Button
|
open={modelPopoverOpen}
|
||||||
variant="outline"
|
onOpenChange={setModelPopoverOpen}
|
||||||
className={cn(
|
>
|
||||||
"w-full justify-between",
|
<PopoverTrigger asChild>
|
||||||
!field.value && "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.value
|
|
||||||
? (selectedModel?.id ?? field.value)
|
|
||||||
: "Select a model"}
|
|
||||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="w-[400px] p-0"
|
|
||||||
align="start"
|
|
||||||
>
|
|
||||||
<Command>
|
|
||||||
<CommandInput
|
|
||||||
placeholder="Search or type a custom model..."
|
|
||||||
value={modelSearch}
|
|
||||||
onValueChange={setModelSearch}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>
|
|
||||||
{modelSearch ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full cursor-pointer px-2 py-1.5 text-left text-sm hover:bg-accent"
|
|
||||||
onClick={() => {
|
|
||||||
field.onChange(modelSearch);
|
|
||||||
setModelPopoverOpen(false);
|
|
||||||
setModelSearch("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Use custom model: "{modelSearch}"
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
"No models found."
|
|
||||||
)}
|
|
||||||
</CommandEmpty>
|
|
||||||
{displayModels.map((model) => {
|
|
||||||
const isSelected = field.value === model.id;
|
|
||||||
return (
|
|
||||||
<CommandItem
|
|
||||||
key={model.id}
|
|
||||||
value={model.id}
|
|
||||||
onSelect={() => {
|
|
||||||
field.onChange(model.id);
|
|
||||||
setModelPopoverOpen(false);
|
|
||||||
setModelSearch("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
isSelected
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{model.id}
|
|
||||||
</CommandItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
) : (
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Button
|
||||||
placeholder={
|
variant="outline"
|
||||||
isLoadingServerModels
|
className={cn(
|
||||||
? "Loading models..."
|
"w-full justify-between",
|
||||||
: "Enter model name (e.g. gpt-4o)"
|
!field.value && "text-muted-foreground",
|
||||||
}
|
)}
|
||||||
disabled={isLoadingServerModels}
|
>
|
||||||
{...field}
|
{field.value
|
||||||
/>
|
? (selectedModel?.id ?? field.value)
|
||||||
|
: "Select a model"}
|
||||||
|
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
</PopoverTrigger>
|
||||||
</div>
|
<PopoverContent className="w-[400px] p-0" align="start">
|
||||||
</div>
|
<Command>
|
||||||
<FormDescription>
|
<CommandInput
|
||||||
Select a model from the list or type a custom model name
|
placeholder="Search models..."
|
||||||
</FormDescription>
|
value={modelSearch}
|
||||||
<FormMessage />
|
onValueChange={setModelSearch}
|
||||||
</FormItem>
|
/>
|
||||||
);
|
<CommandList>
|
||||||
}}
|
<CommandEmpty>No models found.</CommandEmpty>
|
||||||
/>
|
{displayModels.map((model) => {
|
||||||
|
const isSelected = field.value === model.id;
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={model.id}
|
||||||
|
value={model.id}
|
||||||
|
onSelect={() => {
|
||||||
|
field.onChange(model.id);
|
||||||
|
setModelPopoverOpen(false);
|
||||||
|
setModelSearch("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
isSelected
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{model.id}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormDescription>
|
||||||
|
Select an AI model to use
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -459,12 +372,7 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
<TestConnectionButton
|
|
||||||
apiUrl={apiUrl}
|
|
||||||
apiKey={apiKey}
|
|
||||||
model={form.watch("model")}
|
|
||||||
/>
|
|
||||||
<Button type="submit" isLoading={isPending}>
|
<Button type="submit" isLoading={isPending}>
|
||||||
{aiId ? "Update" : "Create"}
|
{aiId ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -475,42 +383,3 @@ export const HandleAi = ({ aiId }: Props) => {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function TestConnectionButton({
|
|
||||||
apiUrl,
|
|
||||||
apiKey,
|
|
||||||
model,
|
|
||||||
}: {
|
|
||||||
apiUrl: string;
|
|
||||||
apiKey: string;
|
|
||||||
model: string;
|
|
||||||
}) {
|
|
||||||
const { mutate, isPending } = api.ai.testConnection.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Connection successful");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Connection failed", {
|
|
||||||
description: error.message,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDisabled = !apiUrl || !model;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
disabled={isDisabled || isPending}
|
|
||||||
onClick={() => mutate({ apiUrl, apiKey, model })}
|
|
||||||
>
|
|
||||||
{isPending ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Plug className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Test Connection
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
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 {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -59,36 +52,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Switch checked={!!enabled} onCheckedChange={handleToggle} />
|
<Switch checked={!!enabled} onCheckedChange={handleToggle} />
|
||||||
<TooltipProvider delayDuration={0}>
|
<Label className="text-primary">Daily Docker Cleanup</Label>
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Label className="text-primary flex items-center gap-1.5 cursor-pointer">
|
|
||||||
Daily Docker Cleanup
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
|
||||||
</Label>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-sm">
|
|
||||||
<p>
|
|
||||||
Runs a full Docker cleanup daily, pruning stopped containers,
|
|
||||||
unused images, volumes, build cache, and system resources. This
|
|
||||||
may remove images built for Compose services that run on-demand
|
|
||||||
(backup runners, cron jobs, one-off tasks).
|
|
||||||
</p>
|
|
||||||
<p className="mt-1">
|
|
||||||
For custom cleanup strategies, use{" "}
|
|
||||||
<a
|
|
||||||
href="https://docs.dokploy.com/docs/core/schedule-jobs#example-1-automatic-docker-cleanup"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="underline text-primary"
|
|
||||||
>
|
|
||||||
Schedule Jobs
|
|
||||||
</a>{" "}
|
|
||||||
on your web server or remote servers.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
export const ToggleEnforceSSO = () => {
|
|
||||||
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
|
|
||||||
const { mutateAsync } = api.settings.updateEnforceSSO.useMutation();
|
|
||||||
|
|
||||||
const handleToggle = async (checked: boolean) => {
|
|
||||||
try {
|
|
||||||
await mutateAsync({ enforceSSO: checked });
|
|
||||||
await refetch();
|
|
||||||
toast.success("Enforce SSO updated");
|
|
||||||
} catch {
|
|
||||||
toast.error("Error updating Enforce SSO");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Switch checked={!!data?.enforceSSO} onCheckedChange={handleToggle} />
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Label className="text-primary flex items-center gap-1.5 cursor-pointer">
|
|
||||||
Enforce SSO
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
|
||||||
</Label>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-sm">
|
|
||||||
<p>
|
|
||||||
When enabled, the email/password login form is hidden and users
|
|
||||||
must sign in exclusively through SSO.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { HelpCircle } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
export const ToggleRemoteServersOnly = () => {
|
|
||||||
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
|
|
||||||
|
|
||||||
const { mutateAsync } = api.settings.updateRemoteServersOnly.useMutation();
|
|
||||||
|
|
||||||
const handleToggle = async (checked: boolean) => {
|
|
||||||
try {
|
|
||||||
await mutateAsync({ remoteServersOnly: checked });
|
|
||||||
await refetch();
|
|
||||||
toast.success("Remote Servers Only updated");
|
|
||||||
} catch {
|
|
||||||
toast.error("Error updating Remote Servers Only");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Switch
|
|
||||||
checked={!!data?.remoteServersOnly}
|
|
||||||
onCheckedChange={handleToggle}
|
|
||||||
/>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Label className="text-primary flex items-center gap-1.5 cursor-pointer">
|
|
||||||
Remote Servers Only
|
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
|
||||||
</Label>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-sm">
|
|
||||||
<p>
|
|
||||||
When enabled, all services (applications, databases, compose) must
|
|
||||||
be deployed to a remote server. Deploying directly to the Dokploy
|
|
||||||
host VM is not allowed.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -131,10 +131,10 @@ export const ShowServers = () => {
|
|||||||
className="relative hover:shadow-lg transition-shadow flex flex-col bg-transparent"
|
className="relative hover:shadow-lg transition-shadow flex flex-col bg-transparent"
|
||||||
>
|
>
|
||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ServerIcon className="size-5 shrink-0 text-muted-foreground" />
|
<ServerIcon className="size-5 text-muted-foreground" />
|
||||||
<CardTitle className="text-lg break-words min-w-0">
|
<CardTitle className="text-lg">
|
||||||
{server.name}
|
{server.name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +145,7 @@ export const ShowServers = () => {
|
|||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8 shrink-0 p-0"
|
className="h-8 w-8 p-0"
|
||||||
>
|
>
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
More options
|
More options
|
||||||
|
|||||||
@@ -55,8 +55,7 @@ export const WelcomeSubscription = () => {
|
|||||||
const [showConfetti, setShowConfetti] = useState(false);
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
const stepper = useStepper();
|
const stepper = useStepper();
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
const router = useRouter();
|
const { push } = useRouter();
|
||||||
const { push } = router;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const confettiShown = localStorage.getItem("hasShownConfetti");
|
const confettiShown = localStorage.getItem("hasShownConfetti");
|
||||||
@@ -67,22 +66,7 @@ export const WelcomeSubscription = () => {
|
|||||||
}, [showConfetti]);
|
}, [showConfetti]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={isOpen}>
|
||||||
open={isOpen}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setIsOpen(open);
|
|
||||||
if (!open) {
|
|
||||||
const { success, ...rest } = router.query;
|
|
||||||
router.replace(
|
|
||||||
{ pathname: router.pathname, query: rest },
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
shallow: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent className="sm:max-w-7xl min-h-[75vh]">
|
<DialogContent className="sm:max-w-7xl min-h-[75vh]">
|
||||||
{showConfetti ?? "Flaso"}
|
{showConfetti ?? "Flaso"}
|
||||||
<div className="flex justify-center items-center w-full">
|
<div className="flex justify-center items-center w-full">
|
||||||
@@ -425,7 +409,7 @@ export const WelcomeSubscription = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (stepper.isLast) {
|
if (stepper.isLast) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
push("/dashboard/home");
|
push("/dashboard/projects");
|
||||||
} else {
|
} else {
|
||||||
stepper.next();
|
stepper.next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
Forward,
|
Forward,
|
||||||
GalleryVerticalEnd,
|
GalleryVerticalEnd,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
House,
|
|
||||||
Key,
|
Key,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -149,12 +148,6 @@ type Menu = {
|
|||||||
// The `isEnabled` function is called to determine if the item should be displayed
|
// The `isEnabled` function is called to determine if the item should be displayed
|
||||||
const MENU: Menu = {
|
const MENU: Menu = {
|
||||||
home: [
|
home: [
|
||||||
{
|
|
||||||
isSingle: true,
|
|
||||||
title: "Home",
|
|
||||||
url: "/dashboard/home",
|
|
||||||
icon: House,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
@@ -868,19 +861,6 @@ function SidebarLogo() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MobileCloser() {
|
|
||||||
const pathname = usePathname();
|
|
||||||
const { setOpenMobile, isMobile } = useSidebar();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isMobile) {
|
|
||||||
setOpenMobile(false);
|
|
||||||
}
|
|
||||||
}, [pathname, isMobile, setOpenMobile]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page({ children }: Props) {
|
export default function Page({ children }: Props) {
|
||||||
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
|
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
@@ -946,7 +926,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
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const UserNav = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push("/dashboard/home");
|
router.push("/dashboard/projects");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Projects
|
Projects
|
||||||
|
|||||||
@@ -29,15 +29,10 @@ type SSOEmailForm = z.infer<typeof ssoEmailSchema>;
|
|||||||
|
|
||||||
interface SignInWithSSOProps {
|
interface SignInWithSSOProps {
|
||||||
/** Content shown when SSO is collapsed (e.g. email/password form) */
|
/** Content shown when SSO is collapsed (e.g. email/password form) */
|
||||||
children?: React.ReactNode;
|
children: React.ReactNode;
|
||||||
/** When true, SSO is the only option — no fallback to email/password */
|
|
||||||
enforce?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SignInWithSSO({
|
export function SignInWithSSO({ children }: SignInWithSSOProps) {
|
||||||
children,
|
|
||||||
enforce = false,
|
|
||||||
}: SignInWithSSOProps) {
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const form = useForm<SSOEmailForm>({
|
const form = useForm<SSOEmailForm>({
|
||||||
@@ -49,7 +44,7 @@ export function SignInWithSSO({
|
|||||||
try {
|
try {
|
||||||
const { data, error } = await authClient.signIn.sso({
|
const { data, error } = await authClient.signIn.sso({
|
||||||
email: values.email,
|
email: values.email,
|
||||||
callbackURL: "/dashboard/home",
|
callbackURL: "/dashboard/projects",
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(error.message ?? "Failed to sign in with SSO");
|
toast.error(error.message ?? "Failed to sign in with SSO");
|
||||||
@@ -77,7 +72,7 @@ export function SignInWithSSO({
|
|||||||
<LogIn className="mr-2 size-4" />
|
<LogIn className="mr-2 size-4" />
|
||||||
Sign in with SSO
|
Sign in with SSO
|
||||||
</Button>
|
</Button>
|
||||||
{!enforce && children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -118,15 +113,13 @@ export function SignInWithSSO({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{!enforce && (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setExpanded(false)}
|
||||||
onClick={() => setExpanded(false)}
|
className="text-xs text-muted-foreground hover:underline"
|
||||||
className="text-xs text-muted-foreground hover:underline"
|
>
|
||||||
>
|
Use email and password instead
|
||||||
Use email and password instead
|
</button>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -116,14 +116,6 @@ export function TagSelector({
|
|||||||
<HandleTag />
|
<HandleTag />
|
||||||
</div>
|
</div>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
{tags.length === 0 && (
|
|
||||||
<div className="flex flex-col items-center gap-2 py-4">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
No tags created yet.
|
|
||||||
</span>
|
|
||||||
<HandleTag />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{tags.map((tag) => {
|
{tags.map((tag) => {
|
||||||
const isSelected = selectedTags.includes(tag.id);
|
const isSelected = selectedTags.includes(tag.id);
|
||||||
|
|||||||
@@ -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,198 +0,0 @@
|
|||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
|
||||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const ContextMenu = ContextMenuPrimitive.Root;
|
|
||||||
|
|
||||||
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
|
||||||
|
|
||||||
const ContextMenuGroup = ContextMenuPrimitive.Group;
|
|
||||||
|
|
||||||
const ContextMenuPortal = ContextMenuPrimitive.Portal;
|
|
||||||
|
|
||||||
const ContextMenuSub = ContextMenuPrimitive.Sub;
|
|
||||||
|
|
||||||
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
|
||||||
|
|
||||||
const ContextMenuSubTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
|
||||||
));
|
|
||||||
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
|
|
||||||
|
|
||||||
const ContextMenuSubContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
|
|
||||||
|
|
||||||
const ContextMenuContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
));
|
|
||||||
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
|
|
||||||
|
|
||||||
const ContextMenuItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
|
|
||||||
|
|
||||||
const ContextMenuCheckboxItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
|
||||||
));
|
|
||||||
ContextMenuCheckboxItem.displayName =
|
|
||||||
ContextMenuPrimitive.CheckboxItem.displayName;
|
|
||||||
|
|
||||||
const ContextMenuRadioItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.RadioItem>
|
|
||||||
));
|
|
||||||
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
|
|
||||||
|
|
||||||
const ContextMenuLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
|
|
||||||
|
|
||||||
const ContextMenuSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
|
|
||||||
|
|
||||||
const ContextMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
ContextMenuShortcut.displayName = "ContextMenuShortcut";
|
|
||||||
|
|
||||||
export {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuCheckboxItem,
|
|
||||||
ContextMenuRadioItem,
|
|
||||||
ContextMenuLabel,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuShortcut,
|
|
||||||
ContextMenuGroup,
|
|
||||||
ContextMenuPortal,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
ContextMenuRadioGroup,
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "user" ADD COLUMN "sendInvoiceNotifications" boolean DEFAULT false NOT NULL;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "schedule" ADD COLUMN "description" text;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "webServerSettings" ADD COLUMN "remoteServersOnly" boolean DEFAULT false NOT NULL;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "webServerSettings" ADD COLUMN "enforceSSO" boolean DEFAULT false NOT NULL;
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1156,34 +1156,6 @@
|
|||||||
"when": 1775369858244,
|
"when": 1775369858244,
|
||||||
"tag": "0164_slippery_sasquatch",
|
"tag": "0164_slippery_sasquatch",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 165,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1775845419261,
|
|
||||||
"tag": "0165_abnormal_greymalkin",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 166,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1778303519111,
|
|
||||||
"tag": "0166_nosy_slapstick",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 167,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1780122576214,
|
|
||||||
"tag": "0167_fresh_goliath",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 168,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1780122833339,
|
|
||||||
"tag": "0168_long_justice",
|
|
||||||
"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.6",
|
"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",
|
||||||
@@ -68,7 +67,6 @@
|
|||||||
"@radix-ui/react-avatar": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-collapsible": "^1.1.11",
|
"@radix-ui/react-collapsible": "^1.1.11",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
@@ -123,11 +121,11 @@
|
|||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
"nanoid": "3.3.11",
|
"nanoid": "3.3.11",
|
||||||
"next": "16.2.6",
|
"next": "^16.2.0",
|
||||||
"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 +146,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",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
|
|
||||||
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/home"
|
href="/dashboard/projects"
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
className: "flex flex-row gap-2",
|
className: "flex flex-row gap-2",
|
||||||
|
|||||||
@@ -12,15 +12,6 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
|||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { 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(
|
||||||
@@ -54,9 +53,14 @@ export default async function handler(
|
|||||||
|
|
||||||
if (sourceType === "github") {
|
if (sourceType === "github") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits =
|
||||||
(commit: any) => commit.modified,
|
req.body?.commits
|
||||||
);
|
?.flatMap((commit: any) => [
|
||||||
|
...(commit.modified || []),
|
||||||
|
...(commit.added || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
])
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
@@ -74,9 +78,14 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
} else if (sourceType === "gitlab") {
|
} else if (sourceType === "gitlab") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits =
|
||||||
(commit: any) => commit.modified,
|
req.body?.commits
|
||||||
);
|
?.flatMap((commit: any) => [
|
||||||
|
...(commit.modified || []),
|
||||||
|
...(commit.added || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
])
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
@@ -125,17 +134,32 @@ export default async function handler(
|
|||||||
let normalizedCommits: string[] = [];
|
let normalizedCommits: string[] = [];
|
||||||
|
|
||||||
if (provider === "github") {
|
if (provider === "github") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits =
|
||||||
(commit: any) => commit.modified,
|
req.body?.commits
|
||||||
);
|
?.flatMap((commit: any) => [
|
||||||
|
...(commit.modified || []),
|
||||||
|
...(commit.added || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
])
|
||||||
|
.filter(Boolean) || [];
|
||||||
} else if (provider === "gitlab") {
|
} else if (provider === "gitlab") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits =
|
||||||
(commit: any) => commit.modified,
|
req.body?.commits
|
||||||
);
|
?.flatMap((commit: any) => [
|
||||||
|
...(commit.modified || []),
|
||||||
|
...(commit.added || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
])
|
||||||
|
.filter(Boolean) || [];
|
||||||
} else if (provider === "gitea") {
|
} else if (provider === "gitea") {
|
||||||
normalizedCommits = req.body?.commits?.flatMap(
|
normalizedCommits =
|
||||||
(commit: any) => commit.modified,
|
req.body?.commits
|
||||||
);
|
?.flatMap((commit: any) => [
|
||||||
|
...(commit.modified || []),
|
||||||
|
...(commit.added || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
])
|
||||||
|
.filter(Boolean) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
@@ -150,9 +174,14 @@ export default async function handler(
|
|||||||
} else if (sourceType === "gitea") {
|
} else if (sourceType === "gitea") {
|
||||||
const branchName = extractBranchName(req.headers, req.body);
|
const branchName = extractBranchName(req.headers, req.body);
|
||||||
|
|
||||||
const normalizedCommits = req.body?.commits?.flatMap(
|
const normalizedCommits =
|
||||||
(commit: any) => commit.modified,
|
req.body?.commits
|
||||||
);
|
?.flatMap((commit: any) => [
|
||||||
|
...(commit.modified || []),
|
||||||
|
...(commit.added || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
])
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
|
||||||
const shouldDeployPaths = shouldDeploy(
|
const shouldDeployPaths = shouldDeploy(
|
||||||
composeResult.watchPaths,
|
composeResult.watchPaths,
|
||||||
@@ -196,14 +225,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,9 +213,14 @@ export default async function handler(
|
|||||||
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
const deploymentTitle = extractCommitMessage(req.headers, req.body);
|
||||||
const deploymentHash = extractHash(req.headers, req.body);
|
const deploymentHash = extractHash(req.headers, req.body);
|
||||||
const owner = githubBody?.repository?.owner?.name;
|
const owner = githubBody?.repository?.owner?.name;
|
||||||
const normalizedCommits = githubBody?.commits?.flatMap(
|
const normalizedCommits =
|
||||||
(commit: any) => commit.modified,
|
githubBody?.commits
|
||||||
);
|
?.flatMap((commit: any) => [
|
||||||
|
...(commit.modified || []),
|
||||||
|
...(commit.added || []),
|
||||||
|
...(commit.removed || []),
|
||||||
|
])
|
||||||
|
.filter(Boolean) || [];
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
@@ -329,8 +327,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;
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ import { and, asc, eq } from "drizzle-orm";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { organization, server, user } from "@/server/db/schema";
|
import { organization, server, user } from "@/server/db/schema";
|
||||||
import {
|
|
||||||
sendInvoiceEmail,
|
|
||||||
sendPaymentFailedEmail,
|
|
||||||
} from "@/server/utils/stripe-notifications";
|
|
||||||
|
|
||||||
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||||
|
|
||||||
@@ -245,11 +241,6 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
const newServersQuantity = admin.serversQuantity;
|
const newServersQuantity = admin.serversQuantity;
|
||||||
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
await updateServersBasedOnQuantity(admin.id, newServersQuantity);
|
||||||
|
|
||||||
if (admin.sendInvoiceNotifications) {
|
|
||||||
await sendInvoiceEmail(newInvoice, admin);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "invoice.payment_failed": {
|
case "invoice.payment_failed": {
|
||||||
@@ -258,6 +249,7 @@ export default async function handler(
|
|||||||
const subscription = await stripe.subscriptions.retrieve(
|
const subscription = await stripe.subscriptions.retrieve(
|
||||||
newInvoice.subscription as string,
|
newInvoice.subscription as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (subscription.status !== "active") {
|
if (subscription.status !== "active") {
|
||||||
const admin = await findUserByStripeCustomerId(
|
const admin = await findUserByStripeCustomerId(
|
||||||
newInvoice.customer as string,
|
newInvoice.customer as string,
|
||||||
@@ -271,10 +263,6 @@ export default async function handler(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (admin.sendInvoiceNotifications) {
|
|
||||||
await sendPaymentFailedEmail(newInvoice, admin);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function DeploymentsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[45vh]">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
|
||||||
<div className="rounded-xl bg-background shadow-md h-full">
|
<div className="rounded-xl bg-background shadow-md h-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
@@ -84,7 +84,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -102,7 +102,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
import superjson from "superjson";
|
|
||||||
import { ShowHome } from "@/components/dashboard/home/show-home";
|
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
|
||||||
|
|
||||||
const Home = () => {
|
|
||||||
return <ShowHome />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Home;
|
|
||||||
|
|
||||||
Home.getLayout = (page: ReactElement) => {
|
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|
||||||
const { req, res } = ctx;
|
|
||||||
const { user, session } = await validateRequest(req);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: false,
|
|
||||||
destination: "/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const helpers = createServerSideHelpers({
|
|
||||||
router: appRouter,
|
|
||||||
ctx: {
|
|
||||||
req: req as any,
|
|
||||||
res: res as any,
|
|
||||||
db: null as any,
|
|
||||||
session: session as any,
|
|
||||||
user: user as any,
|
|
||||||
},
|
|
||||||
transformer: superjson,
|
|
||||||
});
|
|
||||||
|
|
||||||
await helpers.settings.isCloud.prefetch();
|
|
||||||
await helpers.user.get.prefetch();
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: helpers.dehydrate(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -95,8 +95,8 @@ export async function getServerSideProps(
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -122,7 +122,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
Play,
|
Play,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RefreshCw,
|
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
@@ -32,7 +31,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";
|
||||||
@@ -70,14 +68,6 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuLabel,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
} from "@/components/ui/context-menu";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -434,7 +424,6 @@ const EnvironmentPage = (
|
|||||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||||
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
||||||
const [selectedServerId, setSelectedServerId] = useState<string>("all");
|
const [selectedServerId, setSelectedServerId] = useState<string>("all");
|
||||||
const [serviceToDelete, setServiceToDelete] = useState<Services | null>(null);
|
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
if (selectedServices.length === filteredServices.length) {
|
if (selectedServices.length === filteredServices.length) {
|
||||||
@@ -510,14 +499,6 @@ const EnvironmentPage = (
|
|||||||
deploy: api.mongo.deploy.useMutation(),
|
deploy: api.mongo.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const libsqlActions = {
|
|
||||||
start: api.libsql.start.useMutation(),
|
|
||||||
stop: api.libsql.stop.useMutation(),
|
|
||||||
move: api.libsql.move.useMutation(),
|
|
||||||
delete: api.libsql.remove.useMutation(),
|
|
||||||
deploy: api.libsql.deploy.useMutation(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBulkStart = async () => {
|
const handleBulkStart = async () => {
|
||||||
let success = 0;
|
let success = 0;
|
||||||
setIsBulkActionLoading(true);
|
setIsBulkActionLoading(true);
|
||||||
@@ -550,9 +531,6 @@ const EnvironmentPage = (
|
|||||||
case "mongo":
|
case "mongo":
|
||||||
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
||||||
break;
|
break;
|
||||||
case "libsql":
|
|
||||||
await libsqlActions.start.mutateAsync({ libsqlId: serviceId });
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -600,9 +578,6 @@ const EnvironmentPage = (
|
|||||||
case "mongo":
|
case "mongo":
|
||||||
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
||||||
break;
|
break;
|
||||||
case "libsql":
|
|
||||||
await libsqlActions.stop.mutateAsync({ libsqlId: serviceId });
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -679,12 +654,6 @@ const EnvironmentPage = (
|
|||||||
targetEnvironmentId: selectedTargetEnvironment,
|
targetEnvironmentId: selectedTargetEnvironment,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "libsql":
|
|
||||||
await libsqlActions.move.mutateAsync({
|
|
||||||
libsqlId: serviceId,
|
|
||||||
targetEnvironmentId: selectedTargetEnvironment,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
await utils.environment.one.invalidate({
|
await utils.environment.one.invalidate({
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -754,11 +723,6 @@ const EnvironmentPage = (
|
|||||||
mongoId: serviceId,
|
mongoId: serviceId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "libsql":
|
|
||||||
await libsqlActions.delete.mutateAsync({
|
|
||||||
libsqlId: serviceId,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
await utils.environment.one.invalidate({
|
await utils.environment.one.invalidate({
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -825,11 +789,6 @@ const EnvironmentPage = (
|
|||||||
mongoId: serviceId,
|
mongoId: serviceId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "libsql":
|
|
||||||
await libsqlActions.deploy.mutateAsync({
|
|
||||||
libsqlId: serviceId,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -855,110 +814,6 @@ const EnvironmentPage = (
|
|||||||
setIsBulkActionLoading(false);
|
setIsBulkActionLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getServiceActions = (service: Services) => {
|
|
||||||
switch (service.type) {
|
|
||||||
case "application":
|
|
||||||
return applicationActions;
|
|
||||||
case "compose":
|
|
||||||
return composeActions;
|
|
||||||
case "postgres":
|
|
||||||
return postgresActions;
|
|
||||||
case "mysql":
|
|
||||||
return mysqlActions;
|
|
||||||
case "mariadb":
|
|
||||||
return mariadbActions;
|
|
||||||
case "redis":
|
|
||||||
return redisActions;
|
|
||||||
case "mongo":
|
|
||||||
return mongoActions;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServiceIdKey = (service: Services) => {
|
|
||||||
switch (service.type) {
|
|
||||||
case "application":
|
|
||||||
return "applicationId";
|
|
||||||
case "compose":
|
|
||||||
return "composeId";
|
|
||||||
case "postgres":
|
|
||||||
return "postgresId";
|
|
||||||
case "mysql":
|
|
||||||
return "mysqlId";
|
|
||||||
case "mariadb":
|
|
||||||
return "mariadbId";
|
|
||||||
case "redis":
|
|
||||||
return "redisId";
|
|
||||||
case "mongo":
|
|
||||||
return "mongoId";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleServiceAction = async (
|
|
||||||
service: Services,
|
|
||||||
action: "start" | "stop" | "deploy",
|
|
||||||
) => {
|
|
||||||
const actions = getServiceActions(service);
|
|
||||||
const idKey = getServiceIdKey(service);
|
|
||||||
if (!actions || !idKey) return;
|
|
||||||
|
|
||||||
const actionLabels = {
|
|
||||||
start: { loading: "Starting", success: "started", error: "starting" },
|
|
||||||
stop: { loading: "Stopping", success: "stopped", error: "stopping" },
|
|
||||||
deploy: {
|
|
||||||
loading: "Deploying",
|
|
||||||
success: "queued for deployment",
|
|
||||||
error: "deploying",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const labels = actionLabels[action];
|
|
||||||
|
|
||||||
toast.promise(
|
|
||||||
(async () => {
|
|
||||||
await actions[action].mutateAsync({
|
|
||||||
[idKey]: service.id,
|
|
||||||
} as any);
|
|
||||||
})(),
|
|
||||||
{
|
|
||||||
loading: `${labels.loading} ${service.name}...`,
|
|
||||||
success: () => {
|
|
||||||
utils.environment.one.invalidate({ environmentId });
|
|
||||||
return `${service.name} ${labels.success} successfully`;
|
|
||||||
},
|
|
||||||
error: (error) =>
|
|
||||||
`Error ${labels.error} ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleServiceDelete = async (service: Services) => {
|
|
||||||
const actions = getServiceActions(service);
|
|
||||||
const idKey = getServiceIdKey(service);
|
|
||||||
if (!actions || !idKey) return;
|
|
||||||
|
|
||||||
toast.promise(
|
|
||||||
(async () => {
|
|
||||||
await actions.delete.mutateAsync({
|
|
||||||
[idKey]: service.id,
|
|
||||||
} as any);
|
|
||||||
})(),
|
|
||||||
{
|
|
||||||
loading: `Deleting ${service.name}...`,
|
|
||||||
success: () => {
|
|
||||||
utils.environment.one.invalidate({ environmentId });
|
|
||||||
return `${service.name} deleted successfully`;
|
|
||||||
},
|
|
||||||
error: (error) =>
|
|
||||||
`Error deleting ${service.name}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setServiceToDelete(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get unique servers from services
|
// Get unique servers from services
|
||||||
const availableServers = useMemo(() => {
|
const availableServers = useMemo(() => {
|
||||||
if (!applications) return [];
|
if (!applications) return [];
|
||||||
@@ -1092,10 +947,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 +955,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
|
||||||
@@ -1621,156 +1472,110 @@ const EnvironmentPage = (
|
|||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
<div className="gap-5 pb-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
<div className="gap-5 pb-10 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{filteredServices?.map((service) => (
|
{filteredServices?.map((service) => (
|
||||||
<ContextMenu key={service.id}>
|
<Link
|
||||||
<ContextMenuTrigger asChild>
|
key={service.id}
|
||||||
<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"
|
||||||
className="block h-full"
|
>
|
||||||
|
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
|
||||||
|
{service.serverId && (
|
||||||
|
<div className="absolute -left-1 -top-2">
|
||||||
|
<ServerIcon className="size-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="absolute -right-1 -top-2">
|
||||||
|
<StatusTooltip status={service.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
|
||||||
|
selectedServices.includes(service.id)
|
||||||
|
? "opacity-100 translate-y-0"
|
||||||
|
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
|
||||||
|
)}
|
||||||
|
onClick={(e) =>
|
||||||
|
handleServiceSelect(service.id, e)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Card className="flex flex-col h-full group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
|
<div className="h-full w-full flex items-center justify-center">
|
||||||
{service.serverId && (
|
<Checkbox
|
||||||
<div className="absolute -left-1 -top-2">
|
checked={selectedServices.includes(
|
||||||
<ServerIcon className="size-4 text-muted-foreground" />
|
service.id,
|
||||||
|
)}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center gap-2 justify-between w-full">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-base flex items-center gap-2 font-medium leading-none flex-wrap">
|
||||||
|
{service.name}
|
||||||
|
</span>
|
||||||
|
{service.description && (
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
{service.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="text-sm font-medium text-muted-foreground self-start">
|
||||||
|
{service.type === "postgres" && (
|
||||||
|
<PostgresqlIcon className="h-7 w-7" />
|
||||||
|
)}
|
||||||
|
{service.type === "redis" && (
|
||||||
|
<RedisIcon className="h-7 w-7" />
|
||||||
|
)}
|
||||||
|
{service.type === "mariadb" && (
|
||||||
|
<MariadbIcon className="h-7 w-7" />
|
||||||
|
)}
|
||||||
|
{service.type === "mongo" && (
|
||||||
|
<MongodbIcon className="h-7 w-7" />
|
||||||
|
)}
|
||||||
|
{service.type === "mysql" && (
|
||||||
|
<MysqlIcon className="h-7 w-7" />
|
||||||
|
)}
|
||||||
|
{service.type === "application" &&
|
||||||
|
(service.icon ? (
|
||||||
|
// biome-ignore lint/performance/noImgElement: application icon is data URL
|
||||||
|
<img
|
||||||
|
src={service.icon}
|
||||||
|
alt={service.name}
|
||||||
|
className="size-7 object-contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GlobeIcon className="h-6 w-6" />
|
||||||
|
))}
|
||||||
|
{service.type === "compose" && (
|
||||||
|
<CircuitBoard className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
{service.type === "libsql" && (
|
||||||
|
<LibsqlIcon className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="mt-auto">
|
||||||
|
<div className="space-y-1 text-sm w-full">
|
||||||
|
{service.serverName && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
||||||
|
<ServerIcon className="size-3" />
|
||||||
|
<span className="truncate">
|
||||||
|
{service.serverName}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute -right-1 -top-2">
|
<DateTooltip date={service.createdAt}>
|
||||||
<StatusTooltip status={service.status} />
|
Created
|
||||||
</div>
|
</DateTooltip>
|
||||||
|
</div>
|
||||||
<div
|
</CardFooter>
|
||||||
className={cn(
|
</Card>
|
||||||
"absolute -left-3 -bottom-3 size-9 translate-y-1 rounded-full p-0 transition-all duration-200 z-10 bg-background border",
|
</Link>
|
||||||
selectedServices.includes(service.id)
|
|
||||||
? "opacity-100 translate-y-0"
|
|
||||||
: "opacity-0 group-hover:translate-y-0 group-hover:opacity-100",
|
|
||||||
)}
|
|
||||||
onClick={(e) =>
|
|
||||||
handleServiceSelect(service.id, e)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedServices.includes(
|
|
||||||
service.id,
|
|
||||||
)}
|
|
||||||
className="data-[state=checked]:bg-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center justify-between">
|
|
||||||
<div className="flex flex-row items-center gap-2 justify-between w-full">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="text-base flex items-center gap-2 font-medium leading-none flex-wrap">
|
|
||||||
{service.name}
|
|
||||||
</span>
|
|
||||||
{service.description && (
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
{service.description}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className="text-sm font-medium text-muted-foreground self-start">
|
|
||||||
{service.type === "postgres" && (
|
|
||||||
<PostgresqlIcon className="h-7 w-7" />
|
|
||||||
)}
|
|
||||||
{service.type === "redis" && (
|
|
||||||
<RedisIcon className="h-7 w-7" />
|
|
||||||
)}
|
|
||||||
{service.type === "mariadb" && (
|
|
||||||
<MariadbIcon className="h-7 w-7" />
|
|
||||||
)}
|
|
||||||
{service.type === "mongo" && (
|
|
||||||
<MongodbIcon className="h-7 w-7" />
|
|
||||||
)}
|
|
||||||
{service.type === "mysql" && (
|
|
||||||
<MysqlIcon className="h-7 w-7" />
|
|
||||||
)}
|
|
||||||
{service.type === "application" &&
|
|
||||||
(service.icon ? (
|
|
||||||
// biome-ignore lint/performance/noImgElement: application icon is data URL
|
|
||||||
<img
|
|
||||||
src={service.icon}
|
|
||||||
alt={service.name}
|
|
||||||
className="size-7 object-contain"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<GlobeIcon className="h-6 w-6" />
|
|
||||||
))}
|
|
||||||
{service.type === "compose" && (
|
|
||||||
<CircuitBoard className="h-6 w-6" />
|
|
||||||
)}
|
|
||||||
{service.type === "libsql" && (
|
|
||||||
<LibsqlIcon className="h-6 w-6" />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter className="mt-auto">
|
|
||||||
<div className="space-y-1 text-sm w-full">
|
|
||||||
{service.serverName && (
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
|
||||||
<ServerIcon className="size-3" />
|
|
||||||
<span className="truncate">
|
|
||||||
{service.serverName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<DateTooltip date={service.createdAt}>
|
|
||||||
Created
|
|
||||||
</DateTooltip>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</Link>
|
|
||||||
</ContextMenuTrigger>
|
|
||||||
{service.type !== "libsql" && (
|
|
||||||
<ContextMenuContent className="w-48">
|
|
||||||
<ContextMenuLabel className="truncate">
|
|
||||||
{service.name}
|
|
||||||
</ContextMenuLabel>
|
|
||||||
<ContextMenuSeparator />
|
|
||||||
<ContextMenuItem
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() =>
|
|
||||||
handleServiceAction(service, "start")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Play className="size-4" />
|
|
||||||
Start
|
|
||||||
</ContextMenuItem>
|
|
||||||
<ContextMenuItem
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() =>
|
|
||||||
handleServiceAction(service, "deploy")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<RefreshCw className="size-4" />
|
|
||||||
Deploy
|
|
||||||
</ContextMenuItem>
|
|
||||||
<ContextMenuItem
|
|
||||||
className="flex items-center gap-2 text-orange-500 focus:text-orange-500"
|
|
||||||
onClick={() =>
|
|
||||||
handleServiceAction(service, "stop")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Ban className="size-4" />
|
|
||||||
Stop
|
|
||||||
</ContextMenuItem>
|
|
||||||
<ContextMenuSeparator />
|
|
||||||
<ContextMenuItem
|
|
||||||
className="flex items-center gap-2 text-red-500 focus:text-red-500"
|
|
||||||
onClick={() => setServiceToDelete(service)}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4" />
|
|
||||||
Delete
|
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuContent>
|
|
||||||
)}
|
|
||||||
</ContextMenu>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1781,38 +1586,6 @@ const EnvironmentPage = (
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Single Service Delete Dialog */}
|
|
||||||
<Dialog
|
|
||||||
open={!!serviceToDelete}
|
|
||||||
onOpenChange={(open) => !open && setServiceToDelete(null)}
|
|
||||||
>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Delete Service</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Are you sure you want to delete{" "}
|
|
||||||
<span className="font-semibold">{serviceToDelete?.name}</span>?
|
|
||||||
This action cannot be undone.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setServiceToDelete(null)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => {
|
|
||||||
if (serviceToDelete) {
|
|
||||||
handleServiceDelete(serviceToDelete);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1832,7 +1605,7 @@ export async function getServerSideProps(
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1891,7 +1664,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -492,7 +490,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/home",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user