mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 06:05:25 +02:00
Compare commits
10 Commits
copilot/fi
...
3301-volum
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d59b71d636 | ||
|
|
bcab150946 | ||
|
|
0b446d1e2c | ||
|
|
0550ab7ed3 | ||
|
|
02d99798d4 | ||
|
|
ff0b8ffdb7 | ||
|
|
792bf696a6 | ||
|
|
ed36f37dc9 | ||
|
|
125817e9f3 | ||
|
|
04f056eaf0 |
@@ -206,38 +206,4 @@ describe("getRegistryTag", () => {
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("special characters in username", () => {
|
||||
it("should handle Harbor robot account username with $ (e.g. robot$library+dokploy)", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "robot$library+dokploy",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("docker.io/robot$library+dokploy/nginx");
|
||||
});
|
||||
|
||||
it("should handle username with $ and other special characters", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "robot$test+app",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myapp:latest");
|
||||
expect(result).toBe("docker.io/robot$test+app/myapp:latest");
|
||||
});
|
||||
|
||||
it("should handle username with multiple $ symbols", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "user$name$test",
|
||||
});
|
||||
const result = getRegistryTag(registry, "app");
|
||||
expect(result).toBe("docker.io/user$name$test/app");
|
||||
});
|
||||
|
||||
it("should handle username with + and - symbols", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "robot+test-user",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx:latest");
|
||||
expect(result).toBe("docker.io/robot+test-user/nginx:latest");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import { createDomainLabels } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
/**
|
||||
* Regression tests for Traefik Host rule label format.
|
||||
|
||||
332
apps/dokploy/__test__/volume-backups/volume-backup.real.test.ts
Normal file
332
apps/dokploy/__test__/volume-backups/volume-backup.real.test.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { execAsync } from "@dokploy/server/utils/process/execAsync";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const REAL_TEST_TIMEOUT = 300000;
|
||||
|
||||
// Mock ONLY database and notifications
|
||||
vi.mock("@dokploy/server/db", () => ({
|
||||
db: {
|
||||
query: { volumeBackups: { findFirst: vi.fn() } },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/volume-backups", () => ({
|
||||
findVolumeBackupById: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/destination", () => ({
|
||||
findDestinationById: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/application", () => ({
|
||||
findApplicationById: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/deployment", () => ({
|
||||
createDeploymentVolumeBackup: vi.fn(),
|
||||
updateDeploymentStatus: vi.fn(),
|
||||
}));
|
||||
|
||||
import type * as volumeBackupService from "@dokploy/server/services/volume-backups";
|
||||
import { backupVolume } from "@dokploy/server/utils/volume-backups/backup";
|
||||
|
||||
type VolumeBackupData = Awaited<
|
||||
ReturnType<typeof volumeBackupService.findVolumeBackupById>
|
||||
>;
|
||||
|
||||
const createMockDestination = () => ({
|
||||
destinationId: "test-dest",
|
||||
bucket: "test-bucket",
|
||||
accessKey: "key",
|
||||
secretAccessKey: "secret",
|
||||
region: "us-east-1",
|
||||
endpoint: "s3.amazonaws.com",
|
||||
});
|
||||
|
||||
const createMockVolumeBackup = (volumeName: string, appName: string): any => ({
|
||||
volumeBackupId: "id",
|
||||
name: "Test",
|
||||
volumeName,
|
||||
appName,
|
||||
serviceType: "application",
|
||||
turnOff: false,
|
||||
prefix: "backups/",
|
||||
destination: createMockDestination() as any,
|
||||
application: { appName, serverId: null } as any,
|
||||
compose: null,
|
||||
});
|
||||
|
||||
async function cleanupDocker(volumeName: string) {
|
||||
try {
|
||||
await execAsync(`docker volume rm ${volumeName} 2>/dev/null || true`);
|
||||
console.log(`✅ Cleaned up volume: ${volumeName}`);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupFiles(appName: string) {
|
||||
try {
|
||||
const { LOGS_PATH, VOLUME_BACKUPS_PATH } = paths(false);
|
||||
|
||||
// Clean logs
|
||||
const logPath = path.join(LOGS_PATH, appName);
|
||||
await execAsync(`rm -rf "${logPath}" 2>/dev/null || true`);
|
||||
|
||||
// Clean volume backups directory
|
||||
const backupPath = path.join(VOLUME_BACKUPS_PATH, appName);
|
||||
await execAsync(`rm -rf "${backupPath}" 2>/dev/null || true`);
|
||||
|
||||
console.log(`✅ Cleaned up files for ${appName}`);
|
||||
} catch (error) {
|
||||
console.error(`⚠️ Error during cleanup for ${appName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
describe(
|
||||
"Volume Backups - REAL Tests",
|
||||
() => {
|
||||
let currentVolumeName: string;
|
||||
let currentAppName: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
currentVolumeName = `test-vol-${Date.now()}`;
|
||||
currentAppName = `test-backup-${Date.now()}`;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
console.log(`\n🧹 Cleanup: ${currentVolumeName}`);
|
||||
await cleanupDocker(currentVolumeName);
|
||||
await cleanupFiles(currentAppName);
|
||||
|
||||
console.log("✅ Cleanup done\n");
|
||||
});
|
||||
|
||||
it(
|
||||
"should backup volume with tar ",
|
||||
async () => {
|
||||
console.log(`\n🚀 Test backup: ${currentVolumeName}`);
|
||||
|
||||
// Create volume with data
|
||||
await execAsync(`docker volume create ${currentVolumeName}`);
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu bash -c "
|
||||
echo 'test' > /data/file.txt
|
||||
mkdir -p /data/dir
|
||||
echo 'nested' > /data/dir/nested.txt
|
||||
"
|
||||
`);
|
||||
console.log("✅ Volume created with data");
|
||||
|
||||
// Backup using tar (simulating what backupVolume does)
|
||||
const backupVol = `backup-${Date.now()}`;
|
||||
await execAsync(`docker volume create ${backupVol}`);
|
||||
|
||||
try {
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/source -v ${backupVol}:/backup ubuntu bash -c "
|
||||
cd /source && tar cf /backup/test.tar .
|
||||
"
|
||||
`);
|
||||
console.log("✅ Backup created");
|
||||
|
||||
// Verify tar contains files
|
||||
const { stdout } = await execAsync(`
|
||||
docker run --rm -v ${backupVol}:/backup ubuntu tar -tf /backup/test.tar
|
||||
`);
|
||||
expect(stdout).toContain("file.txt");
|
||||
expect(stdout).toContain("dir/nested.txt");
|
||||
console.log("✅ Backup verified");
|
||||
} finally {
|
||||
await execAsync(`docker volume rm ${backupVol} 2>/dev/null || true`);
|
||||
}
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should verify backup command has proper logging",
|
||||
async () => {
|
||||
console.log(`\n🚀 Test logging: ${currentVolumeName}`);
|
||||
|
||||
const mock = createMockVolumeBackup(currentVolumeName, currentAppName);
|
||||
const command = await backupVolume(mock);
|
||||
|
||||
// Verify logging messages
|
||||
expect(command).toContain("Volume name:");
|
||||
expect(command).toContain("Starting volume backup");
|
||||
expect(command).toContain("Volume backup done ✅");
|
||||
expect(command).toContain("Upload to S3 done ✅");
|
||||
expect(command).toContain("tar cvf");
|
||||
|
||||
console.log("✅ All log messages present");
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
"should backup 1GB volume using real backupVolume",
|
||||
async () => {
|
||||
console.log(
|
||||
`\n🚀 Test 1GB backup with real code: ${currentVolumeName}`,
|
||||
);
|
||||
|
||||
// Create volume with ~1GB of data
|
||||
await execAsync(`docker volume create ${currentVolumeName}`);
|
||||
console.log("✅ Volume created");
|
||||
|
||||
const startTime = Date.now();
|
||||
await execAsync(`
|
||||
docker run --rm -v ${currentVolumeName}:/data ubuntu bash -c "
|
||||
echo 'Creating 1GB of test data...'
|
||||
dd if=/dev/zero of=/data/large-file-1.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-2.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-3.dat bs=1M count=250 2>/dev/null
|
||||
dd if=/dev/zero of=/data/large-file-4.dat bs=1M count=250 2>/dev/null
|
||||
mkdir -p /data/metadata
|
||||
echo 'Large backup test - Issue 3301' > /data/metadata/info.txt
|
||||
echo 'marker-67890' > /data/metadata/marker.txt
|
||||
du -sh /data
|
||||
ls -lh /data
|
||||
"
|
||||
`);
|
||||
const createTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(`✅ Created 1GB data in ${createTime}s`);
|
||||
|
||||
// Create backup directory (simulating what Dokploy does)
|
||||
const { VOLUME_BACKUPS_PATH } = paths(false);
|
||||
const volumeBackupPath = path.join(VOLUME_BACKUPS_PATH, currentAppName);
|
||||
await execAsync(`mkdir -p "${volumeBackupPath}"`);
|
||||
|
||||
// Use the REAL backupVolume function to generate the command
|
||||
const mock = createMockVolumeBackup(currentVolumeName, currentAppName);
|
||||
const fullCommand = await backupVolume(mock);
|
||||
|
||||
console.log("📦 Executing REAL Dokploy backupVolume() command...");
|
||||
|
||||
// Execute the REAL command (without S3 upload part)
|
||||
const backupStartTime = Date.now();
|
||||
const backupFileName = `${currentVolumeName}-${new Date().toISOString()}.tar`;
|
||||
|
||||
// Extract and execute just the backup part of the command (tar creation)
|
||||
// This is what Dokploy really does
|
||||
const commandWithoutS3 =
|
||||
fullCommand?.replace(
|
||||
/rclone copyto[^\n]+/g,
|
||||
'echo "Skipping S3 upload - keeping file locally for test"',
|
||||
) || "";
|
||||
|
||||
// Also prevent the cleanup of the backup file so we can verify it
|
||||
const commandWithoutCleanup = commandWithoutS3.replace(
|
||||
/rm "[^"]+\.tar"/g,
|
||||
'echo "Skipping cleanup for test verification"',
|
||||
);
|
||||
|
||||
try {
|
||||
// Execute the real Dokploy backup command
|
||||
await execAsync(commandWithoutCleanup);
|
||||
const backupTime = ((Date.now() - backupStartTime) / 1000).toFixed(2);
|
||||
console.log(`✅ Backup executed in ${backupTime}s`);
|
||||
} catch (error: any) {
|
||||
console.error("Backup command failed:", error.message);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Verify the backup file was actually created by Dokploy's command
|
||||
const { stdout: backupFiles } = await execAsync(
|
||||
`find "${volumeBackupPath}" -name "*.tar" -type f`,
|
||||
);
|
||||
const backupFilePath = backupFiles.trim().split("\n")[0];
|
||||
|
||||
if (!backupFilePath) {
|
||||
throw new Error("No backup file found");
|
||||
}
|
||||
|
||||
expect(existsSync(backupFilePath)).toBe(true);
|
||||
console.log(`✅ Backup file created: ${path.basename(backupFilePath)}`);
|
||||
|
||||
// Verify file size
|
||||
const { stdout: statOutput } = await execAsync(
|
||||
`stat -f%z "${backupFilePath}" 2>/dev/null || stat -c%s "${backupFilePath}"`,
|
||||
);
|
||||
|
||||
const sizeInMB = Number(statOutput.trim()) / (1024 * 1024);
|
||||
expect(sizeInMB).toBeGreaterThan(1000); // Should be > 1GB
|
||||
console.log(`✅ Backup file size: ${sizeInMB.toFixed(2)}MB`);
|
||||
|
||||
// Verify tar contents - this proves the backup worked
|
||||
const { stdout: tarContents } = await execAsync(
|
||||
`tar -tf "${backupFilePath}"`,
|
||||
);
|
||||
|
||||
expect(tarContents).toContain("large-file-1.dat");
|
||||
expect(tarContents).toContain("large-file-2.dat");
|
||||
expect(tarContents).toContain("large-file-3.dat");
|
||||
expect(tarContents).toContain("large-file-4.dat");
|
||||
expect(tarContents).toContain("metadata/");
|
||||
|
||||
// Extract and verify one file to ensure data integrity
|
||||
// First check if marker file exists in tar
|
||||
if (tarContents.includes("marker.txt")) {
|
||||
const tempDir = path.join(volumeBackupPath, "temp-extract");
|
||||
await execAsync(`mkdir -p "${tempDir}"`);
|
||||
|
||||
// Extract entire tar to handle path variations (. vs ./ prefix)
|
||||
await execAsync(`tar -xf "${backupFilePath}" -C "${tempDir}"`);
|
||||
|
||||
// Find marker file regardless of path
|
||||
const { stdout: markerPath } = await execAsync(
|
||||
`find "${tempDir}" -name "marker.txt" -type f`,
|
||||
);
|
||||
|
||||
if (markerPath.trim()) {
|
||||
const { stdout: markerContent } = await execAsync(
|
||||
`cat "${markerPath.trim()}"`,
|
||||
);
|
||||
expect(markerContent.trim()).toBe("marker-67890");
|
||||
}
|
||||
|
||||
await execAsync(`rm -rf "${tempDir}"`);
|
||||
console.log("✅ Data integrity verified");
|
||||
} else {
|
||||
// Alternative: extract entire metadata folder
|
||||
const tempDir = path.join(volumeBackupPath, "temp-extract");
|
||||
await execAsync(`mkdir -p "${tempDir}"`);
|
||||
await execAsync(`tar -xf "${backupFilePath}" -C "${tempDir}"`);
|
||||
|
||||
// Check what was extracted
|
||||
const { stdout: extractedFiles } = await execAsync(
|
||||
`find "${tempDir}" -type f`,
|
||||
);
|
||||
|
||||
// Verify marker file exists somewhere
|
||||
const markerFiles = extractedFiles
|
||||
.split("\n")
|
||||
.filter((f) => f.includes("marker.txt"));
|
||||
expect(markerFiles.length).toBeGreaterThan(0);
|
||||
|
||||
const markerPath = markerFiles[0];
|
||||
const { stdout: markerContent } = await execAsync(
|
||||
`cat "${markerPath}"`,
|
||||
);
|
||||
expect(markerContent.trim()).toBe("marker-67890");
|
||||
await execAsync(`rm -rf "${tempDir}"`);
|
||||
console.log("✅ Data integrity verified (alternative path)");
|
||||
}
|
||||
|
||||
console.log("\n📊 Performance Summary:");
|
||||
console.log(` - Data creation: ${createTime}s`);
|
||||
console.log(` - Size: ${sizeInMB.toFixed(2)}MB`);
|
||||
console.log(
|
||||
"✅ 1GB backup test PASSED - Real Dokploy backupVolume() works correctly",
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
},
|
||||
REAL_TEST_TIMEOUT,
|
||||
);
|
||||
1086
apps/dokploy/__test__/volume-backups/volume-restore.real.test.ts
Normal file
1086
apps/dokploy/__test__/volume-backups/volume-restore.real.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
createConverter,
|
||||
NumberInputWithSteps,
|
||||
} from "@/components/ui/number-input";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -33,23 +30,6 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const CPU_STEP = 0.25;
|
||||
const MEMORY_STEP_MB = 256;
|
||||
|
||||
const formatNumber = (value: number, decimals = 2): string =>
|
||||
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
|
||||
|
||||
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
|
||||
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
|
||||
);
|
||||
|
||||
const memoryConverter = createConverter(1024 * 1024, (mb) => {
|
||||
if (mb <= 0) return "";
|
||||
return mb >= 1024
|
||||
? `${formatNumber(mb / 1024)} GB`
|
||||
: `${formatNumber(mb)} MB`;
|
||||
});
|
||||
|
||||
const addResourcesSchema = z.object({
|
||||
memoryReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
@@ -71,7 +51,6 @@ interface Props {
|
||||
}
|
||||
|
||||
type AddResources = z.infer<typeof addResourcesSchema>;
|
||||
|
||||
export const ShowResources = ({ id, type }: Props) => {
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
@@ -184,20 +163,16 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory hard limit in bytes. Example: 1GB =
|
||||
1073741824 bytes. Use +/- buttons to adjust by
|
||||
256 MB.
|
||||
1073741824 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
<Input
|
||||
placeholder="1073741824 (1GB in bytes)"
|
||||
step={MEMORY_STEP_MB}
|
||||
converter={memoryConverter}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -223,20 +198,16 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Memory soft limit in bytes. Example: 256MB =
|
||||
268435456 bytes. Use +/- buttons to adjust by 256
|
||||
MB.
|
||||
268435456 bytes
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
<Input
|
||||
placeholder="268435456 (256MB in bytes)"
|
||||
step={MEMORY_STEP_MB}
|
||||
converter={memoryConverter}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -263,20 +234,17 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||
CPUs = 2000000000. Use +/- buttons to adjust by
|
||||
0.25 CPU.
|
||||
CPUs = 2000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
<Input
|
||||
placeholder="2000000000 (2 CPUs)"
|
||||
step={CPU_STEP}
|
||||
converter={cpuConverter}
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -303,21 +271,14 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<TooltipContent>
|
||||
<p>
|
||||
CPU shares (relative weight). Example: 1 CPU =
|
||||
1000000000. Use +/- buttons to adjust by 0.25
|
||||
CPU.
|
||||
1000000000
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<FormControl>
|
||||
<NumberInputWithSteps
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
placeholder="1000000000 (1 CPU)"
|
||||
step={CPU_STEP}
|
||||
converter={cpuConverter}
|
||||
/>
|
||||
<Input placeholder="1000000000 (1 CPU)" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -108,8 +108,7 @@ export const getLogType = (message: string): LogStyle => {
|
||||
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
|
||||
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
|
||||
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
|
||||
/\b(?:unstable|experimental)\b/i.test(lowerMessage) ||
|
||||
/⚠|⚠️/i.test(lowerMessage)
|
||||
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
|
||||
) {
|
||||
return LOG_STYLES.warning;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Activity } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { ShowStorageActions } from "./show-storage-actions";
|
||||
import { ShowTraefikActions } from "./show-traefik-actions";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Pencil, PlusIcon } from "lucide-react";
|
||||
import { PlusIcon, Pencil } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Clock,
|
||||
Key,
|
||||
KeyIcon,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Network,
|
||||
Pencil,
|
||||
ServerIcon,
|
||||
Settings,
|
||||
Terminal,
|
||||
Trash2,
|
||||
Clock,
|
||||
User,
|
||||
Key,
|
||||
Network,
|
||||
Terminal,
|
||||
Settings,
|
||||
Pencil,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbPage,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { MinusIcon, PlusIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export interface UnitConverter {
|
||||
toValue: (raw: string | undefined) => number;
|
||||
fromValue: (value: number) => string;
|
||||
formatDisplay: (value: number) => string;
|
||||
}
|
||||
|
||||
export const createConverter = (
|
||||
multiplier: number,
|
||||
formatDisplay: (value: number) => string,
|
||||
): UnitConverter => ({
|
||||
toValue: (raw) => {
|
||||
if (!raw) return 0;
|
||||
const value = Number.parseInt(raw, 10);
|
||||
return Number.isNaN(value) ? 0 : value / multiplier;
|
||||
},
|
||||
fromValue: (value) =>
|
||||
value <= 0 ? "" : String(Math.round(value * multiplier)),
|
||||
formatDisplay,
|
||||
});
|
||||
|
||||
interface NumberInputWithStepsProps {
|
||||
value: string | undefined;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
step: number;
|
||||
converter: UnitConverter;
|
||||
}
|
||||
|
||||
export const NumberInputWithSteps = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
step,
|
||||
converter,
|
||||
}: NumberInputWithStepsProps) => {
|
||||
const numericValue = converter.toValue(value);
|
||||
const displayValue = converter.formatDisplay(numericValue);
|
||||
|
||||
const handleIncrement = () =>
|
||||
onChange(converter.fromValue(numericValue + step));
|
||||
const handleDecrement = () =>
|
||||
onChange(converter.fromValue(Math.max(0, numericValue - step)));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={handleDecrement}
|
||||
disabled={numericValue <= 0}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="text-center"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{displayValue && (
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
{displayValue}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +1,21 @@
|
||||
{
|
||||
"name": "@dokploy/server",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"main": "./src/index.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs.js"
|
||||
},
|
||||
".": "./src/index.ts",
|
||||
"./db": {
|
||||
"import": "./dist/db/index.js",
|
||||
"import": "./src/db/index.ts",
|
||||
"require": "./dist/db/index.cjs.js"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./dist/*",
|
||||
"require": "./dist/*.cjs"
|
||||
"./setup/*": {
|
||||
"import": "./src/setup/*.ts",
|
||||
"require": "./dist/setup/index.cjs.js"
|
||||
},
|
||||
"./dist": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs.js"
|
||||
},
|
||||
"./dist/db": {
|
||||
"import": "./dist/db/index.js",
|
||||
"require": "./dist/db/index.cjs.js"
|
||||
},
|
||||
"./dist/db/schema": {
|
||||
"import": "./dist/db/schema/index.js",
|
||||
"require": "./dist/db/schema/index.cjs.js"
|
||||
"./constants": {
|
||||
"import": "./src/constants/index.ts",
|
||||
"require": "./dist/constants.cjs.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -117,7 +117,7 @@ const getRegistryCommands = (
|
||||
): string => {
|
||||
return `
|
||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
|
||||
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
|
||||
echo "❌ DockerHub Failed" ;
|
||||
exit 1;
|
||||
}
|
||||
|
||||
@@ -167,15 +167,9 @@ while true; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Execute command and capture exit code
|
||||
${exec}
|
||||
EXIT_CODE=$?
|
||||
|
||||
# Wait for all background processes to complete to prevent zombie processes
|
||||
wait
|
||||
|
||||
echo "Execution completed with exit code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
echo "Execution completed."
|
||||
`;
|
||||
|
||||
const cleanupCommands = {
|
||||
|
||||
Reference in New Issue
Block a user