mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-17 21:25:23 +02:00
Compare commits
9 Commits
v0.25.3
...
feat/intro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
847cf6118b | ||
|
|
275d7ee586 | ||
|
|
29aae91959 | ||
|
|
ab6cb7349e | ||
|
|
e36c665e02 | ||
|
|
f3dc480b70 | ||
|
|
6758ef1542 | ||
|
|
bd4ff2dbf2 | ||
|
|
579a2262bf |
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile1 = `
|
||||
version: "3.8"
|
||||
@@ -61,7 +61,7 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile1 = parse(`
|
||||
const expectedComposeFile1 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -120,7 +120,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in compose file 1", () => {
|
||||
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
@@ -185,7 +185,7 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = parse(`
|
||||
const expectedComposeFile2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -243,7 +243,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in compose file 2", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
@@ -308,7 +308,7 @@ secrets:
|
||||
file: ./service_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = parse(`
|
||||
const expectedComposeFile3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -366,7 +366,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in compose file 3", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
@@ -420,7 +420,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFile = parse(`
|
||||
const expectedComposeFile = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -467,7 +467,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all properties in Plausible compose file", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -23,7 +23,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in root property", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -59,7 +59,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to multiple configs in root property", () => {
|
||||
const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification;
|
||||
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -92,7 +92,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs with different properties in root property", () => {
|
||||
const composeData = parse(
|
||||
const composeData = load(
|
||||
composeFileDifferentProperties,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -137,7 +137,7 @@ configs:
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFileConfigRoot = parse(`
|
||||
const expectedComposeFileConfigRoot = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -162,7 +162,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs in root property", () => {
|
||||
const composeData = parse(composeFileConfigRoot) as ComposeSpecification;
|
||||
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
addSuffixToConfigsInServices,
|
||||
generateRandomHash,
|
||||
} from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -22,7 +22,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -54,7 +54,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services with single config", () => {
|
||||
const composeData = parse(
|
||||
const composeData = load(
|
||||
composeFileSingleServiceConfig,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -108,7 +108,7 @@ configs:
|
||||
`;
|
||||
|
||||
test("Add suffix to configs in services with multiple configs", () => {
|
||||
const composeData = parse(
|
||||
const composeData = load(
|
||||
composeFileMultipleServicesConfigs,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -157,7 +157,7 @@ services:
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFileConfigServices = parse(`
|
||||
const expectedComposeFileConfigServices = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -182,7 +182,7 @@ services:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs in services", () => {
|
||||
const composeData = parse(composeFileConfigServices) as ComposeSpecification;
|
||||
const composeData = load(composeFileConfigServices) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -43,7 +43,7 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedConfigs = parse(`
|
||||
const expectedComposeFileCombinedConfigs = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -77,7 +77,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all configs in root and services", () => {
|
||||
const composeData = parse(composeFileCombinedConfigs) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -122,7 +122,7 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileWithEnvAndExternal = parse(`
|
||||
const expectedComposeFileWithEnvAndExternal = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -159,7 +159,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs with environment and external", () => {
|
||||
const composeData = parse(
|
||||
const composeData = load(
|
||||
composeFileWithEnvAndExternal,
|
||||
) as ComposeSpecification;
|
||||
|
||||
@@ -200,7 +200,7 @@ configs:
|
||||
file: ./db-config.yml
|
||||
`;
|
||||
|
||||
const expectedComposeFileWithTemplateDriverAndLabels = parse(`
|
||||
const expectedComposeFileWithTemplateDriverAndLabels = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -231,7 +231,7 @@ configs:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to configs with template driver and labels", () => {
|
||||
const composeData = parse(
|
||||
const composeData = load(
|
||||
composeFileWithTemplateDriverAndLabels,
|
||||
) as ComposeSpecification;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -35,7 +35,7 @@ test("Generate random hash with 8 characters", () => {
|
||||
});
|
||||
|
||||
test("Add suffix to networks root property", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -79,7 +79,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to advanced networks root property (2 TRY)", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -120,7 +120,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with external properties", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -160,7 +160,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with IPAM configurations", () => {
|
||||
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -201,7 +201,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with custom options", () => {
|
||||
const composeData = parse(composeFile5) as ComposeSpecification;
|
||||
const composeData = load(composeFile5) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -264,7 +264,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks with static suffix", () => {
|
||||
const composeData = parse(composeFile6) as ComposeSpecification;
|
||||
const composeData = load(composeFile6) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -273,7 +273,7 @@ test("Add suffix to networks with static suffix", () => {
|
||||
}
|
||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||
|
||||
const expectedComposeData = parse(
|
||||
const expectedComposeData = load(
|
||||
expectedComposeFile6,
|
||||
) as ComposeSpecification;
|
||||
expect(networks).toStrictEqual(expectedComposeData.networks);
|
||||
@@ -293,7 +293,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network", () => {
|
||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
addSuffixToServiceNetworks,
|
||||
generateRandomHash,
|
||||
} from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -23,7 +23,7 @@ services:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -67,7 +67,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services with aliases", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -107,7 +107,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services (Object with simple networks)", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -153,7 +153,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services (combined case)", () => {
|
||||
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -196,7 +196,7 @@ services:
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network in services", () => {
|
||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -245,7 +245,7 @@ services:
|
||||
`;
|
||||
|
||||
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
||||
const composeData = parse(composeFile8) as ComposeSpecification;
|
||||
const composeData = load(composeFile8) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
addSuffixToServiceNetworks,
|
||||
generateRandomHash,
|
||||
} from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFileCombined = `
|
||||
version: "3.8"
|
||||
@@ -39,7 +39,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to networks in services and root (combined case)", () => {
|
||||
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => {
|
||||
expect(redisNetworks).not.toHaveProperty("backend");
|
||||
});
|
||||
|
||||
const expectedComposeFile = parse(`
|
||||
const expectedComposeFile = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -120,7 +120,7 @@ networks:
|
||||
`);
|
||||
|
||||
test("Add suffix to networks in compose file", () => {
|
||||
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
if (!composeData?.networks) {
|
||||
@@ -156,7 +156,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = parse(`
|
||||
const expectedComposeFile2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -182,7 +182,7 @@ networks:
|
||||
`);
|
||||
|
||||
test("Add suffix to networks in compose file with external and internal networks", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
@@ -218,7 +218,7 @@ networks:
|
||||
com.docker.network.bridge.enable_icc: "true"
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = parse(`
|
||||
const expectedComposeFile3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -247,7 +247,7 @@ networks:
|
||||
`);
|
||||
|
||||
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
@@ -289,7 +289,7 @@ networks:
|
||||
|
||||
`;
|
||||
|
||||
const expectedComposeFile4 = parse(`
|
||||
const expectedComposeFile4 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -326,7 +326,7 @@ networks:
|
||||
`);
|
||||
|
||||
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
|
||||
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -23,7 +23,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in root property", () => {
|
||||
const composeData = parse(composeFileSecretsRoot) as ComposeSpecification;
|
||||
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
@@ -52,7 +52,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in root property (Test 1)", () => {
|
||||
const composeData = parse(composeFileSecretsRoot1) as ComposeSpecification;
|
||||
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
@@ -84,7 +84,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in root property (Test 2)", () => {
|
||||
const composeData = parse(composeFileSecretsRoot2) as ComposeSpecification;
|
||||
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData?.secrets) {
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
addSuffixToSecretsInServices,
|
||||
generateRandomHash,
|
||||
} from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFileSecretsServices = `
|
||||
version: "3.8"
|
||||
@@ -21,7 +21,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services", () => {
|
||||
const composeData = parse(composeFileSecretsServices) as ComposeSpecification;
|
||||
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
@@ -54,9 +54,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services (Test 1)", () => {
|
||||
const composeData = parse(
|
||||
composeFileSecretsServices1,
|
||||
) as ComposeSpecification;
|
||||
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
@@ -95,9 +93,7 @@ secrets:
|
||||
`;
|
||||
|
||||
test("Add suffix to secrets in services (Test 2)", () => {
|
||||
const composeData = parse(
|
||||
composeFileSecretsServices2,
|
||||
) as ComposeSpecification;
|
||||
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
if (!composeData.services) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFileCombinedSecrets = `
|
||||
version: "3.8"
|
||||
@@ -25,7 +25,7 @@ secrets:
|
||||
file: ./app_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets = parse(`
|
||||
const expectedComposeFileCombinedSecrets = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -48,7 +48,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets", () => {
|
||||
const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
@@ -77,7 +77,7 @@ secrets:
|
||||
file: ./cache_secret.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets3 = parse(`
|
||||
const expectedComposeFileCombinedSecrets3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -99,9 +99,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets (3rd Case)", () => {
|
||||
const composeData = parse(
|
||||
composeFileCombinedSecrets3,
|
||||
) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
@@ -130,7 +128,7 @@ secrets:
|
||||
file: ./db_password.txt
|
||||
`;
|
||||
|
||||
const expectedComposeFileCombinedSecrets4 = parse(`
|
||||
const expectedComposeFileCombinedSecrets4 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -152,9 +150,7 @@ secrets:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all secrets (4th Case)", () => {
|
||||
const composeData = parse(
|
||||
composeFileCombinedSecrets4,
|
||||
) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -27,7 +27,7 @@ test("Generate random hash with 8 characters", () => {
|
||||
});
|
||||
|
||||
test("Add suffix to service names with container_name in compose file", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -32,7 +32,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with depends_on (array) in compose file", () => {
|
||||
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -102,7 +102,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with depends_on (object) in compose file", () => {
|
||||
const composeData = parse(composeFile5) as ComposeSpecification;
|
||||
const composeData = load(composeFile5) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -30,7 +30,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with extends (string) in compose file", () => {
|
||||
const composeData = parse(composeFile6) as ComposeSpecification;
|
||||
const composeData = load(composeFile6) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -90,7 +90,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with extends (object) in compose file", () => {
|
||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||
const composeData = load(composeFile7) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -31,7 +31,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with links in compose file", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -26,7 +26,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names in compose file", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
addSuffixToAllServiceNames,
|
||||
addSuffixToServiceNames,
|
||||
} from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFileCombinedAllCases = `
|
||||
version: "3.8"
|
||||
@@ -38,7 +38,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile = parse(`
|
||||
const expectedComposeFile = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -71,9 +71,7 @@ networks:
|
||||
`);
|
||||
|
||||
test("Add suffix to all service names in compose file", () => {
|
||||
const composeData = parse(
|
||||
composeFileCombinedAllCases,
|
||||
) as ComposeSpecification;
|
||||
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -133,7 +131,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile1 = parse(`
|
||||
const expectedComposeFile1 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -178,7 +176,7 @@ networks:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all service names in compose file 1", () => {
|
||||
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
@@ -229,7 +227,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile2 = parse(`
|
||||
const expectedComposeFile2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -273,7 +271,7 @@ networks:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all service names in compose file 2", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
@@ -324,7 +322,7 @@ networks:
|
||||
driver: bridge
|
||||
`;
|
||||
|
||||
const expectedComposeFile3 = parse(`
|
||||
const expectedComposeFile3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -368,7 +366,7 @@ networks:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to all service names in compose file 3", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -35,7 +35,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to service names with volumes_from in compose file", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
addSuffixToVolumesRoot,
|
||||
generateRandomHash,
|
||||
} from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
services:
|
||||
@@ -70,7 +70,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedDockerCompose = parse(`
|
||||
const expectedDockerCompose = load(`
|
||||
services:
|
||||
mail:
|
||||
image: bytemark/smtp
|
||||
@@ -143,7 +143,7 @@ test("Generate random hash with 8 characters", () => {
|
||||
// Docker compose needs unique names for services, volumes, networks and containers
|
||||
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
|
||||
test("Add suffix to volumes root property", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -165,7 +165,7 @@ test("Add suffix to volumes root property", () => {
|
||||
});
|
||||
|
||||
test("Expect to change the suffix in all the possible places", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -195,7 +195,7 @@ volumes:
|
||||
mongo-data:
|
||||
`;
|
||||
|
||||
const expectedDockerCompose2 = parse(`
|
||||
const expectedDockerCompose2 = load(`
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
@@ -218,7 +218,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Expect to change the suffix in all the possible places (2 Try)", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -248,7 +248,7 @@ volumes:
|
||||
mongo-data:
|
||||
`;
|
||||
|
||||
const expectedDockerCompose3 = parse(`
|
||||
const expectedDockerCompose3 = load(`
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
@@ -271,7 +271,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Expect to change the suffix in all the possible places (3 Try)", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -645,7 +645,7 @@ volumes:
|
||||
db-config:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeComplex = parse(`
|
||||
const expectedDockerComposeComplex = load(`
|
||||
version: "3.8"
|
||||
services:
|
||||
studio:
|
||||
@@ -1012,7 +1012,7 @@ volumes:
|
||||
`);
|
||||
|
||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||
const composeData = parse(composeFileComplex) as ComposeSpecification;
|
||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -1065,7 +1065,7 @@ volumes:
|
||||
db-data:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeExample1 = parse(`
|
||||
const expectedDockerComposeExample1 = load(`
|
||||
version: "3.8"
|
||||
services:
|
||||
web:
|
||||
@@ -1111,7 +1111,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
||||
const composeData = parse(composeFileExample1) as ComposeSpecification;
|
||||
const composeData = load(composeFileExample1) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
@@ -1143,7 +1143,7 @@ volumes:
|
||||
backrest-cache:
|
||||
`;
|
||||
|
||||
const expectedDockerComposeBackrest = parse(`
|
||||
const expectedDockerComposeBackrest = load(`
|
||||
services:
|
||||
backrest:
|
||||
image: garethgeorge/backrest:v1.7.3
|
||||
@@ -1168,7 +1168,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Should handle volume paths with subdirectories correctly", () => {
|
||||
const composeData = parse(composeFileBackrest) as ComposeSpecification;
|
||||
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
||||
const suffix = "testhash";
|
||||
|
||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFile = `
|
||||
version: "3.8"
|
||||
@@ -29,7 +29,7 @@ test("Generate random hash with 8 characters", () => {
|
||||
});
|
||||
|
||||
test("Add suffix to volumes in root property", () => {
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -67,7 +67,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes in root property (Case 2)", () => {
|
||||
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||
const composeData = load(composeFile2) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -101,7 +101,7 @@ networks:
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes in root property (Case 3)", () => {
|
||||
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||
const composeData = load(composeFile3) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -148,7 +148,7 @@ volumes:
|
||||
`;
|
||||
|
||||
// Expected compose file con el prefijo `testhash`
|
||||
const expectedComposeFile4 = parse(`
|
||||
const expectedComposeFile4 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -179,7 +179,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to volumes in root property", () => {
|
||||
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||
const composeData = load(composeFile4) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
addSuffixToVolumesInServices,
|
||||
generateRandomHash,
|
||||
} from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
test("Generate random hash with 8 characters", () => {
|
||||
const hash = generateRandomHash();
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes declared directly in services", () => {
|
||||
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||
const composeData = load(composeFile1) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
@@ -59,7 +59,7 @@ volumes:
|
||||
`;
|
||||
|
||||
test("Add suffix to volumes declared directly in services (Case 2)", () => {
|
||||
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
|
||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
||||
|
||||
const suffix = generateRandomHash();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { addSuffixToAllVolumes } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const composeFileTypeVolume = `
|
||||
version: "3.8"
|
||||
@@ -23,7 +23,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume = parse(`
|
||||
const expectedComposeFileTypeVolume = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -44,7 +44,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to volumes with type: volume in services", () => {
|
||||
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
|
||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -73,7 +73,7 @@ volumes:
|
||||
driver: local
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume1 = parse(`
|
||||
const expectedComposeFileTypeVolume1 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -93,7 +93,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to mixed volumes in services", () => {
|
||||
const composeData = parse(composeFileTypeVolume1) as ComposeSpecification;
|
||||
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -128,7 +128,7 @@ volumes:
|
||||
device: /path/to/app/logs
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume2 = parse(`
|
||||
const expectedComposeFileTypeVolume2 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -154,7 +154,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to complex volume configurations in services", () => {
|
||||
const composeData = parse(composeFileTypeVolume2) as ComposeSpecification;
|
||||
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
@@ -218,7 +218,7 @@ volumes:
|
||||
device: /path/to/shared/logs
|
||||
`;
|
||||
|
||||
const expectedComposeFileTypeVolume3 = parse(`
|
||||
const expectedComposeFileTypeVolume3 = load(`
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
@@ -273,7 +273,7 @@ volumes:
|
||||
`) as ComposeSpecification;
|
||||
|
||||
test("Add suffix to complex nested volumes configuration in services", () => {
|
||||
const composeData = parse(composeFileTypeVolume3) as ComposeSpecification;
|
||||
const composeData = load(composeFileTypeVolume3) as ComposeSpecification;
|
||||
|
||||
const suffix = "testhash";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import jsyaml from "js-yaml";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { parse, stringify, YAMLParseError } from "yaml";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { CodeEditor } from "@/components/shared/code-editor";
|
||||
@@ -38,11 +38,11 @@ interface Props {
|
||||
|
||||
export const validateAndFormatYAML = (yamlText: string) => {
|
||||
try {
|
||||
const obj = parse(yamlText);
|
||||
const formattedYaml = stringify(obj, { indent: 4 });
|
||||
const obj = jsyaml.load(yamlText);
|
||||
const formattedYaml = jsyaml.dump(obj, { indent: 4 });
|
||||
return { valid: true, formattedYaml, error: null };
|
||||
} catch (error) {
|
||||
if (error instanceof YAMLParseError) {
|
||||
if (error instanceof jsyaml.YAMLException) {
|
||||
return {
|
||||
valid: false,
|
||||
formattedYaml: yamlText,
|
||||
@@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
if (!valid) {
|
||||
form.setError("traefikConfig", {
|
||||
type: "manual",
|
||||
message: (error as string) || "Invalid YAML",
|
||||
message: error || "Invalid YAML",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
<Input
|
||||
placeholder="Search Template"
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="w-full"
|
||||
className="w-full sm:w-[200px]"
|
||||
value={query}
|
||||
/>
|
||||
<Input
|
||||
@@ -248,7 +248,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
onClick={() =>
|
||||
setViewMode(viewMode === "detailed" ? "icon" : "detailed")
|
||||
}
|
||||
className="h-9 w-9 flex-shrink-0"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
{viewMode === "detailed" ? (
|
||||
<LayoutGrid className="size-4" />
|
||||
|
||||
@@ -88,7 +88,7 @@ export const StepThree = ({ templateInfo }: StepProps) => {
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold">Configuration Files</h3>
|
||||
<ul className="list-disc pl-5">
|
||||
{templateInfo?.details?.configFiles?.map((file, index) => (
|
||||
{templateInfo?.details?.configFiles.map((file, index) => (
|
||||
<li key={index}>
|
||||
<strong className="text-sm font-semibold">
|
||||
{file.filePath}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Bot, PlusCircle, Trash2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
@@ -27,6 +27,7 @@ export interface StepProps {
|
||||
export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||
const suggestions = templateInfo.suggestions || [];
|
||||
const selectedVariant = templateInfo.details;
|
||||
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
api.ai.suggest.useMutation();
|
||||
@@ -43,7 +44,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||
.then((data) => {
|
||||
setTemplateInfo({
|
||||
...templateInfo,
|
||||
suggestions: data || [],
|
||||
suggestions: data,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -53,6 +54,10 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||
});
|
||||
}, [templateInfo.userInput]);
|
||||
|
||||
const toggleShowValue = (name: string) => {
|
||||
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
||||
};
|
||||
|
||||
const handleEnvVariableChange = (
|
||||
index: number,
|
||||
field: "name" | "value",
|
||||
@@ -303,9 +308,11 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||
placeholder="Variable Name"
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="flex-1 relative">
|
||||
<Input
|
||||
type={"password"}
|
||||
type={
|
||||
showValues[env.name] ? "text" : "password"
|
||||
}
|
||||
value={env.value}
|
||||
onChange={(e) =>
|
||||
handleEnvVariableChange(
|
||||
@@ -316,6 +323,19 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||
}
|
||||
placeholder="Variable Value"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
||||
onClick={() => toggleShowValue(env.name)}
|
||||
>
|
||||
{showValues[env.name] ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -417,14 +437,13 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||
<AccordionContent>
|
||||
<ScrollArea className="w-full rounded-md border">
|
||||
<div className="p-4 space-y-4">
|
||||
{selectedVariant?.configFiles?.length &&
|
||||
selectedVariant?.configFiles?.length > 0 ? (
|
||||
{selectedVariant?.configFiles?.length > 0 ? (
|
||||
<>
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
This template requires the following
|
||||
configuration files to be mounted:
|
||||
</div>
|
||||
{selectedVariant?.configFiles?.map(
|
||||
{selectedVariant.configFiles.map(
|
||||
(config, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
||||
@@ -47,7 +47,7 @@ interface Details {
|
||||
envVariables: EnvVariable[];
|
||||
shortDescription: string;
|
||||
domains: Domain[];
|
||||
configFiles?: Mount[];
|
||||
configFiles: Mount[];
|
||||
}
|
||||
|
||||
interface Mount {
|
||||
|
||||
@@ -80,29 +80,6 @@ export const DuplicateProject = ({
|
||||
api.project.duplicate.useMutation({
|
||||
onSuccess: async (newProject) => {
|
||||
await utils.project.all.invalidate();
|
||||
|
||||
// If duplicating to same project+environment, invalidate the environment query
|
||||
// to refresh the services list
|
||||
if (duplicateType === "existing-environment") {
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId: selectedTargetEnvironment,
|
||||
});
|
||||
await utils.environment.byProjectId.invalidate({
|
||||
projectId: selectedTargetProject,
|
||||
});
|
||||
|
||||
// If duplicating to the same environment we're currently viewing,
|
||||
// also invalidate the current environment to refresh the services list
|
||||
if (selectedTargetEnvironment === environmentId) {
|
||||
await utils.environment.one.invalidate({ environmentId });
|
||||
// Also invalidate the project query to refresh the project data
|
||||
const projectId = router.query.projectId as string;
|
||||
if (projectId) {
|
||||
await utils.project.one.invalidate({ projectId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(
|
||||
duplicateType === "new-project"
|
||||
? "Project duplicated successfully"
|
||||
|
||||
@@ -291,48 +291,45 @@ export const ShowProjects = () => {
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
)}
|
||||
{project.environments.some(
|
||||
(env) => env.compose.length > 0,
|
||||
) && (
|
||||
{/*
|
||||
{project.compose.length > 0 && (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>
|
||||
Compose
|
||||
</DropdownMenuLabel>
|
||||
{project.environments.map((env) =>
|
||||
env.compose.map((comp) => (
|
||||
<div key={comp.composeId}>
|
||||
{project.compose.map((comp) => (
|
||||
<div key={comp.composeId}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||
{comp.name}
|
||||
<StatusTooltip
|
||||
status={comp.composeStatus}
|
||||
/>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||
{comp.name}
|
||||
<StatusTooltip
|
||||
status={comp.composeStatus}
|
||||
/>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{comp.domains.map((domain) => (
|
||||
<DropdownMenuItem
|
||||
key={domain.domainId}
|
||||
asChild
|
||||
{comp.domains.map((domain) => (
|
||||
<DropdownMenuItem
|
||||
key={domain.domainId}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||
target="_blank"
|
||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||
>
|
||||
<Link
|
||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||
target="_blank"
|
||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||
>
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
)),
|
||||
)}
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
)}
|
||||
)} */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Progress } from "@/components/ui/progress";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { ShowProviders } from "./show-providers";
|
||||
|
||||
const stripePromise = loadStripe(
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!,
|
||||
@@ -151,44 +152,25 @@ export const ShowBilling = () => {
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
{products?.map((product) => {
|
||||
const featured = true;
|
||||
return (
|
||||
<div key={product.id}>
|
||||
<section
|
||||
className={clsx(
|
||||
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||
featured
|
||||
? "order-first border py-8 lg:order-none"
|
||||
: "lg:py-8",
|
||||
)}
|
||||
>
|
||||
{isAnnual && (
|
||||
<div className="mb-4 flex flex-row items-center gap-2">
|
||||
<Badge>Recommended 🚀</Badge>
|
||||
</div>
|
||||
)}
|
||||
{isAnnual ? (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(
|
||||
serverQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}{" "}
|
||||
USD
|
||||
</p>
|
||||
|
|
||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||
${" "}
|
||||
{(
|
||||
calculatePrice(serverQuantity, isAnnual) / 12
|
||||
).toFixed(2)}{" "}
|
||||
/ Month USD
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
products?.map((product) => {
|
||||
const featured = true;
|
||||
return (
|
||||
<div key={product.id}>
|
||||
<section
|
||||
className={clsx(
|
||||
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||
featured
|
||||
? "order-first border py-8 lg:order-none"
|
||||
: "lg:py-8",
|
||||
)}
|
||||
>
|
||||
{isAnnual && (
|
||||
<div className="mb-4 flex flex-row items-center gap-2">
|
||||
<Badge>Recommended 🚀</Badge>
|
||||
</div>
|
||||
)}
|
||||
{isAnnual ? (
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
||||
@@ -196,127 +178,146 @@ export const ShowBilling = () => {
|
||||
)}{" "}
|
||||
USD
|
||||
</p>
|
||||
)}
|
||||
<h3 className="mt-5 font-medium text-lg text-primary">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-sm",
|
||||
featured ? "text-white" : "text-slate-400",
|
||||
)}
|
||||
>
|
||||
{product.description}
|
||||
|
|
||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||
${" "}
|
||||
{(
|
||||
calculatePrice(serverQuantity, isAnnual) / 12
|
||||
).toFixed(2)}{" "}
|
||||
/ Month USD
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
||||
2,
|
||||
)}{" "}
|
||||
USD
|
||||
</p>
|
||||
)}
|
||||
<h3 className="mt-5 font-medium text-lg text-primary">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-sm",
|
||||
featured ? "text-white" : "text-slate-400",
|
||||
)}
|
||||
>
|
||||
{product.description}
|
||||
</p>
|
||||
|
||||
<ul
|
||||
className={clsx(
|
||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||
featured ? "text-white" : "text-slate-200",
|
||||
<ul
|
||||
className={clsx(
|
||||
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||
featured ? "text-white" : "text-slate-200",
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"All the features of Dokploy",
|
||||
"Unlimited deployments",
|
||||
"Self-hosted on your own infrastructure",
|
||||
"Full access to all deployment features",
|
||||
"Dokploy integration",
|
||||
"Backups",
|
||||
"All Incoming features",
|
||||
].map((feature) => (
|
||||
<li
|
||||
key={feature}
|
||||
className="flex text-muted-foreground"
|
||||
>
|
||||
<CheckIcon />
|
||||
<span className="ml-4">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{serverQuantity} Servers
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (serverQuantity <= 1) return;
|
||||
|
||||
setServerQuantity(serverQuantity - 1);
|
||||
}}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
onChange={(e) => {
|
||||
setServerQuantity(
|
||||
e.target.value as unknown as number,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setServerQuantity(serverQuantity + 1);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
data?.subscriptions &&
|
||||
data?.subscriptions?.length > 0
|
||||
? "justify-between"
|
||||
: "justify-end",
|
||||
"flex flex-row items-center gap-2 mt-4",
|
||||
)}
|
||||
>
|
||||
{[
|
||||
"All the features of Dokploy",
|
||||
"Unlimited deployments",
|
||||
"Self-hosted on your own infrastructure",
|
||||
"Full access to all deployment features",
|
||||
"Dokploy integration",
|
||||
"Backups",
|
||||
"All Incoming features",
|
||||
].map((feature) => (
|
||||
<li
|
||||
key={feature}
|
||||
className="flex text-muted-foreground"
|
||||
>
|
||||
<CheckIcon />
|
||||
<span className="ml-4">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{serverQuantity} Servers
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{admin?.user.stripeCustomerId && (
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (serverQuantity <= 1) return;
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
const session =
|
||||
await createCustomerPortalSession();
|
||||
|
||||
setServerQuantity(serverQuantity - 1);
|
||||
window.open(session.url);
|
||||
}}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
Manage Subscription
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
onChange={(e) => {
|
||||
setServerQuantity(
|
||||
e.target.value as unknown as number,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setServerQuantity(serverQuantity + 1);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
data?.subscriptions &&
|
||||
data?.subscriptions?.length > 0
|
||||
? "justify-between"
|
||||
: "justify-end",
|
||||
"flex flex-row items-center gap-2 mt-4",
|
||||
)}
|
||||
>
|
||||
{admin?.user.stripeCustomerId && (
|
||||
{data?.subscriptions?.length === 0 && (
|
||||
<div className="justify-end w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
const session =
|
||||
await createCustomerPortalSession();
|
||||
|
||||
window.open(session.url);
|
||||
handleCheckout(product.id);
|
||||
}}
|
||||
disabled={serverQuantity < 1}
|
||||
>
|
||||
Manage Subscription
|
||||
Subscribe
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{data?.subscriptions?.length === 0 && (
|
||||
<div className="justify-end w-full">
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={async () => {
|
||||
handleCheckout(product.id);
|
||||
}}
|
||||
disabled={serverQuantity < 1}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<div className="flex flex-col gap-4 pb-10">
|
||||
<ShowProviders />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Cpu,
|
||||
EuroIcon,
|
||||
HardDrive,
|
||||
Loader2,
|
||||
MapPin,
|
||||
MemoryStick,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
// Function to classify servers by type
|
||||
function getServerCategory(cpuType: string) {
|
||||
if (cpuType === "shared") {
|
||||
return {
|
||||
category: "Shared CPU",
|
||||
icon: Cpu,
|
||||
badge: "shared",
|
||||
color: "bg-blue-500",
|
||||
description: "Perfect for small and medium projects",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
category: "Dedicated CPU",
|
||||
icon: Zap,
|
||||
badge: "dedicated",
|
||||
color: "bg-purple-500",
|
||||
description: "Maximum performance for demanding applications",
|
||||
};
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
location: z.string().min(1, "Please select a location"),
|
||||
architecture: z.enum(["x86", "arm"], {
|
||||
required_error: "Please select an architecture",
|
||||
}),
|
||||
selectedServerId: z.string().optional(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export const ShowHetznerProviders = () => {
|
||||
const { data: serverTypesData, isLoading: isLoadingTypes } =
|
||||
api.hetzner.serverTypes.useQuery();
|
||||
const { data: locationsData, isLoading: isLoadingLocations } =
|
||||
api.hetzner.locations.useQuery();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
location: "",
|
||||
architecture: "x86",
|
||||
selectedServerId: "",
|
||||
},
|
||||
});
|
||||
|
||||
const selectedLocation = form.watch("location");
|
||||
const selectedArchitecture = form.watch("architecture");
|
||||
|
||||
if (isLoadingTypes || isLoadingLocations) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const locations = locationsData?.locations ?? [];
|
||||
const serverTypes = serverTypesData?.server_types ?? [];
|
||||
|
||||
// Filter server types by selected location AND architecture
|
||||
const filteredServerTypes = serverTypes.filter(
|
||||
(type) =>
|
||||
type.prices.some((price) => price.location === selectedLocation) &&
|
||||
type.architecture === selectedArchitecture,
|
||||
);
|
||||
|
||||
// Group by CPU type (shared/dedicated)
|
||||
const sharedServers = filteredServerTypes.filter(
|
||||
(type) => type.cpu_type === "shared",
|
||||
);
|
||||
const dedicatedServers = filteredServerTypes.filter(
|
||||
(type) => type.cpu_type === "dedicated",
|
||||
);
|
||||
|
||||
const renderServerGrid = (
|
||||
servers: typeof serverTypes,
|
||||
category: ReturnType<typeof getServerCategory>,
|
||||
) => {
|
||||
if (!servers.length) return null;
|
||||
const IconComponent = category.icon;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconComponent className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">{category.category}</h3>
|
||||
<Badge variant="outline" className={`text-white ${category.color}`}>
|
||||
{category.badge}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
Sorted by price
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{category.description}
|
||||
</p>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="selectedServerId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
||||
>
|
||||
{servers.map((server) => (
|
||||
<div key={server.id} className="relative">
|
||||
<RadioGroupItem
|
||||
value={server.id.toString()}
|
||||
id={`server-${server.id}`}
|
||||
className="absolute right-4 top-4 z-10"
|
||||
/>
|
||||
<label htmlFor={`server-${server.id}`}>
|
||||
<Card
|
||||
className={`relative bg-transparent transition-all duration-200 cursor-pointer ${
|
||||
field.value === server.id.toString()
|
||||
? "border-primary bg-primary/5"
|
||||
: "hover:bg-primary/5"
|
||||
}`}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>{server.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{server.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-blue-500" />
|
||||
<div>
|
||||
<strong>Cores:</strong> {server.cores}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<MemoryStick className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<strong>Memory:</strong> {server.memory} GB
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-purple-500" />
|
||||
<div>
|
||||
<strong>Disk:</strong> {server.disk} GB
|
||||
</div>
|
||||
</div>
|
||||
{/* Show price for selected location */}
|
||||
{server.prices
|
||||
.filter((p) => p.location === selectedLocation)
|
||||
.map((p) => (
|
||||
<div
|
||||
key={p.location}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<EuroIcon className="h-4 w-4 text-yellow-500" />
|
||||
<div>
|
||||
<strong>Price (monthly):</strong> €
|
||||
{Number.parseFloat(
|
||||
p.price_monthly.net,
|
||||
).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function onSubmit(values: FormValues) {
|
||||
console.log("Form submitted:", values);
|
||||
// Here you can handle the form submission with the selected server
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Filters Card */}
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-xl">
|
||||
<MapPin className="h-5 w-5" />
|
||||
Filters
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Choose a region and architecture to see location-specific pricing
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Form {...form}>
|
||||
<div className="space-y-6">
|
||||
{/* Region Selector */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="location"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Region</FormLabel>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a region" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{locations.map((loc) => (
|
||||
<SelectItem key={loc.id} value={loc.name}>
|
||||
{loc.description}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Architecture Selector */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="architecture"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Architecture</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="grid grid-cols-2 gap-4"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="x86" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
x86 (Intel/AMD)
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="arm" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">ARM</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Architecture Information */}
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-blue-900 rounded-lg border border-blue-600">
|
||||
<div className="flex items-start gap-2">
|
||||
<Cpu className="h-5 w-5 text-blue-600 mt-0.5" />
|
||||
<div className="text-sm text-blue-200">
|
||||
<strong>x86 Architecture:</strong> Traditional Intel/AMD
|
||||
processors. Most compatible with existing software and
|
||||
applications. Best choice for general-purpose workloads.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-900 rounded-lg border border-green-600">
|
||||
<div className="flex items-start gap-2">
|
||||
<Cpu className="h-5 w-5 text-green-600 mt-0.5" />
|
||||
<div className="text-sm text-green-200">
|
||||
<strong>ARM Architecture:</strong> Modern, energy-efficient
|
||||
processors. Excellent price-to-performance ratio. Perfect for
|
||||
cloud-native and containerized applications.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Types Grid */}
|
||||
{selectedLocation && (
|
||||
<>
|
||||
{renderServerGrid(sharedServers, getServerCategory("shared"))}
|
||||
{renderServerGrid(dedicatedServers, getServerCategory("dedicated"))}
|
||||
{sharedServers.length === 0 && dedicatedServers.length === 0 && (
|
||||
<p className="text-center text-muted-foreground py-10">
|
||||
No server types available for this region and architecture
|
||||
combination. <br />
|
||||
Please try a different region or architecture.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedLocation && form.watch("selectedServerId") && (
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" className="bg-primary">
|
||||
Continue with Selected Server
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,423 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Calendar,
|
||||
Cpu,
|
||||
DollarSign,
|
||||
Globe,
|
||||
HardDrive,
|
||||
Loader2,
|
||||
MemoryStick,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
datacenter: z.string({
|
||||
required_error: "Please select a datacenter",
|
||||
}),
|
||||
plan: z.string({
|
||||
required_error: "Please select a server plan",
|
||||
}),
|
||||
billingPeriod: z.object(
|
||||
{
|
||||
unit: z.enum(["month", "year"]),
|
||||
period: z.number(),
|
||||
},
|
||||
{
|
||||
required_error: "Please select a billing period",
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
// Format billing period
|
||||
function formatBillingPeriod(period: number, unit: string): string {
|
||||
if (unit === "month") {
|
||||
return period === 1 ? "Monthly" : `${period} months`;
|
||||
}
|
||||
if (unit === "year") {
|
||||
return period === 1 ? "Yearly" : `${period} years`;
|
||||
}
|
||||
return `${period} ${unit}`;
|
||||
}
|
||||
|
||||
// Convert price from cents to dollars
|
||||
function formatPrice(priceInCents: number): string {
|
||||
return (priceInCents / 100).toFixed(2);
|
||||
}
|
||||
|
||||
// Calculate yearly savings
|
||||
function calculateSavings(
|
||||
monthlyPriceInCents: number,
|
||||
yearlyPriceInCents: number,
|
||||
): number {
|
||||
return (monthlyPriceInCents * 12 - yearlyPriceInCents) / 100;
|
||||
}
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ShowHostingerServers = () => {
|
||||
const { data: vpsPlans, isLoading: plansLoading } =
|
||||
api.hostinger.vpsPlans.useQuery();
|
||||
const { data: dataCenters, isLoading: centersLoading } =
|
||||
api.hostinger.dataCenters.useQuery();
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
datacenter: "",
|
||||
plan: "",
|
||||
billingPeriod: {
|
||||
unit: "month",
|
||||
period: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = plansLoading || centersLoading;
|
||||
|
||||
function onSubmit(data: FormData) {
|
||||
console.log(data);
|
||||
// Handle form submission here
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5" />
|
||||
Hostinger VPS Plans
|
||||
<Badge variant="outline" className="text-xs text-muted-foreground">
|
||||
Sorted by price
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
VPS plans with real pricing from Hostinger API
|
||||
<br />
|
||||
<span className="text-xs text-orange-600">
|
||||
💡 Promotional pricing applies to first billing period only
|
||||
</span>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{/* Data Center Selection */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="datacenter"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">
|
||||
Select Data Center
|
||||
</span>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"
|
||||
>
|
||||
{dataCenters?.map((center) => (
|
||||
<FormItem key={center.id}>
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value={center.id?.toString() || ""}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="p-4 rounded-lg border-2 transition-all duration-200 flex flex-col items-center gap-2 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
|
||||
<Globe className="h-6 w-6 text-muted-foreground" />
|
||||
<span className="text-sm font-medium text-center">
|
||||
{center.city} / {center.continent}
|
||||
</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Billing Period Selection */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="billingPeriod"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">
|
||||
Billing Period
|
||||
</span>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => {
|
||||
switch (value) {
|
||||
case "monthly":
|
||||
field.onChange({ unit: "month", period: 1 });
|
||||
break;
|
||||
case "yearly":
|
||||
field.onChange({ unit: "year", period: 1 });
|
||||
break;
|
||||
case "2years":
|
||||
field.onChange({ unit: "year", period: 2 });
|
||||
break;
|
||||
}
|
||||
}}
|
||||
defaultValue="monthly"
|
||||
className="grid w-full grid-cols-3 lg:w-[600px] gap-4"
|
||||
>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value="monthly"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="flex items-center justify-center p-3 rounded-lg border-2 transition-all duration-200 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
|
||||
Monthly Billing
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value="yearly"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="flex items-center justify-center p-3 rounded-lg border-2 transition-all duration-200 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
|
||||
Annual Billing
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value="2years"
|
||||
className="peer sr-only"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="flex items-center justify-center p-3 rounded-lg border-2 transition-all duration-200 peer-aria-checked:border-purple-500 peer-aria-checked:bg-purple-50 dark:peer-aria-checked:bg-purple-950 hover:border-purple-300 cursor-pointer">
|
||||
2 Year Billing
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* VPS Plans Selection */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="plan"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-4">
|
||||
<FormLabel>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-lg font-medium">
|
||||
Select Server Plan
|
||||
</span>
|
||||
</div>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
|
||||
>
|
||||
{vpsPlans
|
||||
?.sort((a, b) => {
|
||||
const billingPeriod = form.watch("billingPeriod");
|
||||
const priceA =
|
||||
a.prices?.find(
|
||||
(p) =>
|
||||
p.period_unit === billingPeriod.unit &&
|
||||
p.period === billingPeriod.period,
|
||||
)?.price || 0;
|
||||
const priceB =
|
||||
b.prices?.find(
|
||||
(p) =>
|
||||
p.period_unit === billingPeriod.unit &&
|
||||
p.period === billingPeriod.period,
|
||||
)?.price || 0;
|
||||
return priceA - priceB;
|
||||
})
|
||||
?.map((plan) => {
|
||||
const monthlyPrice =
|
||||
plan.prices?.find(
|
||||
(p) =>
|
||||
p.period === 1 && p.period_unit === "month",
|
||||
)?.price || 0;
|
||||
|
||||
const selectedPrice = plan.prices?.find(
|
||||
(p) =>
|
||||
p.period_unit ===
|
||||
form.watch("billingPeriod.unit") &&
|
||||
p.period === form.watch("billingPeriod.period"),
|
||||
);
|
||||
|
||||
if (!selectedPrice) return null;
|
||||
|
||||
return (
|
||||
<FormItem key={plan.id}>
|
||||
<FormControl>
|
||||
<RadioGroupItem
|
||||
value={plan.id || ""}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="w-full cursor-pointer">
|
||||
<Card
|
||||
className={`border-2 transition-all duration-200 relative bg-transparent hover:border-purple-300 hover:shadow-lg ${field.value === plan.id ? "border-purple-500 bg-purple-950/40" : ""}`}
|
||||
>
|
||||
{plan.name === "KVM 2" && (
|
||||
<div className="absolute -top-2 -right-2 bg-green-500 text-white px-2 py-1 rounded-full text-xs font-semibold">
|
||||
MOST POPULAR
|
||||
</div>
|
||||
)}
|
||||
<CardHeader>
|
||||
<CardTitle>{plan.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{"High-performance VPS hosting"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex flex-col items-center p-2 bg-muted rounded-lg">
|
||||
<Cpu className="h-4 w-4 mb-1 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">
|
||||
{plan.metadata?.cpus || 1} vCPU
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Cores
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-2 bg-muted rounded-lg">
|
||||
<MemoryStick className="h-4 w-4 mb-1 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">
|
||||
{Number.parseInt(
|
||||
plan.metadata?.memory || "2048",
|
||||
) / 1024}{" "}
|
||||
GB
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
RAM
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center p-2 bg-muted rounded-lg">
|
||||
<HardDrive className="h-4 w-4 mb-1 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">
|
||||
{Number.parseInt(
|
||||
plan.metadata?.disk_space ||
|
||||
"20480",
|
||||
) / 1024}{" "}
|
||||
GB
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
SSD
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="flex items-center justify-between p-3 bg-primary/5 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<Calendar className="h-4 w-4 mr-2 text-muted-foreground" />
|
||||
<span className="text-sm">
|
||||
{formatBillingPeriod(
|
||||
selectedPrice.period || 1,
|
||||
selectedPrice.period_unit ||
|
||||
"month",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<DollarSign className="h-4 w-4 mr-1 text-primary" />
|
||||
<span className="text-lg font-semibold">
|
||||
$
|
||||
{formatPrice(
|
||||
selectedPrice.price || 0,
|
||||
)}
|
||||
</span>
|
||||
{selectedPrice.period_unit ===
|
||||
"year" && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 text-xs"
|
||||
>
|
||||
Save $
|
||||
{calculateSavings(
|
||||
monthlyPrice,
|
||||
selectedPrice.price || 0,
|
||||
).toFixed(2)}
|
||||
/yr
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Create Server
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { DollarSign } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ShowHetznerProviders } from "./show-hetzner-providers";
|
||||
import { ShowHostingerServers } from "./show-hostinger-servers";
|
||||
|
||||
export const ShowProviders = () => {
|
||||
return (
|
||||
<Card className="w-full bg-transparent border-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<DollarSign className="h-6 w-6 text-green-600" />
|
||||
Servers
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Manage and view available server types from Hetzner and Hostinger for
|
||||
your business. Here you can see updated pricing and specifications for
|
||||
each plan.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="hetzner" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="hetzner" className="flex items-center gap-2">
|
||||
🇩🇪 Hetzner Cloud
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="hostinger" className="flex items-center gap-2">
|
||||
🌍 Hostinger VPS
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="hetzner" className="mt-4">
|
||||
<ShowHetznerProviders />
|
||||
</TabsContent>
|
||||
<TabsContent value="hostinger" className="mt-4">
|
||||
<ShowHostingerServers />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -26,12 +27,18 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { useUrl } from "@/utils/hooks/use-url";
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1, { message: "Name is required" }),
|
||||
username: z.string().min(1, { message: "Username is required" }),
|
||||
email: z.string().email().optional(),
|
||||
apiToken: z.string().min(1, { message: "API Token is required" }),
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
username: z.string().min(1, {
|
||||
message: "Username is required",
|
||||
}),
|
||||
password: z.string().min(1, {
|
||||
message: "App Password is required",
|
||||
}),
|
||||
workspaceName: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -40,12 +47,14 @@ type Schema = z.infer<typeof Schema>;
|
||||
export const AddBitbucketProvider = () => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const _url = useUrl();
|
||||
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const _router = useRouter();
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
apiToken: "",
|
||||
password: "",
|
||||
workspaceName: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
@@ -54,8 +63,7 @@ export const AddBitbucketProvider = () => {
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
username: "",
|
||||
email: "",
|
||||
apiToken: "",
|
||||
password: "",
|
||||
workspaceName: "",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
@@ -63,11 +71,10 @@ export const AddBitbucketProvider = () => {
|
||||
const onSubmit = async (data: Schema) => {
|
||||
await mutateAsync({
|
||||
bitbucketUsername: data.username,
|
||||
apiToken: data.apiToken,
|
||||
appPassword: data.password,
|
||||
bitbucketWorkspaceName: data.workspaceName || "",
|
||||
authId: auth?.id || "",
|
||||
name: data.name || "",
|
||||
bitbucketEmail: data.email || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
@@ -106,46 +113,37 @@ export const AddBitbucketProvider = () => {
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<AlertBlock type="warning">
|
||||
Bitbucket App Passwords are deprecated for new providers. Use
|
||||
an API Token instead. Existing providers with App Passwords
|
||||
will continue to work until 9th June 2026.
|
||||
</AlertBlock>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
Manage tokens in
|
||||
<Link
|
||||
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-1 ml-1"
|
||||
>
|
||||
<span>Bitbucket settings</span>
|
||||
<ExternalLink className="w-fit text-primary size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
|
||||
<li className="text-muted-foreground text-sm">
|
||||
Click on Create API token with scopes
|
||||
</li>
|
||||
<li className="text-muted-foreground text-sm">
|
||||
Select the expiration date (Max 1 year)
|
||||
</li>
|
||||
<li className="text-muted-foreground text-sm">
|
||||
Select Bitbucket product.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Select the following scopes:
|
||||
To integrate your Bitbucket account, you need to create a new
|
||||
App Password in your Bitbucket settings. Follow these steps:
|
||||
</p>
|
||||
|
||||
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
|
||||
<li>read:repository:bitbucket</li>
|
||||
<li>read:pullrequest:bitbucket</li>
|
||||
<li>read:webhook:bitbucket</li>
|
||||
<li>read:workspace:bitbucket</li>
|
||||
<li>write:webhook:bitbucket</li>
|
||||
</ul>
|
||||
|
||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
||||
<li className="flex flex-row gap-2 items-center">
|
||||
Create new App Password{" "}
|
||||
<Link
|
||||
href="https://bitbucket.org/account/settings/app-passwords/new"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="w-fit text-primary size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
When creating the App Password, ensure you grant the
|
||||
following permissions:
|
||||
<ul className="list-disc list-inside ml-4">
|
||||
<li>Account: Read</li>
|
||||
<li>Workspace membership: Read</li>
|
||||
<li>Projects: Read</li>
|
||||
<li>Repositories: Read</li>
|
||||
<li>Pull requests: Read</li>
|
||||
<li>Webhooks: Read and write</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
After creating, you'll receive an App Password. Copy it and
|
||||
paste it below along with your Bitbucket username.
|
||||
</li>
|
||||
</ol>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -154,7 +152,7 @@ export const AddBitbucketProvider = () => {
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Your Bitbucket Provider, eg: my-personal-account"
|
||||
placeholder="Random Name eg(my-personal-account)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -181,27 +179,14 @@ export const AddBitbucketProvider = () => {
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bitbucket Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your Bitbucket email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormLabel>App Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Paste your Bitbucket API token"
|
||||
type="password"
|
||||
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -215,7 +200,7 @@ export const AddBitbucketProvider = () => {
|
||||
name="workspaceName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Workspace Name (optional)</FormLabel>
|
||||
<FormLabel>Workspace Name (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="For organization accounts"
|
||||
|
||||
@@ -33,10 +33,7 @@ const Schema = z.object({
|
||||
username: z.string().min(1, {
|
||||
message: "Username is required",
|
||||
}),
|
||||
email: z.string().email().optional(),
|
||||
workspaceName: z.string().optional(),
|
||||
apiToken: z.string().optional(),
|
||||
appPassword: z.string().optional(),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
@@ -63,28 +60,19 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
email: "",
|
||||
workspaceName: "",
|
||||
apiToken: "",
|
||||
appPassword: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
|
||||
const username = form.watch("username");
|
||||
const email = form.watch("email");
|
||||
const workspaceName = form.watch("workspaceName");
|
||||
const apiToken = form.watch("apiToken");
|
||||
const appPassword = form.watch("appPassword");
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
username: bitbucket?.bitbucketUsername || "",
|
||||
email: bitbucket?.bitbucketEmail || "",
|
||||
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
|
||||
name: bitbucket?.gitProvider.name || "",
|
||||
apiToken: bitbucket?.apiToken || "",
|
||||
appPassword: bitbucket?.appPassword || "",
|
||||
});
|
||||
}, [form, isOpen, bitbucket]);
|
||||
|
||||
@@ -93,11 +81,8 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
||||
bitbucketId,
|
||||
gitProviderId: bitbucket?.gitProviderId || "",
|
||||
bitbucketUsername: data.username,
|
||||
bitbucketEmail: data.email || "",
|
||||
bitbucketWorkspaceName: data.workspaceName || "",
|
||||
name: data.name || "",
|
||||
apiToken: data.apiToken || "",
|
||||
appPassword: data.appPassword || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
@@ -136,12 +121,6 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Update your Bitbucket authentication. Use API Token for
|
||||
enhanced security (recommended) or App Password for legacy
|
||||
support.
|
||||
</p>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -175,24 +154,6 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email (Required for API Tokens)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Your Bitbucket email address"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="workspaceName"
|
||||
@@ -210,49 +171,6 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t pt-4">
|
||||
<h3 className="text-sm font-medium mb-2">
|
||||
Authentication (Update to use API Token)
|
||||
</h3>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="apiToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token (Recommended)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your Bitbucket API Token"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
App Password (Legacy - will be deprecated June 2026)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Enter your Bitbucket App Password"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full justify-between gap-4 mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -262,10 +180,7 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
||||
await testConnection({
|
||||
bitbucketId,
|
||||
bitbucketUsername: username,
|
||||
bitbucketEmail: email,
|
||||
workspaceName: workspaceName,
|
||||
apiToken: apiToken,
|
||||
appPassword: appPassword,
|
||||
})
|
||||
.then(async (message) => {
|
||||
toast.info(`Message: ${message}`);
|
||||
|
||||
@@ -30,9 +30,6 @@ const Schema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
appName: z.string().min(1, {
|
||||
message: "App Name is required",
|
||||
}),
|
||||
});
|
||||
|
||||
type Schema = z.infer<typeof Schema>;
|
||||
@@ -58,7 +55,6 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
||||
const form = useForm<Schema>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
appName: "",
|
||||
},
|
||||
resolver: zodResolver(Schema),
|
||||
});
|
||||
@@ -66,7 +62,6 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
name: github?.gitProvider.name || "",
|
||||
appName: github?.githubAppName || "",
|
||||
});
|
||||
}, [form, isOpen]);
|
||||
|
||||
@@ -75,7 +70,6 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
||||
githubId,
|
||||
name: data.name || "",
|
||||
gitProviderId: github?.gitProviderId || "",
|
||||
githubAppName: data.appName || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.gitProvider.getAll.invalidate();
|
||||
@@ -130,22 +124,6 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>App Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="pp Name eg(my-personal)"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex w-full justify-between gap-4 mt-4">
|
||||
<Button
|
||||
|
||||
@@ -157,13 +157,7 @@ export const ShowGitProviders = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
{isBitbucket &&
|
||||
gitProvider.bitbucket?.appPassword &&
|
||||
!gitProvider.bitbucket?.apiToken ? (
|
||||
<Badge variant="yellow">Deprecated</Badge>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-row gap-1">
|
||||
{!haveGithubRequirements && isGithub && (
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
<Badge
|
||||
|
||||
@@ -33,10 +33,7 @@ import { Disable2FA } from "./disable-2fa";
|
||||
import { Enable2FA } from "./enable-2fa";
|
||||
|
||||
const profileSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.email("Please enter a valid email address")
|
||||
.min(1, "Email is required"),
|
||||
email: z.string(),
|
||||
password: z.string().nullable(),
|
||||
currentPassword: z.string().nullable(),
|
||||
image: z.string().optional(),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "bitbucket" ADD COLUMN "apiToken" text;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "bitbucket" ADD COLUMN "bitbucketEmail" text;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -778,20 +778,6 @@
|
||||
"when": 1757189541734,
|
||||
"tag": "0110_red_psynapse",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 111,
|
||||
"version": "7",
|
||||
"when": 1758445844561,
|
||||
"tag": "0111_mushy_wolfsbane",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 112,
|
||||
"version": "7",
|
||||
"when": 1758483520214,
|
||||
"tag": "0112_freezing_skrulls",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.25.3",
|
||||
"version": "v0.25.1",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -112,7 +112,7 @@
|
||||
"i18next": "^23.16.8",
|
||||
"input-otp": "^1.4.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"yaml": "2.8.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"lucide-react": "^0.469.0",
|
||||
"micromatch": "4.0.8",
|
||||
@@ -160,6 +160,7 @@
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "^18.19.104",
|
||||
@@ -181,7 +182,8 @@
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-tsconfig-paths": "4.3.2",
|
||||
"vitest": "^1.6.1"
|
||||
"vitest": "^1.6.1",
|
||||
"openapi-typescript": "7.8.0"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.25.2"
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
type Bitbucket,
|
||||
getBitbucketHeaders,
|
||||
IS_CLOUD,
|
||||
shouldDeploy,
|
||||
} from "@dokploy/server";
|
||||
import { IS_CLOUD, shouldDeploy } from "@dokploy/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { db } from "@/server/db";
|
||||
@@ -151,10 +146,10 @@ export default async function handler(
|
||||
|
||||
const commitedPaths = await extractCommitedPaths(
|
||||
req.body,
|
||||
application.bitbucket,
|
||||
application.bitbucketOwner,
|
||||
application.bitbucket?.appPassword || "",
|
||||
application.bitbucketRepository || "",
|
||||
);
|
||||
|
||||
const shouldDeployPaths = shouldDeploy(
|
||||
application.watchPaths,
|
||||
commitedPaths,
|
||||
@@ -359,8 +354,9 @@ export const getProviderByHeader = (headers: any) => {
|
||||
|
||||
export const extractCommitedPaths = async (
|
||||
body: any,
|
||||
bitbucket: Bitbucket | null,
|
||||
repository: string,
|
||||
bitbucketUsername: string | null,
|
||||
bitbucketAppPassword: string | null,
|
||||
repository: string | null,
|
||||
) => {
|
||||
const changes = body.push?.changes || [];
|
||||
|
||||
@@ -369,16 +365,18 @@ export const extractCommitedPaths = async (
|
||||
.filter(Boolean);
|
||||
const commitedPaths: string[] = [];
|
||||
for (const commit of commitHashes) {
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucket?.bitbucketUsername}/${repository}/diffstat/${commit}`;
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucketUsername}/${repository}/diffstat/${commit}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: getBitbucketHeaders(bitbucket!),
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketUsername}:${bitbucketAppPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
for (const value of data.values) {
|
||||
if (value?.new?.path) commitedPaths.push(value.new.path);
|
||||
commitedPaths.push(value.new?.path);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
||||
@@ -99,7 +99,8 @@ export default async function handler(
|
||||
|
||||
const commitedPaths = await extractCommitedPaths(
|
||||
req.body,
|
||||
composeResult.bitbucket,
|
||||
composeResult.bitbucketOwner,
|
||||
composeResult.bitbucket?.appPassword || "",
|
||||
composeResult.bitbucketRepository || "",
|
||||
);
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import { gitProviderRouter } from "./routers/git-provider";
|
||||
import { giteaRouter } from "./routers/gitea";
|
||||
import { githubRouter } from "./routers/github";
|
||||
import { gitlabRouter } from "./routers/gitlab";
|
||||
import { hetznerRouter } from "./routers/hetzner";
|
||||
import { hostingerRouter } from "./routers/hostinger";
|
||||
import { mariadbRouter } from "./routers/mariadb";
|
||||
import { mongoRouter } from "./routers/mongo";
|
||||
import { mountRouter } from "./routers/mount";
|
||||
@@ -85,6 +87,8 @@ export const appRouter = createTRPCRouter({
|
||||
schedule: scheduleRouter,
|
||||
rollback: rollbackRouter,
|
||||
volumeBackups: volumeBackupsRouter,
|
||||
hetzner: hetznerRouter,
|
||||
hostinger: hostingerRouter,
|
||||
environment: environmentRouter,
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { getLocalServerIp } from "@/server/wss/terminal";
|
||||
import { getPublicIpWithFallback } from "@/server/wss/terminal";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
export const clusterRouter = createTRPCRouter({
|
||||
getNodes: protectedProcedure
|
||||
@@ -61,7 +61,7 @@ export const clusterRouter = createTRPCRouter({
|
||||
const result = await docker.swarmInspect();
|
||||
const docker_version = await docker.version();
|
||||
|
||||
let ip = await getLocalServerIp();
|
||||
let ip = await getPublicIpWithFallback();
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
ip = server?.ipAddress;
|
||||
@@ -85,7 +85,7 @@ export const clusterRouter = createTRPCRouter({
|
||||
const result = await docker.swarmInspect();
|
||||
const docker_version = await docker.version();
|
||||
|
||||
let ip = await getLocalServerIp();
|
||||
let ip = await getPublicIpWithFallback();
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
ip = server?.ipAddress;
|
||||
|
||||
@@ -39,10 +39,10 @@ import {
|
||||
import { processTemplate } from "@dokploy/server/templates/processors";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { dump } from "js-yaml";
|
||||
import _ from "lodash";
|
||||
import { nanoid } from "nanoid";
|
||||
import { parse } from "toml";
|
||||
import { stringify } from "yaml";
|
||||
import { z } from "zod";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { db } from "@/server/db";
|
||||
@@ -364,7 +364,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
const domains = await findDomainsByComposeId(input.composeId);
|
||||
const composeFile = await addDomainToCompose(compose, domains);
|
||||
return stringify(composeFile, {
|
||||
return dump(composeFile, {
|
||||
lineWidth: 1000,
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
getGithubBranches,
|
||||
getGithubRepositories,
|
||||
haveGithubRequirements,
|
||||
updateGithub,
|
||||
updateGitProvider,
|
||||
} from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -135,9 +134,5 @@ export const githubRouter = createTRPCRouter({
|
||||
name: input.name,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
});
|
||||
|
||||
await updateGithub(input.githubId, {
|
||||
...input,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
21
apps/dokploy/server/api/routers/hetzner.ts
Normal file
21
apps/dokploy/server/api/routers/hetzner.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
fetchHetznerLocations,
|
||||
fetchHetznerServers,
|
||||
fetchHetznerServerTypes,
|
||||
} from "@dokploy/server/index";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const hetznerRouter = createTRPCRouter({
|
||||
locations: protectedProcedure.query(async () => {
|
||||
const locations = await fetchHetznerLocations();
|
||||
return locations;
|
||||
}),
|
||||
|
||||
serverTypes: protectedProcedure.query(async () => {
|
||||
return await fetchHetznerServerTypes();
|
||||
}),
|
||||
|
||||
servers: protectedProcedure.query(async () => {
|
||||
return await fetchHetznerServers();
|
||||
}),
|
||||
});
|
||||
16
apps/dokploy/server/api/routers/hostinger.ts
Normal file
16
apps/dokploy/server/api/routers/hostinger.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
fetchHostingerCatalog,
|
||||
fetchHostingerDataCenters,
|
||||
} from "@dokploy/server/index";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const hostingerRouter = createTRPCRouter({
|
||||
vpsPlans: protectedProcedure.query(async () => {
|
||||
const catalogItems = await fetchHostingerCatalog();
|
||||
return catalogItems.filter((item) => item?.name?.startsWith("KVM"));
|
||||
}),
|
||||
|
||||
dataCenters: protectedProcedure.query(async () => {
|
||||
return await fetchHostingerDataCenters();
|
||||
}),
|
||||
});
|
||||
@@ -46,8 +46,8 @@ import {
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
@@ -657,7 +657,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
const config = readMainConfig();
|
||||
|
||||
if (!config) return false;
|
||||
const parsedConfig = parse(config) as {
|
||||
const parsedConfig = load(config) as {
|
||||
accessLog?: {
|
||||
filePath: string;
|
||||
};
|
||||
@@ -678,7 +678,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
const mainConfig = readMainConfig();
|
||||
if (!mainConfig) return false;
|
||||
|
||||
const currentConfig = parse(mainConfig) as {
|
||||
const currentConfig = load(mainConfig) as {
|
||||
accessLog?: {
|
||||
filePath: string;
|
||||
};
|
||||
@@ -701,7 +701,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
currentConfig.accessLog = undefined;
|
||||
}
|
||||
|
||||
writeMainConfig(stringify(currentConfig));
|
||||
writeMainConfig(dump(currentConfig));
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -192,16 +192,7 @@ export const userRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(account.userId, ctx.user.id));
|
||||
}
|
||||
|
||||
try {
|
||||
return await updateUser(ctx.user.id, input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message:
|
||||
error instanceof Error ? error.message : "Failed to update user",
|
||||
});
|
||||
}
|
||||
return await updateUser(ctx.user.id, input);
|
||||
}),
|
||||
getUserByToken: publicProcedure
|
||||
.input(apiFindOneToken)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type http from "node:http";
|
||||
import {
|
||||
execAsync,
|
||||
findServerById,
|
||||
IS_CLOUD,
|
||||
validateRequest,
|
||||
} from "@dokploy/server";
|
||||
import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||
import { Client, type ConnectConfig } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
@@ -49,21 +44,6 @@ export const getPublicIpWithFallback = async () => {
|
||||
return ip;
|
||||
};
|
||||
|
||||
export const getLocalServerIp = async () => {
|
||||
try {
|
||||
const command = `ip addr show | grep -E "inet (192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.)" | head -n1 | awk '{print $2}' | cut -d/ -f1`;
|
||||
const { stdout } = await execAsync(command);
|
||||
const ip = stdout.trim();
|
||||
return (
|
||||
ip ||
|
||||
"We were unable to obtain the local server IP, please use your private IP address"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error to obtain local server IP", error);
|
||||
return "We were unable to obtain the local server IP, please use your private IP address";
|
||||
}
|
||||
};
|
||||
|
||||
export const setupTerminalWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { exit } from "node:process";
|
||||
import { execAsync } from "@dokploy/server";
|
||||
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||
@@ -26,8 +25,6 @@ import {
|
||||
await initializeStandaloneTraefik();
|
||||
await initializeRedis();
|
||||
await initializePostgres();
|
||||
console.log("Dokploy setup completed");
|
||||
exit(0);
|
||||
} catch (e) {
|
||||
console.error("Error in dokploy setup", e);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "./queue.js";
|
||||
import { jobQueueSchema } from "./schema.js";
|
||||
import { initializeJobs } from "./utils.js";
|
||||
import { firstWorker, secondWorker, thirdWorker } from "./workers.js";
|
||||
import { firstWorker, secondWorker } from "./workers.js";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -91,7 +91,6 @@ export const gracefulShutdown = async (signal: string) => {
|
||||
logger.warn(`Received ${signal}, closing server...`);
|
||||
await firstWorker.close();
|
||||
await secondWorker.close();
|
||||
await thirdWorker.close();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
|
||||
@@ -7,34 +7,22 @@ import { runJobs } from "./utils.js";
|
||||
export const firstWorker = new Worker(
|
||||
"backupQueue",
|
||||
async (job: Job<QueueJob>) => {
|
||||
logger.info({ data: job.data }, "Running job first worker");
|
||||
logger.info({ data: job.data }, "Running job");
|
||||
await runJobs(job.data);
|
||||
},
|
||||
{
|
||||
concurrency: 100,
|
||||
concurrency: 50,
|
||||
connection,
|
||||
},
|
||||
);
|
||||
export const secondWorker = new Worker(
|
||||
"backupQueue",
|
||||
async (job: Job<QueueJob>) => {
|
||||
logger.info({ data: job.data }, "Running job second worker");
|
||||
logger.info({ data: job.data }, "Running job");
|
||||
await runJobs(job.data);
|
||||
},
|
||||
{
|
||||
concurrency: 100,
|
||||
connection,
|
||||
},
|
||||
);
|
||||
|
||||
export const thirdWorker = new Worker(
|
||||
"backupQueue",
|
||||
async (job: Job<QueueJob>) => {
|
||||
logger.info({ data: job.data }, "Running job third worker");
|
||||
await runJobs(job.data);
|
||||
},
|
||||
{
|
||||
concurrency: 100,
|
||||
concurrency: 50,
|
||||
connection,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"openapi-fetch": "0.14.0",
|
||||
"@ai-sdk/anthropic": "^2.0.5",
|
||||
"@ai-sdk/azure": "^2.0.16",
|
||||
"@ai-sdk/cohere": "^2.0.4",
|
||||
@@ -57,7 +58,7 @@
|
||||
"drizzle-orm": "^0.39.3",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"hi-base32": "^0.5.1",
|
||||
"yaml": "2.8.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
@@ -85,6 +86,7 @@
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/dockerode": "3.3.23",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "^18.19.104",
|
||||
|
||||
@@ -11,9 +11,7 @@ export const bitbucket = pgTable("bitbucket", {
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
bitbucketUsername: text("bitbucketUsername"),
|
||||
bitbucketEmail: text("bitbucketEmail"),
|
||||
appPassword: text("appPassword"),
|
||||
apiToken: text("apiToken"),
|
||||
bitbucketWorkspaceName: text("bitbucketWorkspaceName"),
|
||||
gitProviderId: text("gitProviderId")
|
||||
.notNull()
|
||||
@@ -31,9 +29,7 @@ const createSchema = createInsertSchema(bitbucket);
|
||||
|
||||
export const apiCreateBitbucket = createSchema.extend({
|
||||
bitbucketUsername: z.string().optional(),
|
||||
bitbucketEmail: z.string().email().optional(),
|
||||
appPassword: z.string().optional(),
|
||||
apiToken: z.string().optional(),
|
||||
bitbucketWorkspaceName: z.string().optional(),
|
||||
gitProviderId: z.string().optional(),
|
||||
authId: z.string().min(1),
|
||||
@@ -50,19 +46,9 @@ export const apiBitbucketTestConnection = createSchema
|
||||
.extend({
|
||||
bitbucketId: z.string().min(1),
|
||||
bitbucketUsername: z.string().optional(),
|
||||
bitbucketEmail: z.string().email().optional(),
|
||||
workspaceName: z.string().optional(),
|
||||
apiToken: z.string().optional(),
|
||||
appPassword: z.string().optional(),
|
||||
})
|
||||
.pick({
|
||||
bitbucketId: true,
|
||||
bitbucketUsername: true,
|
||||
bitbucketEmail: true,
|
||||
workspaceName: true,
|
||||
apiToken: true,
|
||||
appPassword: true,
|
||||
});
|
||||
.pick({ bitbucketId: true, bitbucketUsername: true, workspaceName: true });
|
||||
|
||||
export const apiFindBitbucketBranches = z.object({
|
||||
owner: z.string(),
|
||||
@@ -74,9 +60,6 @@ export const apiUpdateBitbucket = createSchema.extend({
|
||||
bitbucketId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
bitbucketUsername: z.string().optional(),
|
||||
bitbucketEmail: z.string().email().optional(),
|
||||
appPassword: z.string().optional(),
|
||||
apiToken: z.string().optional(),
|
||||
bitbucketWorkspaceName: z.string().optional(),
|
||||
organizationId: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -58,5 +58,4 @@ export const apiUpdateGithub = createSchema.extend({
|
||||
githubId: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
gitProviderId: z.string().min(1),
|
||||
githubAppName: z.string().min(1),
|
||||
});
|
||||
|
||||
@@ -322,11 +322,6 @@ export const apiUpdateWebServerMonitoring = z.object({
|
||||
});
|
||||
|
||||
export const apiUpdateUser = createSchema.partial().extend({
|
||||
email: z
|
||||
.string()
|
||||
.email("Please enter a valid email address")
|
||||
.min(1, "Email is required")
|
||||
.optional(),
|
||||
password: z.string().optional(),
|
||||
currentPassword: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
|
||||
@@ -21,6 +21,8 @@ export * from "./services/git-provider";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/hetzner";
|
||||
export * from "./services/hostinger";
|
||||
export * from "./services/mariadb";
|
||||
export * from "./services/mongo";
|
||||
export * from "./services/mount";
|
||||
@@ -76,7 +78,6 @@ export * from "./utils/builders/nixpacks";
|
||||
export * from "./utils/builders/paketo";
|
||||
export * from "./utils/builders/static";
|
||||
export * from "./utils/builders/utils";
|
||||
|
||||
export * from "./utils/cluster/upload";
|
||||
export * from "./utils/databases/rebuild";
|
||||
export * from "./utils/docker/collision";
|
||||
@@ -106,7 +107,6 @@ export * from "./utils/providers/docker";
|
||||
export * from "./utils/providers/git";
|
||||
export * from "./utils/providers/gitea";
|
||||
export * from "./utils/providers/github";
|
||||
export * from "./utils/providers/github";
|
||||
export * from "./utils/providers/gitlab";
|
||||
export * from "./utils/providers/raw";
|
||||
export * from "./utils/schedules/index";
|
||||
|
||||
@@ -92,48 +92,31 @@ export const suggestVariants = async ({
|
||||
|
||||
const { object } = await generateObject({
|
||||
model,
|
||||
output: "object",
|
||||
output: "array",
|
||||
schema: z.object({
|
||||
suggestions: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
shortDescription: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
shortDescription: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
prompt: `
|
||||
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
|
||||
|
||||
Return your response as a JSON object with the following structure:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "project-slug",
|
||||
"name": "Project Name",
|
||||
"shortDescription": "Brief one-line description",
|
||||
"description": "Detailed description"
|
||||
}
|
||||
]
|
||||
}
|
||||
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion
|
||||
should include id, name, shortDescription, and description. Use slug of title for id.
|
||||
|
||||
Important rules for the response:
|
||||
1. Use slug format for the id field (lowercase, hyphenated)
|
||||
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
|
||||
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
||||
4. The shortDescription should be a single-line summary focusing on the main technologies
|
||||
5. All projects should be installable in docker and have docker compose support
|
||||
1. The description field should ONLY contain a plain text description of the project, its features, and use cases
|
||||
2. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
||||
3. The shortDescription should be a single-line summary focusing on the main technologies
|
||||
|
||||
User wants to create a new project with the following details:
|
||||
User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it:
|
||||
|
||||
${input}
|
||||
`,
|
||||
});
|
||||
|
||||
if (object?.suggestions?.length) {
|
||||
if (object?.length) {
|
||||
const result = [];
|
||||
for (const suggestion of object.suggestions) {
|
||||
for (const suggestion of object) {
|
||||
try {
|
||||
const { object: docker } = await generateObject({
|
||||
model,
|
||||
@@ -153,29 +136,16 @@ export const suggestVariants = async ({
|
||||
serviceName: z.string(),
|
||||
}),
|
||||
),
|
||||
configFiles: z
|
||||
.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
filePath: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
configFiles: z.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
filePath: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
prompt: `
|
||||
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
|
||||
|
||||
Return your response as a JSON object with this structure:
|
||||
{
|
||||
"dockerCompose": "yaml string here",
|
||||
"envVariables": [{"name": "VAR_NAME", "value": "example_value"}],
|
||||
"domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}],
|
||||
"configFiles": [{"content": "file content", "filePath": "path/to/file"}]
|
||||
}
|
||||
|
||||
Note: configFiles is optional - only include it if configuration files are absolutely required.
|
||||
|
||||
Follow these rules:
|
||||
Return the docker compose as a YAML string and environment variables configuration. Follow these rules:
|
||||
|
||||
Docker Compose Rules:
|
||||
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker-compose.yml
|
||||
@@ -228,7 +198,6 @@ export const suggestVariants = async ({
|
||||
console.error("Error in docker compose generation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,26 +68,10 @@ export const updateBitbucket = async (
|
||||
input: typeof apiUpdateBitbucket._type,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
// First get the current bitbucket provider to get gitProviderId
|
||||
const currentProvider = await tx.query.bitbucket.findFirst({
|
||||
where: eq(bitbucket.bitbucketId, bitbucketId),
|
||||
});
|
||||
|
||||
if (!currentProvider) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Bitbucket provider not found",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await tx
|
||||
.update(bitbucket)
|
||||
.set({
|
||||
bitbucketUsername: input.bitbucketUsername,
|
||||
bitbucketEmail: input.bitbucketEmail,
|
||||
appPassword: input.appPassword,
|
||||
apiToken: input.apiToken,
|
||||
bitbucketWorkspaceName: input.bitbucketWorkspaceName,
|
||||
...input,
|
||||
})
|
||||
.where(eq(bitbucket.bitbucketId, bitbucketId))
|
||||
.returning();
|
||||
@@ -99,7 +83,7 @@ export const updateBitbucket = async (
|
||||
name: input.name,
|
||||
organizationId: input.organizationId,
|
||||
})
|
||||
.where(eq(gitProvider.gitProviderId, currentProvider.gitProviderId))
|
||||
.where(eq(gitProvider.gitProviderId, input.gitProviderId))
|
||||
.returning();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { stringify } from "yaml";
|
||||
import { dump } from "js-yaml";
|
||||
import type { z } from "zod";
|
||||
import { encodeBase64 } from "../utils/docker/utils";
|
||||
import { execAsyncRemote } from "../utils/process/execAsync";
|
||||
@@ -101,7 +101,7 @@ const createCertificateFiles = async (certificate: Certificate) => {
|
||||
],
|
||||
},
|
||||
};
|
||||
const yamlConfig = stringify(traefikConfig);
|
||||
const yamlConfig = dump(traefikConfig);
|
||||
const configFile = path.join(certDir, "certificate.yml");
|
||||
|
||||
if (certificate.serverId) {
|
||||
|
||||
49
packages/server/src/services/hetzner.ts
Normal file
49
packages/server/src/services/hetzner.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import createClient from "openapi-fetch";
|
||||
import type { paths } from "../types/hetzner-types";
|
||||
|
||||
const HETZNER_API_URL = "https://api.hetzner.cloud/v1";
|
||||
|
||||
const hetznerApiKey = process.env.HETZNER_API_KEY;
|
||||
|
||||
const client = createClient<paths>({ baseUrl: HETZNER_API_URL });
|
||||
|
||||
export const fetchHetznerLocations = async () => {
|
||||
const { data, error } = await client.GET("/locations", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${hetznerApiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch Hetzner locations: ${error}`);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchHetznerServerTypes = async () => {
|
||||
const { data, error } = await client.GET("/server_types", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${hetznerApiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch Hetzner server types: ${error}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchHetznerServers = async () => {
|
||||
const { data, error } = await client.GET("/servers", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${hetznerApiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch Hetzner servers: ${error}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
35
packages/server/src/services/hostinger.ts
Normal file
35
packages/server/src/services/hostinger.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import createClient from "openapi-fetch";
|
||||
import type { paths } from "../types/hostinger-types";
|
||||
|
||||
const HOSTINGER_API_URL = "https://developers.hostinger.com";
|
||||
const hostingerApiKey = process.env.HOSTINGER_API_KEY;
|
||||
|
||||
const client = createClient<paths>({ baseUrl: HOSTINGER_API_URL });
|
||||
|
||||
export const fetchHostingerCatalog = async () => {
|
||||
const { data, error } = await client.GET("/api/billing/v1/catalog", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${hostingerApiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch Hostinger catalog: ${error}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const fetchHostingerDataCenters = async () => {
|
||||
const { data, error } = await client.GET("/api/vps/v1/data-centers", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${hostingerApiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Failed to fetch Hostinger data centers: ${error}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
@@ -10,22 +10,6 @@ import { IS_CLOUD } from "../constants";
|
||||
|
||||
export type Registry = typeof registry.$inferSelect;
|
||||
|
||||
function shEscape(s: string | undefined): string {
|
||||
if (!s) return "''";
|
||||
return `'${s.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function safeDockerLoginCommand(
|
||||
registry: string | undefined,
|
||||
user: string | undefined,
|
||||
pass: string | undefined,
|
||||
) {
|
||||
const escapedRegistry = shEscape(registry);
|
||||
const escapedUser = shEscape(user);
|
||||
const escapedPassword = shEscape(pass);
|
||||
return `printf %s ${escapedPassword} | docker login ${escapedRegistry} -u ${escapedUser} --password-stdin`;
|
||||
}
|
||||
|
||||
export const createRegistry = async (
|
||||
input: typeof apiCreateRegistry._type,
|
||||
organizationId: string,
|
||||
@@ -53,11 +37,7 @@ export const createRegistry = async (
|
||||
message: "Select a server to add the registry",
|
||||
});
|
||||
}
|
||||
const loginCommand = safeDockerLoginCommand(
|
||||
input.registryUrl,
|
||||
input.username,
|
||||
input.password,
|
||||
);
|
||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||
if (input.serverId && input.serverId !== "none") {
|
||||
await execAsyncRemote(input.serverId, loginCommand);
|
||||
} else if (newRegistry.registryType === "cloud") {
|
||||
@@ -111,11 +91,7 @@ export const updateRegistry = async (
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
const loginCommand = safeDockerLoginCommand(
|
||||
response?.registryUrl,
|
||||
response?.username,
|
||||
response?.password,
|
||||
);
|
||||
const loginCommand = `echo ${response?.password} | docker login ${response?.registryUrl} --username ${response?.username} --password-stdin`;
|
||||
|
||||
if (
|
||||
IS_CLOUD &&
|
||||
|
||||
@@ -296,19 +296,6 @@ export const findMemberById = async (
|
||||
};
|
||||
|
||||
export const updateUser = async (userId: string, userData: Partial<User>) => {
|
||||
// Validate email if it's being updated
|
||||
if (userData.email !== undefined) {
|
||||
if (!userData.email || userData.email.trim() === "") {
|
||||
throw new Error("Email is required and cannot be empty");
|
||||
}
|
||||
|
||||
// Basic email format validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(userData.email)) {
|
||||
throw new Error("Please enter a valid email address");
|
||||
}
|
||||
}
|
||||
|
||||
const user = await db
|
||||
.update(users_temp)
|
||||
.set({
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { ContainerCreateOptions, CreateServiceOptions } from "dockerode";
|
||||
import { stringify } from "yaml";
|
||||
import { dump } from "js-yaml";
|
||||
import { paths } from "../constants";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import type { FileConfig } from "../utils/traefik/file-types";
|
||||
@@ -241,7 +234,7 @@ export const createDefaultServerTraefikConfig = () => {
|
||||
},
|
||||
};
|
||||
|
||||
const yamlStr = stringify(config);
|
||||
const yamlStr = dump(config);
|
||||
mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`),
|
||||
@@ -315,7 +308,7 @@ export const getDefaultTraefikConfig = () => {
|
||||
}),
|
||||
};
|
||||
|
||||
const yamlStr = stringify(configObject);
|
||||
const yamlStr = dump(configObject);
|
||||
|
||||
return yamlStr;
|
||||
};
|
||||
@@ -369,7 +362,7 @@ export const getDefaultServerTraefikConfig = () => {
|
||||
},
|
||||
};
|
||||
|
||||
const yamlStr = stringify(configObject);
|
||||
const yamlStr = dump(configObject);
|
||||
|
||||
return yamlStr;
|
||||
};
|
||||
@@ -382,26 +375,13 @@ export const createDefaultTraefikConfig = () => {
|
||||
if (existsSync(acmeJsonPath)) {
|
||||
chmodSync(acmeJsonPath, "600");
|
||||
}
|
||||
|
||||
// Create the traefik directory first
|
||||
mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
|
||||
|
||||
// Check if traefik.yml exists and handle the case where it might be a directory
|
||||
if (existsSync(mainConfig)) {
|
||||
const stats = statSync(mainConfig);
|
||||
if (stats.isDirectory()) {
|
||||
// If traefik.yml is a directory, remove it
|
||||
console.log("Found traefik.yml as directory, removing it...");
|
||||
rmSync(mainConfig, { recursive: true, force: true });
|
||||
} else if (stats.isFile()) {
|
||||
console.log("Main config already exists");
|
||||
return;
|
||||
}
|
||||
console.log("Main config already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
const yamlStr = getDefaultTraefikConfig();
|
||||
mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(mainConfig, yamlStr, "utf8");
|
||||
console.log("Traefik config created successfully");
|
||||
};
|
||||
|
||||
export const getDefaultMiddlewares = () => {
|
||||
@@ -417,7 +397,7 @@ export const getDefaultMiddlewares = () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const yamlStr = stringify(defaultMiddlewares);
|
||||
const yamlStr = dump(defaultMiddlewares);
|
||||
return yamlStr;
|
||||
};
|
||||
export const createDefaultMiddlewares = () => {
|
||||
|
||||
31177
packages/server/src/types/hetzner-types.ts
Normal file
31177
packages/server/src/types/hetzner-types.ts
Normal file
File diff suppressed because it is too large
Load Diff
5377
packages/server/src/types/hostinger-types.ts
Normal file
5377
packages/server/src/types/hostinger-types.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -89,7 +89,7 @@ export const getMariadbBackupCommand = (
|
||||
databaseUser: string,
|
||||
databasePassword: string,
|
||||
) => {
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --single-transaction --quick --databases ${database} | gzip"`;
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mariadb-dump --user='${databaseUser}' --password='${databasePassword}' --databases ${database} | gzip"`;
|
||||
};
|
||||
|
||||
export const getMysqlBackupCommand = (
|
||||
|
||||
@@ -220,8 +220,8 @@ const getImageName = (application: ApplicationNested) => {
|
||||
if (registry) {
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
return registryTag;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ export const uploadImage = async (
|
||||
// For ghcr.io: ghcr.io/username/image:tag
|
||||
// For docker.io: docker.io/username/image:tag
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
? `${registryUrl}/${imagePrefix}/${imageName}`
|
||||
: `${registryUrl}/${username}/${imageName}`;
|
||||
|
||||
try {
|
||||
writeStream.write(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
import { stringify } from "yaml";
|
||||
import { dump } from "js-yaml";
|
||||
import { addAppNameToAllServiceNames } from "./collision/root-network";
|
||||
import { generateRandomHash } from "./compose";
|
||||
import { addSuffixToAllVolumes } from "./compose/volume";
|
||||
@@ -59,7 +59,7 @@ export const randomizeIsolatedDeploymentComposeFile = async (
|
||||
)
|
||||
: composeData;
|
||||
|
||||
return stringify(newComposeFile);
|
||||
return dump(newComposeFile);
|
||||
};
|
||||
|
||||
export const randomizeDeployableSpecificationFile = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import { findComposeById } from "@dokploy/server/services/compose";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { addSuffixToAllConfigs } from "./compose/configs";
|
||||
import { addSuffixToAllNetworks } from "./compose/network";
|
||||
import { addSuffixToAllSecrets } from "./compose/secrets";
|
||||
@@ -18,13 +18,13 @@ export const randomizeComposeFile = async (
|
||||
) => {
|
||||
const compose = await findComposeById(composeId);
|
||||
const composeFile = compose.composeFile;
|
||||
const composeData = parse(composeFile) as ComposeSpecification;
|
||||
const composeData = load(composeFile) as ComposeSpecification;
|
||||
|
||||
const randomSuffix = suffix || generateRandomHash();
|
||||
|
||||
const newComposeFile = addSuffixToAllProperties(composeData, randomSuffix);
|
||||
|
||||
return stringify(newComposeFile);
|
||||
return dump(newComposeFile);
|
||||
};
|
||||
|
||||
export const randomizeSpecificationFile = (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import {
|
||||
cloneRawBitbucketRepository,
|
||||
@@ -92,7 +92,7 @@ export const loadDockerCompose = async (
|
||||
|
||||
if (existsSync(path)) {
|
||||
const yamlStr = readFileSync(path, "utf8");
|
||||
const parsedConfig = parse(yamlStr) as ComposeSpecification;
|
||||
const parsedConfig = load(yamlStr) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
}
|
||||
return null;
|
||||
@@ -115,7 +115,7 @@ export const loadDockerComposeRemote = async (
|
||||
return null;
|
||||
}
|
||||
if (!stdout) return null;
|
||||
const parsedConfig = parse(stdout) as ComposeSpecification;
|
||||
const parsedConfig = load(stdout) as ComposeSpecification;
|
||||
return parsedConfig;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -141,7 +141,7 @@ export const writeDomainsToCompose = async (
|
||||
const composeConverted = await addDomainToCompose(compose, domains);
|
||||
|
||||
const path = getComposePath(compose);
|
||||
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
||||
try {
|
||||
await writeFile(path, composeString, "utf8");
|
||||
} catch (error) {
|
||||
@@ -169,7 +169,7 @@ exit 1;
|
||||
`;
|
||||
}
|
||||
if (compose.serverId) {
|
||||
const composeString = stringify(composeConverted, { lineWidth: 1000 });
|
||||
const composeString = dump(composeConverted, { lineWidth: 1000 });
|
||||
const encodedContent = encodeBase64(composeString);
|
||||
return `echo "${encodedContent}" | base64 -d > "${path}";`;
|
||||
}
|
||||
@@ -251,15 +251,11 @@ export const addDomainToCompose = async (
|
||||
}
|
||||
labels.unshift(...httpLabels);
|
||||
if (!compose.isolatedDeployment) {
|
||||
if (compose.composeType === "docker-compose") {
|
||||
if (!labels.includes("traefik.docker.network=dokploy-network")) {
|
||||
labels.unshift("traefik.docker.network=dokploy-network");
|
||||
}
|
||||
} else {
|
||||
// Stack Case
|
||||
if (!labels.includes("traefik.swarm.network=dokploy-network")) {
|
||||
labels.unshift("traefik.swarm.network=dokploy-network");
|
||||
}
|
||||
if (!labels.includes("traefik.docker.network=dokploy-network")) {
|
||||
labels.unshift("traefik.docker.network=dokploy-network");
|
||||
}
|
||||
if (!labels.includes("traefik.swarm.network=dokploy-network")) {
|
||||
labels.unshift("traefik.swarm.network=dokploy-network");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +283,7 @@ export const writeComposeFile = async (
|
||||
const path = getComposePath(compose);
|
||||
|
||||
try {
|
||||
const composeFile = stringify(composeSpec, {
|
||||
const composeFile = dump(composeSpec, {
|
||||
lineWidth: 1000,
|
||||
});
|
||||
fs.writeFileSync(path, composeFile, "utf8");
|
||||
|
||||
@@ -5,10 +5,7 @@ import type {
|
||||
apiBitbucketTestConnection,
|
||||
apiFindBitbucketBranches,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import {
|
||||
type Bitbucket,
|
||||
findBitbucketById,
|
||||
} from "@dokploy/server/services/bitbucket";
|
||||
import { findBitbucketById } from "@dokploy/server/services/bitbucket";
|
||||
import type { Compose } from "@dokploy/server/services/compose";
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -26,39 +23,6 @@ export type ComposeWithBitbucket = InferResultType<
|
||||
{ bitbucket: true }
|
||||
>;
|
||||
|
||||
export const getBitbucketCloneUrl = (
|
||||
bitbucketProvider: {
|
||||
apiToken?: string | null;
|
||||
bitbucketUsername?: string | null;
|
||||
appPassword?: string | null;
|
||||
} | null,
|
||||
repoClone: string,
|
||||
) => {
|
||||
if (!bitbucketProvider) {
|
||||
throw new Error("Bitbucket provider is required");
|
||||
}
|
||||
return bitbucketProvider.apiToken
|
||||
? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}`
|
||||
: `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
|
||||
};
|
||||
|
||||
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
|
||||
if (bitbucketProvider.apiToken) {
|
||||
// For API tokens, use HTTP Basic auth with email and token
|
||||
// According to Bitbucket docs: email:token for API calls
|
||||
const email =
|
||||
bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername;
|
||||
return {
|
||||
Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`,
|
||||
};
|
||||
}
|
||||
|
||||
// For app passwords, use HTTP Basic auth with username and app password
|
||||
return {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
};
|
||||
};
|
||||
|
||||
export const cloneBitbucketRepository = async (
|
||||
entity: ApplicationWithBitbucket | ComposeWithBitbucket,
|
||||
logPath: string,
|
||||
@@ -87,7 +51,7 @@ export const cloneBitbucketRepository = async (
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
|
||||
const cloneUrl = `https://${bitbucket?.bitbucketUsername}:${bitbucket?.appPassword}@${repoclone}`;
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
@@ -139,7 +103,7 @@ export const cloneRawBitbucketRepository = async (entity: Compose) => {
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
const cloneArgs = [
|
||||
@@ -189,7 +153,7 @@ export const cloneRawBitbucketRepositoryRemote = async (compose: Compose) => {
|
||||
const basePath = COMPOSE_PATH;
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
try {
|
||||
const cloneCommand = `
|
||||
@@ -242,7 +206,7 @@ export const getBitbucketCloneCommand = async (
|
||||
const outputPath = join(basePath, appName, "code");
|
||||
await recreateDirectory(outputPath);
|
||||
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`;
|
||||
const cloneUrl = getBitbucketCloneUrl(bitbucketProvider, repoclone);
|
||||
const cloneUrl = `https://${bitbucketProvider?.bitbucketUsername}:${bitbucketProvider?.appPassword}@${repoclone}`;
|
||||
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
@@ -277,7 +241,9 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: getBitbucketHeaders(bitbucketProvider),
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -313,43 +279,35 @@ export const getBitbucketBranches = async (
|
||||
}
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
const { owner, repo } = input;
|
||||
let url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=1`;
|
||||
let allBranches: {
|
||||
name: string;
|
||||
commit: {
|
||||
sha: string;
|
||||
};
|
||||
}[] = [];
|
||||
const url = `https://api.bitbucket.org/2.0/repositories/${owner}/${repo}/refs/branches?pagelen=100`;
|
||||
|
||||
try {
|
||||
while (url) {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: getBitbucketHeaders(bitbucketProvider),
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `HTTP error! status: ${response.status}`,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `HTTP error! status: ${response.status}`,
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((branch: any) => {
|
||||
return {
|
||||
name: branch.name,
|
||||
commit: {
|
||||
sha: branch.target.hash,
|
||||
},
|
||||
};
|
||||
});
|
||||
allBranches = allBranches.concat(mappedData);
|
||||
url = data.next || null;
|
||||
}
|
||||
|
||||
return allBranches as {
|
||||
const data = await response.json();
|
||||
|
||||
const mappedData = data.values.map((branch: any) => {
|
||||
return {
|
||||
name: branch.name,
|
||||
commit: {
|
||||
sha: branch.target.hash,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData as {
|
||||
name: string;
|
||||
commit: {
|
||||
sha: string;
|
||||
@@ -377,7 +335,9 @@ export const testBitbucketConnection = async (
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: getBitbucketHeaders(bitbucketProvider),
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -362,22 +362,17 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
|
||||
}
|
||||
|
||||
const baseUrl = provider.giteaUrl.replace(/\/+$/, "");
|
||||
|
||||
// Use /user/repos to get authenticated user's repositories with pagination
|
||||
const limit = 30;
|
||||
let allRepos = 0;
|
||||
let page = 1;
|
||||
const limit = 50; // Max per page
|
||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${provider.accessToken}`,
|
||||
},
|
||||
while (nextUrl) {
|
||||
const response = await fetch(nextUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${provider.accessToken}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
@@ -386,18 +381,22 @@ export const testGiteaConnection = async (input: { giteaId: string }) => {
|
||||
}
|
||||
|
||||
const repos = await response.json();
|
||||
if (!Array.isArray(repos) || repos.length === 0) {
|
||||
break; // No more repositories
|
||||
allRepos += repos.data.length;
|
||||
|
||||
const linkHeader = response.headers.get("link");
|
||||
nextUrl = "";
|
||||
|
||||
if (linkHeader) {
|
||||
const nextLink = linkHeader
|
||||
.split(",")
|
||||
.find((link) => link.includes('rel="next"'));
|
||||
if (nextLink) {
|
||||
const matches = nextLink.match(/<([^>]+)>/);
|
||||
if (matches?.[1]) {
|
||||
nextUrl = matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allRepos += repos.length;
|
||||
|
||||
// Check if there are more pages
|
||||
if (repos.length < limit) {
|
||||
break; // Last page (fewer results than limit)
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
await updateGitea(giteaId, {
|
||||
@@ -419,22 +418,17 @@ export const getGiteaRepositories = async (giteaId?: string) => {
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
|
||||
// Use /user/repos to get authenticated user's repositories with pagination
|
||||
const limit = 30;
|
||||
let allRepositories: any[] = [];
|
||||
let page = 1;
|
||||
const limit = 50; // Max per page
|
||||
let nextUrl = `${baseUrl}/api/v1/repos/search?limit=${limit}`;
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/v1/user/repos?page=${page}&limit=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
while (nextUrl) {
|
||||
const response = await fetch(nextUrl, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new TRPCError({
|
||||
@@ -443,19 +437,23 @@ export const getGiteaRepositories = async (giteaId?: string) => {
|
||||
});
|
||||
}
|
||||
|
||||
const repos = await response.json();
|
||||
if (!Array.isArray(repos) || repos.length === 0) {
|
||||
break; // No more repositories
|
||||
const result = await response.json();
|
||||
allRepositories = [...allRepositories, ...result.data];
|
||||
|
||||
const linkHeader = response.headers.get("link");
|
||||
nextUrl = "";
|
||||
|
||||
if (linkHeader) {
|
||||
const nextLink = linkHeader
|
||||
.split(",")
|
||||
.find((link) => link.includes('rel="next"'));
|
||||
if (nextLink) {
|
||||
const matches = nextLink.match(/<([^>]+)>/);
|
||||
if (matches?.[1]) {
|
||||
nextUrl = matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allRepositories = [...allRepositories, ...repos];
|
||||
|
||||
// Check if there are more pages
|
||||
if (repos.length < limit) {
|
||||
break; // Last page (fewer results than limit)
|
||||
}
|
||||
|
||||
page++;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -484,43 +482,25 @@ export const getGiteaBranches = async (input: {
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
|
||||
const baseUrl = giteaProvider.giteaUrl.replace(/\/+$/, "");
|
||||
const url = `${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches`;
|
||||
|
||||
// Handle pagination for branches
|
||||
let allBranches: any[] = [];
|
||||
let page = 1;
|
||||
const limit = 50; // Max per page
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
while (true) {
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/v1/repos/${input.owner}/${input.repo}/branches?page=${page}&limit=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `token ${giteaProvider.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const branches = await response.json();
|
||||
if (!Array.isArray(branches) || branches.length === 0) {
|
||||
break; // No more branches
|
||||
}
|
||||
|
||||
allBranches = [...allBranches, ...branches];
|
||||
|
||||
// Check if there are more pages
|
||||
if (branches.length < limit) {
|
||||
break; // Last page (fewer results than limit)
|
||||
}
|
||||
|
||||
page++;
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch branches: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return allBranches?.map((branch: any) => ({
|
||||
const branches = await response.json();
|
||||
|
||||
if (!branches) {
|
||||
return [];
|
||||
}
|
||||
return branches?.map((branch: any) => ({
|
||||
id: branch.name,
|
||||
name: branch.name,
|
||||
commit: {
|
||||
|
||||
@@ -171,7 +171,7 @@ export const cloneGithubRepository = async ({
|
||||
const cloneUrl = `https://oauth2:${token}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
|
||||
@@ -401,7 +401,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
const {
|
||||
appName,
|
||||
gitlabPathNamespace,
|
||||
gitlabBranch,
|
||||
branch,
|
||||
gitlabId,
|
||||
serverId,
|
||||
enableSubmodules,
|
||||
@@ -429,7 +429,7 @@ export const cloneRawGitlabRepositoryRemote = async (compose: Compose) => {
|
||||
try {
|
||||
const command = `
|
||||
rm -rf ${outputPath};
|
||||
git clone --branch ${gitlabBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath}
|
||||
`;
|
||||
await execAsyncRemote(serverId, command);
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { encodeBase64 } from "../docker/utils";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import type { FileConfig, HttpLoadBalancerService } from "./file-types";
|
||||
@@ -40,7 +40,7 @@ export const createTraefikConfig = (appName: string) => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const yamlStr = stringify(config);
|
||||
const yamlStr = dump(config);
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
fs.mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true });
|
||||
writeFileSync(
|
||||
@@ -87,7 +87,7 @@ export const loadOrCreateConfig = (appName: string): FileConfig => {
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
if (fs.existsSync(configPath)) {
|
||||
const yamlStr = fs.readFileSync(configPath, "utf8");
|
||||
const parsedConfig = (parse(yamlStr) as FileConfig) || {
|
||||
const parsedConfig = (load(yamlStr) as FileConfig) || {
|
||||
http: { routers: {}, services: {} },
|
||||
};
|
||||
return parsedConfig;
|
||||
@@ -107,7 +107,7 @@ export const loadOrCreateConfigRemote = async (
|
||||
|
||||
if (!stdout) return fileConfig;
|
||||
|
||||
const parsedConfig = (parse(stdout) as FileConfig) || {
|
||||
const parsedConfig = (load(stdout) as FileConfig) || {
|
||||
http: { routers: {}, services: {} },
|
||||
};
|
||||
return parsedConfig;
|
||||
@@ -248,7 +248,7 @@ export const writeTraefikConfig = (
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
const yamlStr = stringify(traefikConfig);
|
||||
const yamlStr = dump(traefikConfig);
|
||||
fs.writeFileSync(configPath, yamlStr, "utf8");
|
||||
} catch (e) {
|
||||
console.error("Error saving the YAML config file:", e);
|
||||
@@ -263,7 +263,7 @@ export const writeTraefikConfigRemote = async (
|
||||
try {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths(true);
|
||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`);
|
||||
const yamlStr = stringify(traefikConfig);
|
||||
const yamlStr = dump(traefikConfig);
|
||||
await execAsyncRemote(serverId, `echo '${yamlStr}' > ${configPath}`);
|
||||
} catch (e) {
|
||||
console.error("Error saving the YAML config file:", e);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { Domain } from "@dokploy/server/services/domain";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { dump, load } from "js-yaml";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import { execAsyncRemote } from "../process/execAsync";
|
||||
import { writeTraefikConfigRemote } from "./application";
|
||||
@@ -76,7 +76,7 @@ export const loadMiddlewares = <T>() => {
|
||||
throw new Error(`File not found: ${configPath}`);
|
||||
}
|
||||
const yamlStr = readFileSync(configPath, "utf8");
|
||||
const config = parse(yamlStr) as T;
|
||||
const config = load(yamlStr) as T;
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -94,7 +94,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => {
|
||||
console.error(`Error: ${stderr}`);
|
||||
throw new Error(`File not found: ${configPath}`);
|
||||
}
|
||||
const config = parse(stdout) as FileConfig;
|
||||
const config = load(stdout) as FileConfig;
|
||||
return config;
|
||||
} catch (_) {
|
||||
throw new Error(`File not found: ${configPath}`);
|
||||
@@ -103,7 +103,7 @@ export const loadRemoteMiddlewares = async (serverId: string) => {
|
||||
export const writeMiddleware = <T>(config: T) => {
|
||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||
const configPath = join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml");
|
||||
const newYamlContent = stringify(config);
|
||||
const newYamlContent = dump(config);
|
||||
writeFileSync(configPath, newYamlContent, "utf8");
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import type { User } from "@dokploy/server/services/user";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { dump, load } from "js-yaml";
|
||||
import {
|
||||
loadOrCreateConfig,
|
||||
removeTraefikConfig,
|
||||
@@ -79,13 +79,13 @@ export const updateLetsEncryptEmail = (newEmail: string | null) => {
|
||||
const { MAIN_TRAEFIK_PATH } = paths();
|
||||
const configPath = join(MAIN_TRAEFIK_PATH, "traefik.yml");
|
||||
const configContent = readFileSync(configPath, "utf8");
|
||||
const config = parse(configContent) as MainTraefikConfig;
|
||||
const config = load(configContent) as MainTraefikConfig;
|
||||
if (config?.certificatesResolvers?.letsencrypt?.acme) {
|
||||
config.certificatesResolvers.letsencrypt.acme.email = newEmail;
|
||||
} else {
|
||||
throw new Error("Invalid Let's Encrypt configuration structure.");
|
||||
}
|
||||
const newYamlContent = stringify(config);
|
||||
const newYamlContent = dump(config);
|
||||
writeFileSync(configPath, newYamlContent, "utf8");
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import {
|
||||
createDeploymentVolumeBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||
import { backupVolume } from "./backup";
|
||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||
|
||||
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||
@@ -78,20 +76,7 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const volumeBackupPath = path.join(
|
||||
VOLUME_BACKUPS_PATH,
|
||||
volumeBackup.appName,
|
||||
);
|
||||
// delete all the .tar files
|
||||
const command = `rm -rf ${volumeBackupPath}/*.tar`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
209
pnpm-lock.yaml
generated
209
pnpm-lock.yaml
generated
@@ -325,6 +325,9 @@ importers:
|
||||
js-cookie:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5
|
||||
js-yaml:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
lodash:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
@@ -445,9 +448,6 @@ importers:
|
||||
xterm-addon-fit:
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0(xterm@5.3.0)
|
||||
yaml:
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
zod:
|
||||
specifier: ^3.25.32
|
||||
version: 3.25.32
|
||||
@@ -464,6 +464,9 @@ importers:
|
||||
'@types/js-cookie':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/js-yaml':
|
||||
specifier: 4.0.9
|
||||
version: 4.0.9
|
||||
'@types/lodash':
|
||||
specifier: 4.17.4
|
||||
version: 4.17.4
|
||||
@@ -515,6 +518,9 @@ importers:
|
||||
memfs:
|
||||
specifier: ^4.17.2
|
||||
version: 4.17.2
|
||||
openapi-typescript:
|
||||
specifier: 7.8.0
|
||||
version: 7.8.0(typescript@5.8.3)
|
||||
tailwindcss:
|
||||
specifier: ^3.4.17
|
||||
version: 3.4.17
|
||||
@@ -678,6 +684,9 @@ importers:
|
||||
hi-base32:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1
|
||||
js-yaml:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
lodash:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
@@ -702,6 +711,9 @@ importers:
|
||||
octokit:
|
||||
specifier: 3.1.2
|
||||
version: 3.1.2
|
||||
openapi-fetch:
|
||||
specifier: 0.14.0
|
||||
version: 0.14.0
|
||||
otpauth:
|
||||
specifier: ^9.4.0
|
||||
version: 9.4.0
|
||||
@@ -741,9 +753,6 @@ importers:
|
||||
ws:
|
||||
specifier: 8.16.0
|
||||
version: 8.16.0
|
||||
yaml:
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
zod:
|
||||
specifier: ^3.25.32
|
||||
version: 3.25.32
|
||||
@@ -757,6 +766,9 @@ importers:
|
||||
'@types/dockerode':
|
||||
specifier: 3.3.23
|
||||
version: 3.3.23
|
||||
'@types/js-yaml':
|
||||
specifier: 4.0.9
|
||||
version: 4.0.9
|
||||
'@types/lodash':
|
||||
specifier: 4.17.4
|
||||
version: 4.17.4
|
||||
@@ -3564,6 +3576,16 @@ packages:
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
|
||||
|
||||
'@redocly/config@0.22.2':
|
||||
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
|
||||
|
||||
'@redocly/openapi-core@1.34.3':
|
||||
resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==}
|
||||
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.41.1':
|
||||
resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==}
|
||||
cpu: [arm]
|
||||
@@ -3959,6 +3981,9 @@ packages:
|
||||
'@types/js-cookie@3.0.6':
|
||||
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
|
||||
|
||||
'@types/js-yaml@4.0.9':
|
||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||
|
||||
'@types/jsonwebtoken@9.0.9':
|
||||
resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==}
|
||||
|
||||
@@ -4185,6 +4210,10 @@ packages:
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-escapes@7.0.0:
|
||||
resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4431,6 +4460,9 @@ packages:
|
||||
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
change-case@5.4.4:
|
||||
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
||||
|
||||
character-entities-html4@2.1.0:
|
||||
resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
|
||||
|
||||
@@ -4544,6 +4576,9 @@ packages:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
colorette@1.4.0:
|
||||
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
@@ -5453,6 +5488,10 @@ packages:
|
||||
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
index-to-position@1.1.0:
|
||||
resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
inflation@2.1.0:
|
||||
resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5667,6 +5706,10 @@ packages:
|
||||
js-file-download@0.4.12:
|
||||
resolution: {integrity: sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==}
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -6122,6 +6165,10 @@ packages:
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
minimatch@5.1.6:
|
||||
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@7.4.6:
|
||||
resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6373,6 +6420,9 @@ packages:
|
||||
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
openapi-fetch@0.14.0:
|
||||
resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==}
|
||||
|
||||
openapi-path-templating@2.2.1:
|
||||
resolution: {integrity: sha512-eN14VrDvl/YyGxxrkGOHkVkWEoPyhyeydOUrbvjoz8K5eIGgELASwN1eqFOJ2CTQMGCy2EntOK1KdtJ8ZMekcg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -6384,6 +6434,15 @@ packages:
|
||||
openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
openapi-typescript@7.8.0:
|
||||
resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.x
|
||||
|
||||
otpauth@9.4.0:
|
||||
resolution: {integrity: sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==}
|
||||
|
||||
@@ -6432,6 +6491,10 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse-json@8.3.0:
|
||||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
parseley@0.12.1:
|
||||
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
|
||||
|
||||
@@ -6534,6 +6597,10 @@ packages:
|
||||
resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -7283,6 +7350,10 @@ packages:
|
||||
resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
supports-color@10.0.0:
|
||||
resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7469,6 +7540,10 @@ packages:
|
||||
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
type-is@1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -7541,6 +7616,9 @@ packages:
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
uri-js-replace@1.0.1:
|
||||
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
|
||||
|
||||
url-parse@1.5.10:
|
||||
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
|
||||
|
||||
@@ -7769,16 +7847,14 @@ packages:
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yaml-ast-parser@0.0.43:
|
||||
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
|
||||
|
||||
yaml@2.8.0:
|
||||
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.8.1:
|
||||
resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -10609,6 +10685,29 @@ snapshots:
|
||||
dependencies:
|
||||
'@redis/client': 1.6.0
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
uri-js-replace: 1.0.1
|
||||
|
||||
'@redocly/config@0.22.2': {}
|
||||
|
||||
'@redocly/openapi-core@1.34.3(supports-color@10.0.0)':
|
||||
dependencies:
|
||||
'@redocly/ajv': 8.11.2
|
||||
'@redocly/config': 0.22.2
|
||||
colorette: 1.4.0
|
||||
https-proxy-agent: 7.0.6(supports-color@10.0.0)
|
||||
js-levenshtein: 1.1.6
|
||||
js-yaml: 4.1.0
|
||||
minimatch: 5.1.6
|
||||
pluralize: 8.0.0
|
||||
yaml-ast-parser: 0.0.43
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.41.1':
|
||||
optional: true
|
||||
|
||||
@@ -11219,6 +11318,8 @@ snapshots:
|
||||
|
||||
'@types/js-cookie@3.0.6': {}
|
||||
|
||||
'@types/js-yaml@4.0.9': {}
|
||||
|
||||
'@types/jsonwebtoken@9.0.9':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@@ -11443,7 +11544,7 @@ snapshots:
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11487,6 +11588,8 @@ snapshots:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-escapes@7.0.0:
|
||||
dependencies:
|
||||
environment: 1.1.0
|
||||
@@ -11762,6 +11865,8 @@ snapshots:
|
||||
|
||||
chalk@5.4.1: {}
|
||||
|
||||
change-case@5.4.4: {}
|
||||
|
||||
character-entities-html4@2.1.0: {}
|
||||
|
||||
character-entities-legacy@1.1.4: {}
|
||||
@@ -11885,6 +11990,8 @@ snapshots:
|
||||
color-string: 1.9.1
|
||||
optional: true
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
@@ -12057,9 +12164,11 @@ snapshots:
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
debug@4.4.1:
|
||||
debug@4.4.1(supports-color@10.0.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.0.0
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
@@ -12131,7 +12240,7 @@ snapshots:
|
||||
|
||||
docker-modem@5.0.6:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
readable-stream: 3.6.2
|
||||
split-ca: 1.0.1
|
||||
ssh2: 1.15.0
|
||||
@@ -12269,7 +12378,7 @@ snapshots:
|
||||
|
||||
esbuild-register@3.6.0(esbuild@0.19.12):
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
esbuild: 0.19.12
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -12516,7 +12625,7 @@ snapshots:
|
||||
gaxios@6.7.1:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
https-proxy-agent: 7.0.6(supports-color@10.0.0)
|
||||
is-stream: 2.0.1
|
||||
node-fetch: 2.7.0
|
||||
uuid: 9.0.1
|
||||
@@ -12536,7 +12645,7 @@ snapshots:
|
||||
gel@2.1.0:
|
||||
dependencies:
|
||||
'@petamoriken/float16': 3.9.2
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
env-paths: 3.0.0
|
||||
semver: 7.7.2
|
||||
shell-quote: 1.8.2
|
||||
@@ -12763,14 +12872,14 @@ snapshots:
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
https-proxy-agent@7.0.6(supports-color@10.0.0):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12814,6 +12923,8 @@ snapshots:
|
||||
|
||||
indent-string@5.0.0: {}
|
||||
|
||||
index-to-position@1.1.0: {}
|
||||
|
||||
inflation@2.1.0: {}
|
||||
|
||||
inflight@1.0.6:
|
||||
@@ -12845,7 +12956,7 @@ snapshots:
|
||||
canonicalize: 1.0.8
|
||||
chalk: 4.1.2
|
||||
cross-fetch: 4.1.0
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
hash.js: 1.1.7
|
||||
json-stringify-safe: 5.0.1
|
||||
ms: 2.1.3
|
||||
@@ -12877,7 +12988,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.2.0
|
||||
cluster-key-slot: 1.1.2
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
denque: 2.1.0
|
||||
lodash.defaults: 4.2.0
|
||||
lodash.isarguments: 3.1.0
|
||||
@@ -12994,6 +13105,8 @@ snapshots:
|
||||
|
||||
js-file-download@0.4.12: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-tokens@9.0.1: {}
|
||||
@@ -13193,7 +13306,7 @@ snapshots:
|
||||
dependencies:
|
||||
chalk: 5.4.1
|
||||
commander: 13.1.0
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
execa: 8.0.1
|
||||
lilconfig: 3.1.3
|
||||
listr2: 8.3.3
|
||||
@@ -13544,7 +13657,7 @@ snapshots:
|
||||
micromark@4.0.2:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
decode-named-character-reference: 1.1.0
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
@@ -13594,6 +13707,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
|
||||
minimatch@5.1.6:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minimatch@7.4.6:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
@@ -13835,6 +13952,10 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-function: 5.0.1
|
||||
|
||||
openapi-fetch@0.14.0:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
openapi-path-templating@2.2.1:
|
||||
dependencies:
|
||||
apg-lite: 1.0.4
|
||||
@@ -13845,6 +13966,18 @@ snapshots:
|
||||
|
||||
openapi-types@12.1.3: {}
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
openapi-typescript@7.8.0(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@redocly/openapi-core': 1.34.3(supports-color@10.0.0)
|
||||
ansi-colors: 4.1.3
|
||||
change-case: 5.4.4
|
||||
parse-json: 8.3.0
|
||||
supports-color: 10.0.0
|
||||
typescript: 5.8.3
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
otpauth@9.4.0:
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
@@ -13905,6 +14038,12 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
index-to-position: 1.1.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
parseley@0.12.1:
|
||||
dependencies:
|
||||
leac: 0.6.0
|
||||
@@ -14009,6 +14148,8 @@ snapshots:
|
||||
dependencies:
|
||||
queue-lit: 1.5.2
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.5.3):
|
||||
@@ -14026,7 +14167,7 @@ snapshots:
|
||||
postcss-load-config@4.0.2(postcss@8.5.3):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
yaml: 2.8.1
|
||||
yaml: 2.8.0
|
||||
optionalDependencies:
|
||||
postcss: 8.5.3
|
||||
|
||||
@@ -14460,7 +14601,7 @@ snapshots:
|
||||
|
||||
require-in-the-middle@7.5.2:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
module-details-from-path: 1.0.4
|
||||
resolve: 1.22.10
|
||||
transitivePeerDependencies:
|
||||
@@ -14803,6 +14944,8 @@ snapshots:
|
||||
dependencies:
|
||||
copy-anything: 3.0.5
|
||||
|
||||
supports-color@10.0.0: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
@@ -15048,6 +15191,8 @@ snapshots:
|
||||
|
||||
type-fest@2.19.0: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
type-is@1.6.18:
|
||||
dependencies:
|
||||
media-typer: 0.3.0
|
||||
@@ -15123,6 +15268,8 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
uri-js-replace@1.0.1: {}
|
||||
|
||||
url-parse@1.5.10:
|
||||
dependencies:
|
||||
querystringify: 2.2.0
|
||||
@@ -15187,7 +15334,7 @@ snapshots:
|
||||
vite-node@1.6.1(@types/node@18.19.104):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.1.1
|
||||
vite: 5.4.19(@types/node@18.19.104)
|
||||
@@ -15204,7 +15351,7 @@ snapshots:
|
||||
|
||||
vite-tsconfig-paths@4.3.2(typescript@5.8.3)(vite@5.4.19(@types/node@18.19.104)):
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.6(typescript@5.8.3)
|
||||
optionalDependencies:
|
||||
@@ -15231,7 +15378,7 @@ snapshots:
|
||||
'@vitest/utils': 1.6.1
|
||||
acorn-walk: 8.3.4
|
||||
chai: 4.5.0
|
||||
debug: 4.4.1
|
||||
debug: 4.4.1(supports-color@10.0.0)
|
||||
execa: 8.0.1
|
||||
local-pkg: 0.5.1
|
||||
magic-string: 0.30.17
|
||||
@@ -15345,9 +15492,9 @@ snapshots:
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yaml@2.8.0: {}
|
||||
yaml-ast-parser@0.0.43: {}
|
||||
|
||||
yaml@2.8.1: {}
|
||||
yaml@2.8.0: {}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user