mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-22 14:25:24 +02:00
Compare commits
4 Commits
dosu/doc-u
...
feat/add-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a7da40ffe | ||
|
|
7a1703a191 | ||
|
|
f6af5daf5e | ||
|
|
452b9a3c78 |
1
.github/workflows/pr-quality.yml
vendored
1
.github/workflows/pr-quality.yml
vendored
@@ -16,6 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: peakoss/anti-slop@v0
|
- uses: peakoss/anti-slop@v0
|
||||||
with:
|
with:
|
||||||
|
max-failures: 4
|
||||||
blocked-commit-authors: "claude,copilot"
|
blocked-commit-authors: "claude,copilot"
|
||||||
require-description: true
|
require-description: true
|
||||||
min-account-age: 5
|
min-account-age: 5
|
||||||
|
|||||||
@@ -99,14 +99,7 @@ pnpm run dokploy:build
|
|||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
To build the docker image first run commands to copy .env files
|
To build the docker image
|
||||||
|
|
||||||
```bash
|
|
||||||
cp apps/dokploy/.env.production.example .env.production
|
|
||||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
then run build command
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run docker:build
|
pnpm run docker:build
|
||||||
@@ -132,21 +125,6 @@ If you want to test the webhooks on development mode using localtunnel, make sur
|
|||||||
pnpm dlx localtunnel --port 3000
|
pnpm dlx localtunnel --port 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing GitLab Webhooks
|
|
||||||
|
|
||||||
To test GitLab webhook functionality locally:
|
|
||||||
|
|
||||||
1. Configure a GitLab provider in Dokploy with a webhook secret
|
|
||||||
2. Set up the webhook in your GitLab project (Settings → Webhooks):
|
|
||||||
- **Webhook URL:** Your localtunnel URL + `/api/deploy/gitlab`
|
|
||||||
- **Secret token:** Paste the webhook secret from your Dokploy GitLab provider
|
|
||||||
- **Enable events:** Push events and Merge request events
|
|
||||||
|
|
||||||
The GitLab webhook endpoint (`/api/deploy/gitlab`) authenticates requests via the `X-Gitlab-Token` header matched against the provider's `webhookSecret`. The endpoint handles:
|
|
||||||
|
|
||||||
- **Push Hooks** — deploys matching applications and compose stacks; respects `watchPaths` filtering using commit file lists (added/modified/removed)
|
|
||||||
- **Merge Request Hooks** — creates or rebuilds preview deployments on `open/update/reopen/labeled` events; tears down preview deployments on `close/merge` events
|
|
||||||
|
|
||||||
If you run into permission issues of docker run the following command
|
If you run into permission issues of docker run the following command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies th
|
|||||||
Dokploy includes multiple features to make your life easier.
|
Dokploy includes multiple features to make your life easier.
|
||||||
|
|
||||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, libsql, and Redis.
|
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||||
- **Backups**: Automate backups for databases to an external storage destination.
|
- **Backups**: Automate backups for databases to an external storage destination.
|
||||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||||
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||||
|
|||||||
@@ -1,11 +1,2 @@
|
|||||||
LEMON_SQUEEZY_API_KEY=""
|
LEMON_SQUEEZY_API_KEY=""
|
||||||
LEMON_SQUEEZY_STORE_ID=""
|
LEMON_SQUEEZY_STORE_ID=""
|
||||||
|
|
||||||
# Inngest (for GET /jobs - list deployment queue). Self-hosted example:
|
|
||||||
# INNGEST_BASE_URL="http://localhost:8288"
|
|
||||||
# Production: INNGEST_BASE_URL="https://dev-inngest.dokploy.com"
|
|
||||||
# INNGEST_SIGNING_KEY="your-signing-key"
|
|
||||||
# Optional: only events after this RFC3339 timestamp. If unset, no date filter is applied.
|
|
||||||
# INNGEST_EVENTS_RECEIVED_AFTER="2024-01-01T00:00:00Z"
|
|
||||||
# Max events to fetch when listing jobs (paginates with cursor). Default 100, max 10000.
|
|
||||||
# INNGEST_JOBS_MAX_EVENTS=100
|
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
type DeployJob,
|
type DeployJob,
|
||||||
deployJobSchema,
|
deployJobSchema,
|
||||||
} from "./schema.js";
|
} from "./schema.js";
|
||||||
import { fetchDeploymentJobs } from "./service.js";
|
|
||||||
import { deploy } from "./utils.js";
|
import { deploy } from "./utils.js";
|
||||||
|
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@@ -119,6 +118,7 @@ app.post("/deploy", zValidator("json", deployJobSchema), async (c) => {
|
|||||||
200,
|
200,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
logger.error("Failed to send deployment event", error);
|
logger.error("Failed to send deployment event", error);
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
@@ -176,29 +176,6 @@ app.get("/health", async (c) => {
|
|||||||
return c.json({ status: "ok" });
|
return c.json({ status: "ok" });
|
||||||
});
|
});
|
||||||
|
|
||||||
// List deployment jobs (Inngest runs) for a server - same shape as BullMQ queue for the UI
|
|
||||||
app.get("/jobs", async (c) => {
|
|
||||||
const serverId = c.req.query("serverId");
|
|
||||||
if (!serverId) {
|
|
||||||
return c.json({ message: "serverId is required" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rows = await fetchDeploymentJobs(serverId);
|
|
||||||
return c.json(rows);
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
if (message.includes("INNGEST_BASE_URL")) {
|
|
||||||
return c.json(
|
|
||||||
{ message: "INNGEST_BASE_URL is required to list deployment jobs" },
|
|
||||||
503,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
logger.error("Failed to fetch jobs from Inngest", { serverId, error });
|
|
||||||
return c.json([], 200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serve Inngest functions endpoint
|
// Serve Inngest functions endpoint
|
||||||
app.on(
|
app.on(
|
||||||
["GET", "POST", "PUT"],
|
["GET", "POST", "PUT"],
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
import { logger } from "./logger.js";
|
|
||||||
|
|
||||||
const baseUrl = process.env.INNGEST_BASE_URL ?? "";
|
|
||||||
const signingKey = process.env.INNGEST_SIGNING_KEY ?? "";
|
|
||||||
|
|
||||||
const DEFAULT_MAX_EVENTS = 500;
|
|
||||||
const MAX_EVENTS = DEFAULT_MAX_EVENTS;
|
|
||||||
|
|
||||||
/** Event shape from GET /v1/events (https://api.inngest.com/v1/events) */
|
|
||||||
type InngestEventRow = {
|
|
||||||
internal_id?: string;
|
|
||||||
accountID?: string;
|
|
||||||
environmentID?: string;
|
|
||||||
source?: string;
|
|
||||||
sourceID?: string | null;
|
|
||||||
/** RFC3339 timestamp – API uses receivedAt, dev server may use received_at */
|
|
||||||
receivedAt?: string;
|
|
||||||
received_at?: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
user?: unknown;
|
|
||||||
ts: number;
|
|
||||||
v?: string | null;
|
|
||||||
metadata?: {
|
|
||||||
fetchedAt: string;
|
|
||||||
cachedUntil: string | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Run shape from GET /v1/events/{eventId}/runs – the actual job execution */
|
|
||||||
type InngestRun = {
|
|
||||||
run_id: string;
|
|
||||||
event_id: string;
|
|
||||||
status: string; // "Running" | "Completed" | "Failed" | "Cancelled" | "Queued"?
|
|
||||||
run_started_at?: string;
|
|
||||||
ended_at?: string | null;
|
|
||||||
output?: unknown;
|
|
||||||
// dev server / API may use different casing
|
|
||||||
run_started_at_ms?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getEventReceivedAt(ev: InngestEventRow): string | undefined {
|
|
||||||
return ev.receivedAt ?? ev.received_at;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Map Inngest run status to BullMQ-style state for the UI */
|
|
||||||
function runStatusToState(
|
|
||||||
status: string,
|
|
||||||
): "pending" | "active" | "completed" | "failed" | "cancelled" {
|
|
||||||
const s = status.toLowerCase();
|
|
||||||
if (s === "running") return "active";
|
|
||||||
if (s === "completed") return "completed";
|
|
||||||
if (s === "failed") return "failed";
|
|
||||||
if (s === "cancelled") return "cancelled";
|
|
||||||
if (s === "queued") return "pending";
|
|
||||||
return "pending";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchInngestEvents = async () => {
|
|
||||||
const maxEvents = MAX_EVENTS;
|
|
||||||
const all: InngestEventRow[] = [];
|
|
||||||
let cursor: string | undefined;
|
|
||||||
|
|
||||||
do {
|
|
||||||
const params = new URLSearchParams({ limit: "100" });
|
|
||||||
if (cursor) {
|
|
||||||
params.set("cursor", cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${baseUrl}/v1/events?${params}`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${signingKey}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
logger.warn("Inngest API error", {
|
|
||||||
status: res.status,
|
|
||||||
body: await res.text(),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = (await res.json()) as {
|
|
||||||
data?: InngestEventRow[];
|
|
||||||
cursor?: string;
|
|
||||||
nextCursor?: string;
|
|
||||||
};
|
|
||||||
const data = Array.isArray(body.data) ? body.data : [];
|
|
||||||
all.push(...data);
|
|
||||||
|
|
||||||
// Next page: API may return cursor/nextCursor, or use last event's internal_id (per API docs)
|
|
||||||
const nextCursor =
|
|
||||||
body.cursor ?? body.nextCursor ?? data[data.length - 1]?.internal_id;
|
|
||||||
const hasMore = data.length === 100 && nextCursor && all.length < maxEvents;
|
|
||||||
cursor = hasMore ? nextCursor : undefined;
|
|
||||||
} while (cursor);
|
|
||||||
|
|
||||||
return all.slice(0, maxEvents);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Fetch runs for a single event (GET /v1/events/{eventId}/runs) – runs are the actual jobs */
|
|
||||||
export const fetchInngestRunsForEvent = async (
|
|
||||||
eventId: string,
|
|
||||||
): Promise<InngestRun[]> => {
|
|
||||||
const res = await fetch(
|
|
||||||
`${baseUrl}/v1/events/${encodeURIComponent(eventId)}/runs`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${signingKey}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (!res.ok) {
|
|
||||||
logger.warn("Inngest runs API error", {
|
|
||||||
eventId,
|
|
||||||
status: res.status,
|
|
||||||
body: await res.text(),
|
|
||||||
});
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const body = (await res.json()) as { data?: InngestRun[] };
|
|
||||||
return Array.isArray(body.data) ? body.data : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** One row for the queue UI (BullMQ-compatible shape) */
|
|
||||||
export type DeploymentJobRow = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
timestamp: number;
|
|
||||||
processedOn?: number;
|
|
||||||
finishedOn?: number;
|
|
||||||
failedReason?: string;
|
|
||||||
state: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Build queue rows from events + their runs (one row per run, or pending if no run yet) */
|
|
||||||
function buildDeploymentRowsFromRuns(
|
|
||||||
events: InngestEventRow[],
|
|
||||||
runsByEventId: Map<string, InngestRun[]>,
|
|
||||||
serverId: string,
|
|
||||||
): DeploymentJobRow[] {
|
|
||||||
const requested = events.filter(
|
|
||||||
(e) =>
|
|
||||||
e.name === "deployment/requested" &&
|
|
||||||
(e.data as Record<string, unknown>)?.serverId === serverId,
|
|
||||||
);
|
|
||||||
const rows: DeploymentJobRow[] = [];
|
|
||||||
|
|
||||||
for (const ev of requested) {
|
|
||||||
const data = (ev.data ?? {}) as Record<string, unknown>;
|
|
||||||
const runs = runsByEventId.get(ev.id) ?? [];
|
|
||||||
|
|
||||||
if (runs.length === 0) {
|
|
||||||
// Queued: event received but no run yet
|
|
||||||
rows.push({
|
|
||||||
id: ev.id,
|
|
||||||
name: ev.name,
|
|
||||||
data,
|
|
||||||
timestamp: ev.ts,
|
|
||||||
processedOn: ev.ts,
|
|
||||||
finishedOn: undefined,
|
|
||||||
failedReason: undefined,
|
|
||||||
state: "pending",
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const run of runs) {
|
|
||||||
const state = runStatusToState(run.status);
|
|
||||||
const runStartedMs =
|
|
||||||
run.run_started_at_ms ??
|
|
||||||
(run.run_started_at ? new Date(run.run_started_at).getTime() : ev.ts);
|
|
||||||
const endedMs = run.ended_at
|
|
||||||
? new Date(run.ended_at).getTime()
|
|
||||||
: undefined;
|
|
||||||
const failedReason =
|
|
||||||
state === "failed" &&
|
|
||||||
run.output &&
|
|
||||||
typeof run.output === "object" &&
|
|
||||||
"error" in run.output
|
|
||||||
? String((run.output as { error?: unknown }).error)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
rows.push({
|
|
||||||
id: run.run_id,
|
|
||||||
name: ev.name,
|
|
||||||
data,
|
|
||||||
timestamp: runStartedMs,
|
|
||||||
processedOn: runStartedMs,
|
|
||||||
finishedOn:
|
|
||||||
state === "completed" || state === "failed" || state === "cancelled"
|
|
||||||
? endedMs
|
|
||||||
: undefined,
|
|
||||||
failedReason,
|
|
||||||
state,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fetch deployment jobs for a server: events → runs → rows (correct model: runs = jobs) */
|
|
||||||
export const fetchDeploymentJobs = async (
|
|
||||||
serverId: string,
|
|
||||||
): Promise<DeploymentJobRow[]> => {
|
|
||||||
if (!signingKey) {
|
|
||||||
logger.warn("INNGEST_SIGNING_KEY not set, returning empty jobs list");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (!baseUrl) {
|
|
||||||
throw new Error("INNGEST_BASE_URL is required to list deployment jobs");
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = await fetchInngestEvents();
|
|
||||||
|
|
||||||
const requestedForServer = events.filter(
|
|
||||||
(e) =>
|
|
||||||
e.name === "deployment/requested" &&
|
|
||||||
(e.data as Record<string, unknown>)?.serverId === serverId,
|
|
||||||
);
|
|
||||||
// Limit to avoid too many run fetches
|
|
||||||
const toFetch = requestedForServer.slice(0, 50);
|
|
||||||
const runsByEventId = new Map<string, InngestRun[]>();
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
toFetch.map(async (ev) => {
|
|
||||||
const runs = await fetchInngestRunsForEvent(ev.id);
|
|
||||||
runsByEventId.set(ev.id, runs);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return buildDeploymentRowsFromRuns(toFetch, runsByEventId, serverId);
|
|
||||||
};
|
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
updateCompose,
|
updateCompose,
|
||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import type { DeployJob } from "./schema.js";
|
import type { DeployJob } from "./schema";
|
||||||
|
|
||||||
export const deploy = async (job: DeployJob) => {
|
export const deploy = async (job: DeployJob) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
const mockMemberData = (
|
|
||||||
role: string,
|
|
||||||
overrides: Record<string, boolean> = {},
|
|
||||||
) => ({
|
|
||||||
id: "member-1",
|
|
||||||
role,
|
|
||||||
userId: "user-1",
|
|
||||||
organizationId: "org-1",
|
|
||||||
accessedProjects: [] as string[],
|
|
||||||
accessedServices: [] as string[],
|
|
||||||
accessedEnvironments: [] as string[],
|
|
||||||
canCreateProjects: overrides.canCreateProjects ?? false,
|
|
||||||
canDeleteProjects: overrides.canDeleteProjects ?? false,
|
|
||||||
canCreateServices: overrides.canCreateServices ?? false,
|
|
||||||
canDeleteServices: overrides.canDeleteServices ?? false,
|
|
||||||
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
|
|
||||||
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
|
|
||||||
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
|
|
||||||
canAccessToDocker: overrides.canAccessToDocker ?? false,
|
|
||||||
canAccessToAPI: overrides.canAccessToAPI ?? false,
|
|
||||||
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
|
|
||||||
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
|
|
||||||
user: { id: "user-1", email: "test@test.com" },
|
|
||||||
});
|
|
||||||
|
|
||||||
let memberToReturn: ReturnType<typeof mockMemberData> =
|
|
||||||
mockMemberData("member");
|
|
||||||
|
|
||||||
vi.mock("@dokploy/server/db", () => ({
|
|
||||||
db: {
|
|
||||||
query: {
|
|
||||||
member: {
|
|
||||||
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
|
||||||
findMany: vi.fn(() => Promise.resolve([])),
|
|
||||||
},
|
|
||||||
organizationRole: {
|
|
||||||
findFirst: vi.fn(),
|
|
||||||
findMany: vi.fn(() => Promise.resolve([])),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
|
||||||
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { checkPermission } = await import("@dokploy/server/services/permission");
|
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
user: { id: "user-1" },
|
|
||||||
session: { activeOrganizationId: "org-1" },
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("static roles bypass enterprise resources", () => {
|
|
||||||
it("owner bypasses deployment.read", async () => {
|
|
||||||
memberToReturn = mockMemberData("owner");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { deployment: ["read"] }),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("admin bypasses backup.create", async () => {
|
|
||||||
memberToReturn = mockMemberData("admin");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { backup: ["create"] }),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member bypasses schedule.delete", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { schedule: ["delete"] }),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member bypasses multiple enterprise permissions at once", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, {
|
|
||||||
deployment: ["read"],
|
|
||||||
backup: ["create"],
|
|
||||||
domain: ["delete"],
|
|
||||||
}),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("static roles validate free-tier resources", () => {
|
|
||||||
it("owner passes project.create", async () => {
|
|
||||||
memberToReturn = mockMemberData("owner");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { project: ["create"] }),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member fails project.create (no legacy override)", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { project: ["create"] }),
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member passes service.read", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { service: ["read"] }),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member fails service.create", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { service: ["create"] }),
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("legacy boolean overrides for member", () => {
|
|
||||||
it("member passes project.create with canCreateProjects=true", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", { canCreateProjects: true });
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { project: ["create"] }),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member passes docker.read with canAccessToDocker=true", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
|
|
||||||
await expect(
|
|
||||||
checkPermission(ctx, { docker: ["read"] }),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member fails docker.read with canAccessToDocker=false", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import {
|
|
||||||
enterpriseOnlyResources,
|
|
||||||
statements,
|
|
||||||
} from "@dokploy/server/lib/access-control";
|
|
||||||
|
|
||||||
const FREE_TIER_RESOURCES = [
|
|
||||||
"organization",
|
|
||||||
"member",
|
|
||||||
"invitation",
|
|
||||||
"team",
|
|
||||||
"ac",
|
|
||||||
"project",
|
|
||||||
"service",
|
|
||||||
"environment",
|
|
||||||
"docker",
|
|
||||||
"sshKeys",
|
|
||||||
"gitProviders",
|
|
||||||
"traefikFiles",
|
|
||||||
"api",
|
|
||||||
];
|
|
||||||
|
|
||||||
const ENTERPRISE_RESOURCES = [
|
|
||||||
"volume",
|
|
||||||
"deployment",
|
|
||||||
"envVars",
|
|
||||||
"projectEnvVars",
|
|
||||||
"environmentEnvVars",
|
|
||||||
"server",
|
|
||||||
"registry",
|
|
||||||
"certificate",
|
|
||||||
"backup",
|
|
||||||
"volumeBackup",
|
|
||||||
"schedule",
|
|
||||||
"domain",
|
|
||||||
"destination",
|
|
||||||
"notification",
|
|
||||||
"tag",
|
|
||||||
"logs",
|
|
||||||
"monitoring",
|
|
||||||
"auditLog",
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("enterpriseOnlyResources set", () => {
|
|
||||||
it("contains all enterprise resources", () => {
|
|
||||||
for (const resource of ENTERPRISE_RESOURCES) {
|
|
||||||
expect(enterpriseOnlyResources.has(resource)).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does NOT contain free-tier resources", () => {
|
|
||||||
for (const resource of FREE_TIER_RESOURCES) {
|
|
||||||
expect(enterpriseOnlyResources.has(resource)).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("every resource in statements is either free or enterprise", () => {
|
|
||||||
const allResources = Object.keys(statements);
|
|
||||||
for (const resource of allResources) {
|
|
||||||
const isFree = FREE_TIER_RESOURCES.includes(resource);
|
|
||||||
const isEnterprise = enterpriseOnlyResources.has(resource);
|
|
||||||
expect(isFree || isEnterprise).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("free and enterprise sets don't overlap", () => {
|
|
||||||
for (const resource of FREE_TIER_RESOURCES) {
|
|
||||||
expect(enterpriseOnlyResources.has(resource)).toBe(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("all statement resources are accounted for", () => {
|
|
||||||
const allResources = Object.keys(statements);
|
|
||||||
const categorized = [...FREE_TIER_RESOURCES, ...ENTERPRISE_RESOURCES];
|
|
||||||
for (const resource of allResources) {
|
|
||||||
expect(categorized).toContain(resource);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
const mockMemberData = (
|
|
||||||
role: string,
|
|
||||||
overrides: Record<string, boolean> = {},
|
|
||||||
) => ({
|
|
||||||
id: "member-1",
|
|
||||||
role,
|
|
||||||
userId: "user-1",
|
|
||||||
organizationId: "org-1",
|
|
||||||
accessedProjects: [] as string[],
|
|
||||||
accessedServices: [] as string[],
|
|
||||||
accessedEnvironments: [] as string[],
|
|
||||||
canCreateProjects: overrides.canCreateProjects ?? false,
|
|
||||||
canDeleteProjects: overrides.canDeleteProjects ?? false,
|
|
||||||
canCreateServices: overrides.canCreateServices ?? false,
|
|
||||||
canDeleteServices: overrides.canDeleteServices ?? false,
|
|
||||||
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
|
|
||||||
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
|
|
||||||
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
|
|
||||||
canAccessToDocker: overrides.canAccessToDocker ?? false,
|
|
||||||
canAccessToAPI: overrides.canAccessToAPI ?? false,
|
|
||||||
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
|
|
||||||
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
|
|
||||||
user: { id: "user-1", email: "test@test.com" },
|
|
||||||
});
|
|
||||||
|
|
||||||
let memberToReturn: ReturnType<typeof mockMemberData> =
|
|
||||||
mockMemberData("member");
|
|
||||||
|
|
||||||
vi.mock("@dokploy/server/db", () => ({
|
|
||||||
db: {
|
|
||||||
query: {
|
|
||||||
member: {
|
|
||||||
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
|
||||||
findMany: vi.fn(() => Promise.resolve([])),
|
|
||||||
},
|
|
||||||
organizationRole: {
|
|
||||||
findFirst: vi.fn(),
|
|
||||||
findMany: vi.fn(() => Promise.resolve([])),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
|
||||||
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { resolvePermissions } = await import(
|
|
||||||
"@dokploy/server/services/permission"
|
|
||||||
);
|
|
||||||
const { enterpriseOnlyResources, statements } = await import(
|
|
||||||
"@dokploy/server/lib/access-control"
|
|
||||||
);
|
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
user: { id: "user-1" },
|
|
||||||
session: { activeOrganizationId: "org-1" },
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("enterprise resources for static roles", () => {
|
|
||||||
it("owner gets true for all enterprise resources", async () => {
|
|
||||||
memberToReturn = mockMemberData("owner");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
|
|
||||||
for (const resource of enterpriseOnlyResources) {
|
|
||||||
const actions = statements[resource as keyof typeof statements];
|
|
||||||
for (const action of actions) {
|
|
||||||
expect((perms as any)[resource][action]).toBe(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("admin gets true for all enterprise resources", async () => {
|
|
||||||
memberToReturn = mockMemberData("admin");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
|
|
||||||
for (const resource of enterpriseOnlyResources) {
|
|
||||||
const actions = statements[resource as keyof typeof statements];
|
|
||||||
for (const action of actions) {
|
|
||||||
expect((perms as any)[resource][action]).toBe(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member gets true for service-level enterprise resources", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
|
|
||||||
expect(perms.deployment.read).toBe(true);
|
|
||||||
expect(perms.deployment.create).toBe(true);
|
|
||||||
expect(perms.domain.read).toBe(true);
|
|
||||||
expect(perms.backup.read).toBe(true);
|
|
||||||
expect(perms.logs.read).toBe(true);
|
|
||||||
expect(perms.monitoring.read).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member gets false for org-level enterprise resources", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
|
|
||||||
expect(perms.server.read).toBe(false);
|
|
||||||
expect(perms.registry.read).toBe(false);
|
|
||||||
expect(perms.certificate.read).toBe(false);
|
|
||||||
expect(perms.destination.read).toBe(false);
|
|
||||||
expect(perms.notification.read).toBe(false);
|
|
||||||
expect(perms.auditLog.read).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("free-tier resources for member", () => {
|
|
||||||
it("member gets service.read=true", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
expect(perms.service.read).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member gets project.create=false without legacy override", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
expect(perms.project.create).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member gets project.create=true with canCreateProjects", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", { canCreateProjects: true });
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
expect(perms.project.create).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member gets docker.read=false without legacy override", async () => {
|
|
||||||
memberToReturn = mockMemberData("member");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
expect(perms.docker.read).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member gets docker.read=true with canAccessToDocker", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
expect(perms.docker.read).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("free-tier resources for owner", () => {
|
|
||||||
it("owner gets all free-tier permissions as true", async () => {
|
|
||||||
memberToReturn = mockMemberData("owner");
|
|
||||||
const perms = await resolvePermissions(ctx);
|
|
||||||
expect(perms.project.create).toBe(true);
|
|
||||||
expect(perms.project.delete).toBe(true);
|
|
||||||
expect(perms.service.create).toBe(true);
|
|
||||||
expect(perms.service.read).toBe(true);
|
|
||||||
expect(perms.service.delete).toBe(true);
|
|
||||||
expect(perms.docker.read).toBe(true);
|
|
||||||
expect(perms.traefikFiles.read).toBe(true);
|
|
||||||
expect(perms.traefikFiles.write).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
const mockMemberData = (
|
|
||||||
role: string,
|
|
||||||
accessedServices: string[] = [],
|
|
||||||
accessedProjects: string[] = [],
|
|
||||||
) => ({
|
|
||||||
id: "member-1",
|
|
||||||
role,
|
|
||||||
userId: "user-1",
|
|
||||||
organizationId: "org-1",
|
|
||||||
accessedProjects,
|
|
||||||
accessedServices,
|
|
||||||
accessedEnvironments: [] as string[],
|
|
||||||
canCreateProjects: false,
|
|
||||||
canDeleteProjects: false,
|
|
||||||
canCreateServices: false,
|
|
||||||
canDeleteServices: false,
|
|
||||||
canCreateEnvironments: false,
|
|
||||||
canDeleteEnvironments: false,
|
|
||||||
canAccessToTraefikFiles: false,
|
|
||||||
canAccessToDocker: false,
|
|
||||||
canAccessToAPI: false,
|
|
||||||
canAccessToSSHKeys: false,
|
|
||||||
canAccessToGitProviders: false,
|
|
||||||
user: { id: "user-1", email: "test@test.com" },
|
|
||||||
});
|
|
||||||
|
|
||||||
let memberToReturn: ReturnType<typeof mockMemberData> =
|
|
||||||
mockMemberData("member");
|
|
||||||
|
|
||||||
vi.mock("@dokploy/server/db", () => ({
|
|
||||||
db: {
|
|
||||||
query: {
|
|
||||||
member: {
|
|
||||||
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
|
||||||
findMany: vi.fn(() => Promise.resolve([])),
|
|
||||||
},
|
|
||||||
organizationRole: {
|
|
||||||
findFirst: vi.fn(),
|
|
||||||
findMany: vi.fn(() => Promise.resolve([])),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
|
||||||
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { checkServicePermissionAndAccess, checkServiceAccess } = await import(
|
|
||||||
"@dokploy/server/services/permission"
|
|
||||||
);
|
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
user: { id: "user-1" },
|
|
||||||
session: { activeOrganizationId: "org-1" },
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("checkServicePermissionAndAccess", () => {
|
|
||||||
it("owner bypasses accessedServices check", async () => {
|
|
||||||
memberToReturn = mockMemberData("owner", []);
|
|
||||||
await expect(
|
|
||||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
|
||||||
deployment: ["read"],
|
|
||||||
}),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("admin bypasses accessedServices check", async () => {
|
|
||||||
memberToReturn = mockMemberData("admin", []);
|
|
||||||
await expect(
|
|
||||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
|
||||||
backup: ["create"],
|
|
||||||
}),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member with access to service passes", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", ["service-123"]);
|
|
||||||
await expect(
|
|
||||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
|
||||||
deployment: ["read"],
|
|
||||||
}),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member WITHOUT access to service fails", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", ["other-service"]);
|
|
||||||
await expect(
|
|
||||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
|
||||||
deployment: ["read"],
|
|
||||||
}),
|
|
||||||
).rejects.toThrow("You don't have access to this service");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member with empty accessedServices fails", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", []);
|
|
||||||
await expect(
|
|
||||||
checkServicePermissionAndAccess(ctx, "service-123", {
|
|
||||||
domain: ["delete"],
|
|
||||||
}),
|
|
||||||
).rejects.toThrow("You don't have access to this service");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("checkServiceAccess", () => {
|
|
||||||
it("member with service access passes read check", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", ["app-1"]);
|
|
||||||
await expect(
|
|
||||||
checkServiceAccess(ctx, "app-1", "read"),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("member without service access fails read check", async () => {
|
|
||||||
memberToReturn = mockMemberData("member", []);
|
|
||||||
await expect(checkServiceAccess(ctx, "app-1", "read")).rejects.toThrow(
|
|
||||||
"You don't have access to this service",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("owner bypasses all access checks", async () => {
|
|
||||||
memberToReturn = mockMemberData("owner", [], []);
|
|
||||||
await expect(
|
|
||||||
checkServiceAccess(ctx, "project-1", "create"),
|
|
||||||
).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -48,20 +48,6 @@ const baseSettings: WebServerSettings = {
|
|||||||
urlCallback: "",
|
urlCallback: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
whitelabelingConfig: {
|
|
||||||
appName: null,
|
|
||||||
appDescription: null,
|
|
||||||
logoUrl: null,
|
|
||||||
faviconUrl: null,
|
|
||||||
customCss: null,
|
|
||||||
loginLogoUrl: null,
|
|
||||||
supportUrl: null,
|
|
||||||
docsUrl: null,
|
|
||||||
errorPageTitle: null,
|
|
||||||
errorPageDescription: null,
|
|
||||||
metaTitle: null,
|
|
||||||
footerText: null,
|
|
||||||
},
|
|
||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
|
|||||||
@@ -110,16 +110,16 @@ const menuItems: MenuItem[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const hasStopGracePeriodSwarm = (
|
||||||
|
value: unknown,
|
||||||
|
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"stopGracePeriodSwarm" in value;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "application"
|
|
||||||
| "libsql"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "postgres"
|
|
||||||
| "redis";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddSwarmSettings = ({ id, type }: Props) => {
|
export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { AddSwarmSettings } from "./modify-swarm-settings";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectchema = z.object({
|
||||||
@@ -49,15 +49,15 @@ type AddCommand = z.infer<typeof AddRedirectchema>;
|
|||||||
|
|
||||||
export const ShowClusterSettings = ({ id, type }: Props) => {
|
export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
application: () =>
|
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
|
||||||
mariadb: () =>
|
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -65,13 +65,12 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
|
|||||||
const { data: registries } = api.registry.all.useQuery();
|
const { data: registries } = api.registry.all.useQuery();
|
||||||
|
|
||||||
const mutationMap = {
|
const mutationMap = {
|
||||||
application: () => api.application.update.useMutation(),
|
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
mariadb: () => api.mariadb.update.useMutation(),
|
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
|
||||||
mysql: () => api.mysql.update.useMutation(),
|
|
||||||
postgres: () => api.postgres.update.useMutation(),
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
redis: () => api.redis.update.useMutation(),
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync, isPending } = mutationMap[type]
|
const { mutateAsync, isPending } = mutationMap[type]
|
||||||
@@ -106,11 +105,11 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
|
|||||||
const onSubmit = async (data: AddCommand) => {
|
const onSubmit = async (data: AddCommand) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId: id || "",
|
applicationId: id || "",
|
||||||
mariadbId: id || "",
|
|
||||||
mongoId: id || "",
|
|
||||||
mysqlId: id || "",
|
|
||||||
postgresId: id || "",
|
postgresId: id || "",
|
||||||
redisId: id || "",
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
mongoId: id || "",
|
||||||
...(type === "application"
|
...(type === "application"
|
||||||
? {
|
? {
|
||||||
registryId:
|
registryId:
|
||||||
|
|||||||
@@ -28,14 +28,7 @@ export const endpointSpecFormSchema = z.object({
|
|||||||
|
|
||||||
interface EndpointSpecFormProps {
|
interface EndpointSpecFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||||
@@ -51,7 +44,6 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -64,7 +56,6 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -103,7 +94,6 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
endpointSpecSwarm: hasAnyValue ? formData : null,
|
endpointSpecSwarm: hasAnyValue ? formData : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,7 @@ export const healthCheckFormSchema = z.object({
|
|||||||
|
|
||||||
interface HealthCheckFormProps {
|
interface HealthCheckFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||||
@@ -49,7 +42,6 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -62,7 +54,6 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -113,7 +104,6 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
healthCheckSwarm: hasAnyValue ? formData : null,
|
healthCheckSwarm: hasAnyValue ? formData : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,7 @@ export const labelsFormSchema = z.object({
|
|||||||
|
|
||||||
interface LabelsFormProps {
|
interface LabelsFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||||
@@ -52,7 +45,6 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -65,7 +57,6 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -121,7 +112,6 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
labelsSwarm: labelsToSend,
|
labelsSwarm: labelsToSend,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,7 @@ import { api } from "@/utils/api";
|
|||||||
|
|
||||||
interface ModeFormProps {
|
interface ModeFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModeForm = ({ id, type }: ModeFormProps) => {
|
export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||||
@@ -46,7 +39,6 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -59,7 +51,6 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -104,7 +95,6 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
modeSwarm: null,
|
modeSwarm: null,
|
||||||
});
|
});
|
||||||
toast.success("Mode updated successfully");
|
toast.success("Mode updated successfully");
|
||||||
@@ -132,7 +122,6 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
modeSwarm: modeData,
|
modeSwarm: modeData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,14 +35,7 @@ export const networkFormSchema = z.object({
|
|||||||
|
|
||||||
interface NetworkFormProps {
|
interface NetworkFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||||
@@ -58,7 +51,6 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -71,7 +63,6 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -141,7 +132,6 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
networkSwarm: networksToSend,
|
networkSwarm: networksToSend,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,7 @@ export const placementFormSchema = z.object({
|
|||||||
|
|
||||||
interface PlacementFormProps {
|
interface PlacementFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||||
@@ -57,7 +50,6 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -70,7 +62,6 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -123,7 +114,6 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
placementSwarm: hasAnyValue
|
placementSwarm: hasAnyValue
|
||||||
? {
|
? {
|
||||||
...formData,
|
...formData,
|
||||||
|
|||||||
@@ -32,14 +32,7 @@ export const restartPolicyFormSchema = z.object({
|
|||||||
|
|
||||||
interface RestartPolicyFormProps {
|
interface RestartPolicyFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||||
@@ -55,7 +48,6 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -68,7 +60,6 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -113,7 +104,6 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
restartPolicySwarm: hasAnyValue ? formData : null,
|
restartPolicySwarm: hasAnyValue ? formData : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,7 @@ export const rollbackConfigFormSchema = z.object({
|
|||||||
|
|
||||||
interface RollbackConfigFormProps {
|
interface RollbackConfigFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||||
@@ -57,7 +50,6 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -70,7 +62,6 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -112,7 +103,6 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
rollbackConfigSwarm: (hasAnyValue ? formData : null) as any,
|
rollbackConfigSwarm: (hasAnyValue ? formData : null) as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,7 @@ const hasStopGracePeriodSwarm = (
|
|||||||
|
|
||||||
interface StopGracePeriodFormProps {
|
interface StopGracePeriodFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||||
@@ -46,7 +39,6 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -59,7 +51,6 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -97,7 +88,6 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
stopGracePeriodSwarm: formData.value,
|
stopGracePeriodSwarm: formData.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,14 +34,7 @@ export const updateConfigFormSchema = z.object({
|
|||||||
|
|
||||||
interface UpdateConfigFormProps {
|
interface UpdateConfigFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type:
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "redis"
|
|
||||||
| "application"
|
|
||||||
| "libsql";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||||
@@ -57,7 +50,6 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
|||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -70,7 +62,6 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
|||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
@@ -118,7 +109,6 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
|||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
updateConfigSwarm: (hasAnyValue ? formData : null) as any,
|
updateConfigSwarm: (hasAnyValue ? formData : null) as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,13 +89,12 @@ const ULIMIT_PRESETS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export type ServiceType =
|
export type ServiceType =
|
||||||
| "application"
|
|
||||||
| "libsql"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "postgres"
|
| "postgres"
|
||||||
| "redis";
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "mysql"
|
||||||
|
| "mariadb"
|
||||||
|
| "application";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -106,29 +105,27 @@ type AddResources = z.infer<typeof addResourcesSchema>;
|
|||||||
|
|
||||||
export const ShowResources = ({ id, type }: Props) => {
|
export const ShowResources = ({ id, type }: Props) => {
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
application: () =>
|
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
mariadb: () =>
|
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
const mutationMap = {
|
const mutationMap = {
|
||||||
application: () => api.application.update.useMutation(),
|
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
mariadb: () => api.mariadb.update.useMutation(),
|
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
|
||||||
mysql: () => api.mysql.update.useMutation(),
|
|
||||||
postgres: () => api.postgres.update.useMutation(),
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
redis: () => api.redis.update.useMutation(),
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
application: () => api.application.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync, isPending } = mutationMap[type]
|
const { mutateAsync, isPending } = mutationMap[type]
|
||||||
@@ -158,20 +155,19 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
cpuReservation: data?.cpuReservation || undefined,
|
cpuReservation: data?.cpuReservation || undefined,
|
||||||
memoryLimit: data?.memoryLimit || undefined,
|
memoryLimit: data?.memoryLimit || undefined,
|
||||||
memoryReservation: data?.memoryReservation || undefined,
|
memoryReservation: data?.memoryReservation || undefined,
|
||||||
ulimitsSwarm: (data as any)?.ulimitsSwarm || [],
|
ulimitsSwarm: data?.ulimitsSwarm || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form, form.reset]);
|
}, [data, form, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (formData: AddResources) => {
|
const onSubmit = async (formData: AddResources) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId: id || "",
|
|
||||||
libsqlId: id || "",
|
|
||||||
mariadbId: id || "",
|
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
mysqlId: id || "",
|
|
||||||
postgresId: id || "",
|
postgresId: id || "",
|
||||||
redisId: id || "",
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
applicationId: id || "",
|
||||||
cpuLimit: formData.cpuLimit || null,
|
cpuLimit: formData.cpuLimit || null,
|
||||||
cpuReservation: formData.cpuReservation || null,
|
cpuReservation: formData.cpuReservation || null,
|
||||||
memoryLimit: formData.memoryLimit || null,
|
memoryLimit: formData.memoryLimit || null,
|
||||||
|
|||||||
@@ -15,17 +15,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canRead = permissions?.traefikFiles.read ?? false;
|
|
||||||
const { data, isPending } = api.application.readTraefikConfig.useQuery(
|
const { data, isPending } = api.application.readTraefikConfig.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId && canRead },
|
{ enabled: !!applicationId },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!canRead) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between">
|
<CardHeader className="flex flex-row justify-between">
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ export const validateAndFormatYAML = (yamlText: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canWrite = permissions?.traefikFiles.write ?? false;
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
|
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
|
||||||
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
||||||
@@ -127,11 +125,9 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{canWrite && (
|
<DialogTrigger asChild>
|
||||||
<DialogTrigger asChild>
|
<Button isLoading={isPending}>Modify</Button>
|
||||||
<Button isLoading={isPending}>Modify</Button>
|
</DialogTrigger>
|
||||||
</DialogTrigger>
|
|
||||||
)}
|
|
||||||
<DialogContent className="sm:max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ interface Props {
|
|||||||
serviceId: string;
|
serviceId: string;
|
||||||
serviceType:
|
serviceType:
|
||||||
| "application"
|
| "application"
|
||||||
| "compose"
|
|
||||||
| "libsql"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "postgres"
|
| "postgres"
|
||||||
| "redis";
|
| "redis"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "mysql"
|
||||||
|
| "mariadb"
|
||||||
|
| "compose";
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,33 +21,24 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowVolumes = ({ id, type }: Props) => {
|
export const ShowVolumes = ({ id, type }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canRead = permissions?.volume.read ?? false;
|
|
||||||
const canCreate = permissions?.volume.create ?? false;
|
|
||||||
const canDelete = permissions?.volume.delete ?? false;
|
|
||||||
|
|
||||||
if (!canRead) return null;
|
|
||||||
|
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
application: () =>
|
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
|
||||||
compose: () =>
|
|
||||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
mariadb: () =>
|
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
application: () =>
|
||||||
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
compose: () =>
|
||||||
|
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
const { mutateAsync: deleteVolume, isPending: isRemoving } =
|
const { mutateAsync: deleteVolume, isPending: isRemoving } =
|
||||||
api.mounts.remove.useMutation();
|
api.mounts.remove.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||||
@@ -59,7 +50,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canCreate && data && data?.mounts.length > 0 && (
|
{data && data?.mounts.length > 0 && (
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
@@ -72,11 +63,9 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No volumes/mounts configured
|
No volumes/mounts configured
|
||||||
</span>
|
</span>
|
||||||
{canCreate && (
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
Add Volume
|
||||||
Add Volume
|
</AddVolumes>
|
||||||
</AddVolumes>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
@@ -141,42 +130,38 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
{canCreate && (
|
<UpdateVolume
|
||||||
<UpdateVolume
|
mountId={mount.mountId}
|
||||||
mountId={mount.mountId}
|
type={mount.type}
|
||||||
type={mount.type}
|
refetch={refetch}
|
||||||
refetch={refetch}
|
serviceType={type}
|
||||||
serviceType={type}
|
/>
|
||||||
/>
|
<DialogAction
|
||||||
)}
|
title="Delete Volume"
|
||||||
{canDelete && (
|
description="Are you sure you want to delete this volume?"
|
||||||
<DialogAction
|
type="destructive"
|
||||||
title="Delete Volume"
|
onClick={async () => {
|
||||||
description="Are you sure you want to delete this volume?"
|
await deleteVolume({
|
||||||
type="destructive"
|
mountId: mount.mountId,
|
||||||
onClick={async () => {
|
})
|
||||||
await deleteVolume({
|
.then(() => {
|
||||||
mountId: mount.mountId,
|
refetch();
|
||||||
|
toast.success("Volume deleted successfully");
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
refetch();
|
toast.error("Error deleting volume");
|
||||||
toast.success("Volume deleted successfully");
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error("Error deleting volume");
|
<Button
|
||||||
});
|
variant="ghost"
|
||||||
}}
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,13 +67,13 @@ interface Props {
|
|||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
serviceType:
|
serviceType:
|
||||||
| "application"
|
| "application"
|
||||||
| "compose"
|
|
||||||
| "libsql"
|
|
||||||
| "mariadb"
|
|
||||||
| "mongo"
|
|
||||||
| "mysql"
|
|
||||||
| "postgres"
|
| "postgres"
|
||||||
| "redis";
|
| "redis"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "mysql"
|
||||||
|
| "mariadb"
|
||||||
|
| "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateVolume = ({
|
export const UpdateVolume = ({
|
||||||
@@ -253,7 +253,7 @@ export const UpdateVolume = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="content"
|
name="content"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full max-w-[45rem]">
|
<FormItem className="max-w-full max-w-[45rem]">
|
||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Clock,
|
Clock,
|
||||||
Copy,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
@@ -11,7 +10,6 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -99,12 +97,6 @@ export const ShowDeployments = ({
|
|||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const webhookUrl = useMemo(
|
|
||||||
() =>
|
|
||||||
`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`,
|
|
||||||
[url, refreshToken, type],
|
|
||||||
);
|
|
||||||
|
|
||||||
const MAX_DESCRIPTION_LENGTH = 200;
|
const MAX_DESCRIPTION_LENGTH = 200;
|
||||||
|
|
||||||
const truncateDescription = (description: string): string => {
|
const truncateDescription = (description: string): string => {
|
||||||
@@ -232,27 +224,11 @@ export const ShowDeployments = ({
|
|||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
<span>Webhook URL: </span>
|
<span>Webhook URL: </span>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Badge
|
<span className="break-all text-muted-foreground">
|
||||||
role="button"
|
{`${url}/api/deploy${
|
||||||
tabIndex={0}
|
type === "compose" ? "/compose" : ""
|
||||||
aria-label="Copy webhook URL to clipboard"
|
}/${refreshToken}`}
|
||||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer whitespace-normal break-all"
|
</span>
|
||||||
variant="outline"
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
copy(webhookUrl);
|
|
||||||
toast.success("Copied to clipboard.");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
copy(webhookUrl);
|
|
||||||
toast.success("Copied to clipboard.");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{webhookUrl}
|
|
||||||
<Copy className="h-4 w-4 ml-2" />
|
|
||||||
</Badge>
|
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<RefreshToken id={id} type={type} />
|
<RefreshToken id={id} type={type} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ id, type }: Props) => {
|
export const ShowDomains = ({ id, type }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canCreateDomain = permissions?.domain.create ?? false;
|
|
||||||
const canDeleteDomain = permissions?.domain.delete ?? false;
|
|
||||||
const { data: application } =
|
const { data: application } =
|
||||||
type === "application"
|
type === "application"
|
||||||
? api.application.one.useQuery(
|
? api.application.one.useQuery(
|
||||||
@@ -152,7 +149,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
{canCreateDomain && data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<AddDomain id={id} type={type}>
|
<AddDomain id={id} type={type}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
@@ -176,15 +173,13 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
To access the application it is required to set at least 1
|
To access the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
{canCreateDomain && (
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<AddDomain id={id} type={type}>
|
||||||
<AddDomain id={id} type={type}>
|
<Button>
|
||||||
<Button>
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
</Button>
|
||||||
</Button>
|
</AddDomain>
|
||||||
</AddDomain>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
||||||
@@ -219,51 +214,47 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canCreateDomain && (
|
<AddDomain
|
||||||
<AddDomain
|
id={id}
|
||||||
id={id}
|
type={type}
|
||||||
type={type}
|
domainId={item.domainId}
|
||||||
domainId={item.domainId}
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10"
|
||||||
>
|
>
|
||||||
<Button
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</AddDomain>
|
||||||
className="group hover:bg-blue-500/10"
|
<DialogAction
|
||||||
>
|
title="Delete Domain"
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
description="Are you sure you want to delete this domain?"
|
||||||
</Button>
|
type="destructive"
|
||||||
</AddDomain>
|
onClick={async () => {
|
||||||
)}
|
await deleteDomain({
|
||||||
{canDeleteDomain && (
|
domainId: item.domainId,
|
||||||
<DialogAction
|
})
|
||||||
title="Delete Domain"
|
.then((_data) => {
|
||||||
description="Are you sure you want to delete this domain?"
|
refetch();
|
||||||
type="destructive"
|
toast.success(
|
||||||
onClick={async () => {
|
"Domain deleted successfully",
|
||||||
await deleteDomain({
|
);
|
||||||
domainId: item.domainId,
|
|
||||||
})
|
})
|
||||||
.then((_data) => {
|
.catch(() => {
|
||||||
refetch();
|
toast.error("Error deleting domain");
|
||||||
toast.success(
|
});
|
||||||
"Domain deleted successfully",
|
}}
|
||||||
);
|
>
|
||||||
})
|
<Button
|
||||||
.catch(() => {
|
variant="ghost"
|
||||||
toast.error("Error deleting domain");
|
size="icon"
|
||||||
});
|
className="group hover:bg-red-500/10"
|
||||||
}}
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full break-all">
|
<div className="w-full break-all">
|
||||||
|
|||||||
@@ -36,19 +36,16 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ id, type }: Props) => {
|
export const ShowEnvironment = ({ id, type }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canWrite = permissions?.envVars.write ?? false;
|
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
compose: () =>
|
|
||||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
mariadb: () =>
|
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
|
mariadb: () =>
|
||||||
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
|
compose: () =>
|
||||||
|
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -56,13 +53,12 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
|
|
||||||
const mutationMap = {
|
const mutationMap = {
|
||||||
compose: () => api.compose.update.useMutation(),
|
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
mariadb: () => api.mariadb.update.useMutation(),
|
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
|
||||||
mysql: () => api.mysql.update.useMutation(),
|
|
||||||
postgres: () => api.postgres.update.useMutation(),
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
redis: () => api.redis.update.useMutation(),
|
redis: () => api.redis.update.useMutation(),
|
||||||
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
|
compose: () => api.compose.update.useMutation(),
|
||||||
};
|
};
|
||||||
const { mutateAsync, isPending } = mutationMap[type]
|
const { mutateAsync, isPending } = mutationMap[type]
|
||||||
? mutationMap[type]()
|
? mutationMap[type]()
|
||||||
@@ -89,13 +85,12 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (formData: EnvironmentSchema) => {
|
const onSubmit = async (formData: EnvironmentSchema) => {
|
||||||
mutateAsync({
|
mutateAsync({
|
||||||
composeId: id || "",
|
|
||||||
libsqlId: id || "",
|
|
||||||
mariadbId: id || "",
|
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
mysqlId: id || "",
|
|
||||||
postgresId: id || "",
|
postgresId: id || "",
|
||||||
redisId: id || "",
|
redisId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
mariadbId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
env: formData.environment,
|
env: formData.environment,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -190,27 +185,25 @@ PORT=3000
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{canWrite && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{hasChanges && (
|
||||||
{hasChanges && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={isPending}
|
type="button"
|
||||||
className="w-fit"
|
variant="outline"
|
||||||
type="submit"
|
onClick={handleCancel}
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
>
|
||||||
Save
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
)}
|
<Button
|
||||||
|
isLoading={isPending}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canWrite = permissions?.envVars.write ?? false;
|
|
||||||
const { mutateAsync, isPending } =
|
const { mutateAsync, isPending } =
|
||||||
api.application.saveEnvironment.useMutation();
|
api.application.saveEnvironment.useMutation();
|
||||||
|
|
||||||
@@ -203,30 +201,27 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
disabled={!canWrite}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canWrite && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{hasChanges && (
|
||||||
{hasChanges && (
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
Cancel
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
isLoading={isPending}
|
|
||||||
className="w-fit"
|
|
||||||
type="submit"
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
)}
|
<Button
|
||||||
|
isLoading={isPending}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -416,8 +416,10 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -228,8 +228,10 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ interface Props {
|
|||||||
|
|
||||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDeploy = permissions?.deployment.create ?? false;
|
|
||||||
const canUpdateService = permissions?.service.create ?? false;
|
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -60,135 +57,128 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
{canDeploy && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Deploy Application"
|
||||||
title="Deploy Application"
|
description="Are you sure you want to deploy this application?"
|
||||||
description="Are you sure you want to deploy this application?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await deploy({
|
||||||
await deploy({
|
applicationId: applicationId,
|
||||||
applicationId: applicationId,
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Application deployed successfully");
|
||||||
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success("Application deployed successfully");
|
toast.error("Error deploying application");
|
||||||
refetch();
|
});
|
||||||
router.push(
|
}}
|
||||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
>
|
||||||
);
|
<Button
|
||||||
})
|
variant="default"
|
||||||
.catch(() => {
|
isLoading={data?.applicationStatus === "running"}
|
||||||
toast.error("Error deploying application");
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Rocket className="size-4 mr-1" />
|
||||||
>
|
Deploy
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<Rocket className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Deploy
|
<p>
|
||||||
</div>
|
Downloads the source code and performs a complete build
|
||||||
</TooltipTrigger>
|
</p>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipPrimitive.Portal>
|
||||||
<p>
|
</Tooltip>
|
||||||
Downloads the source code and performs a complete
|
</Button>
|
||||||
build
|
</DialogAction>
|
||||||
</p>
|
<DialogAction
|
||||||
</TooltipContent>
|
title="Reload Application"
|
||||||
</TooltipPrimitive.Portal>
|
description="Are you sure you want to reload this application?"
|
||||||
</Tooltip>
|
type="default"
|
||||||
</Button>
|
onClick={async () => {
|
||||||
</DialogAction>
|
await reload({
|
||||||
)}
|
applicationId: applicationId,
|
||||||
{canDeploy && (
|
appName: data?.appName || "",
|
||||||
<DialogAction
|
})
|
||||||
title="Reload Application"
|
.then(() => {
|
||||||
description="Are you sure you want to reload this application?"
|
toast.success("Application reloaded successfully");
|
||||||
type="default"
|
refetch();
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
applicationId: applicationId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success("Application reloaded successfully");
|
toast.error("Error reloading application");
|
||||||
refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error("Error reloading application");
|
<Button
|
||||||
});
|
variant="secondary"
|
||||||
}}
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={isReloading}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
>
|
Reload
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Reload
|
<p>Reload the application without rebuilding it</p>
|
||||||
</div>
|
</TooltipContent>
|
||||||
</TooltipTrigger>
|
</TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</Button>
|
||||||
<p>Reload the application without rebuilding it</p>
|
</DialogAction>
|
||||||
</TooltipContent>
|
<DialogAction
|
||||||
</TooltipPrimitive.Portal>
|
title="Rebuild Application"
|
||||||
</Tooltip>
|
description="Are you sure you want to rebuild this application?"
|
||||||
</Button>
|
type="default"
|
||||||
</DialogAction>
|
onClick={async () => {
|
||||||
)}
|
await redeploy({
|
||||||
{canDeploy && (
|
applicationId: applicationId,
|
||||||
<DialogAction
|
})
|
||||||
title="Rebuild Application"
|
.then(() => {
|
||||||
description="Are you sure you want to rebuild this application?"
|
toast.success("Application rebuilt successfully");
|
||||||
type="default"
|
refetch();
|
||||||
onClick={async () => {
|
|
||||||
await redeploy({
|
|
||||||
applicationId: applicationId,
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success("Application rebuilt successfully");
|
toast.error("Error rebuilding application");
|
||||||
refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error("Error rebuilding application");
|
<Button
|
||||||
});
|
variant="secondary"
|
||||||
}}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="secondary"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Hammer className="size-4 mr-1" />
|
||||||
>
|
Rebuild
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<Hammer className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Rebuild
|
<p>
|
||||||
</div>
|
Only rebuilds the application without downloading new
|
||||||
</TooltipTrigger>
|
code
|
||||||
<TooltipPrimitive.Portal>
|
</p>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</TooltipContent>
|
||||||
<p>
|
</TooltipPrimitive.Portal>
|
||||||
Only rebuilds the application without downloading new
|
</Tooltip>
|
||||||
code
|
</Button>
|
||||||
</p>
|
</DialogAction>
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canDeploy && data?.applicationStatus === "idle" ? (
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Application"
|
title="Start Application"
|
||||||
description="Are you sure you want to start this application?"
|
description="Are you sure you want to start this application?"
|
||||||
@@ -229,7 +219,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : canDeploy ? (
|
) : (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Application"
|
title="Stop Application"
|
||||||
description="Are you sure you want to stop this application?"
|
description="Are you sure you want to stop this application?"
|
||||||
@@ -266,7 +256,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : null}
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -280,53 +270,49 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
{canUpdateService && (
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<Switch
|
||||||
<Switch
|
aria-label="Toggle autodeploy"
|
||||||
aria-label="Toggle autodeploy"
|
checked={data?.autoDeploy || false}
|
||||||
checked={data?.autoDeploy || false}
|
onCheckedChange={async (enabled) => {
|
||||||
onCheckedChange={async (enabled) => {
|
await update({
|
||||||
await update({
|
applicationId,
|
||||||
applicationId,
|
autoDeploy: enabled,
|
||||||
autoDeploy: enabled,
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Auto Deploy Updated");
|
||||||
|
await refetch();
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.catch(() => {
|
||||||
toast.success("Auto Deploy Updated");
|
toast.error("Error updating Auto Deploy");
|
||||||
await refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
toast.error("Error updating Auto Deploy");
|
/>
|
||||||
});
|
</div>
|
||||||
}}
|
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canUpdateService && (
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
<span className="text-sm font-medium">Clean Cache</span>
|
||||||
<span className="text-sm font-medium">Clean Cache</span>
|
<Switch
|
||||||
<Switch
|
aria-label="Toggle clean cache"
|
||||||
aria-label="Toggle clean cache"
|
checked={data?.cleanCache || false}
|
||||||
checked={data?.cleanCache || false}
|
onCheckedChange={async (enabled) => {
|
||||||
onCheckedChange={async (enabled) => {
|
await update({
|
||||||
await update({
|
applicationId,
|
||||||
applicationId,
|
cleanCache: enabled,
|
||||||
cleanCache: enabled,
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Clean Cache Updated");
|
||||||
|
await refetch();
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.catch(() => {
|
||||||
toast.success("Clean Cache Updated");
|
toast.error("Error updating Clean Cache");
|
||||||
await refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
toast.error("Error updating Clean Cache");
|
/>
|
||||||
});
|
</div>
|
||||||
}}
|
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<ShowProviderForm applicationId={applicationId} />
|
<ShowProviderForm applicationId={applicationId} />
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ const formSchema = z
|
|||||||
"mongo",
|
"mongo",
|
||||||
"mysql",
|
"mysql",
|
||||||
"redis",
|
"redis",
|
||||||
"libsql",
|
|
||||||
]),
|
]),
|
||||||
serviceName: z.string(),
|
serviceName: z.string(),
|
||||||
destinationId: z.string().min(1, "Destination required"),
|
destinationId: z.string().min(1, "Destination required"),
|
||||||
|
|||||||
@@ -46,8 +46,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteService = ({ id, type }: Props) => {
|
export const DeleteService = ({ id, type }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDelete = permissions?.service.delete ?? false;
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
@@ -57,7 +55,6 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
mariadb: () =>
|
mariadb: () =>
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
application: () =>
|
application: () =>
|
||||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
@@ -73,7 +70,6 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
redis: () => api.redis.remove.useMutation(),
|
redis: () => api.redis.remove.useMutation(),
|
||||||
mysql: () => api.mysql.remove.useMutation(),
|
mysql: () => api.mysql.remove.useMutation(),
|
||||||
mariadb: () => api.mariadb.remove.useMutation(),
|
mariadb: () => api.mariadb.remove.useMutation(),
|
||||||
libsql: () => api.libsql.remove.useMutation(),
|
|
||||||
application: () => api.application.delete.useMutation(),
|
application: () => api.application.delete.useMutation(),
|
||||||
mongo: () => api.mongo.remove.useMutation(),
|
mongo: () => api.mongo.remove.useMutation(),
|
||||||
compose: () => api.compose.delete.useMutation(),
|
compose: () => api.compose.delete.useMutation(),
|
||||||
@@ -100,7 +96,6 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
redisId: id || "",
|
redisId: id || "",
|
||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
applicationId: id || "",
|
applicationId: id || "",
|
||||||
composeId: id || "",
|
composeId: id || "",
|
||||||
deleteVolumes,
|
deleteVolumes,
|
||||||
@@ -128,8 +123,6 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
data?.applicationStatus === "running") ||
|
data?.applicationStatus === "running") ||
|
||||||
(data && "composeStatus" in data && data?.composeStatus === "running");
|
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||||
|
|
||||||
if (!canDelete) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const ComposeActions = ({ composeId }: Props) => {
|
export const ComposeActions = ({ composeId }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDeploy = permissions?.deployment.create ?? false;
|
|
||||||
const canUpdateService = permissions?.service.create ?? false;
|
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
@@ -38,169 +35,162 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
{canDeploy && (
|
<DialogAction
|
||||||
|
title="Deploy Compose"
|
||||||
|
description="Are you sure you want to deploy this compose?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await deploy({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose deployed successfully");
|
||||||
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deploying compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Rocket className="size-4 mr-1" />
|
||||||
|
Deploy
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Downloads the source code and performs a complete build</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Compose"
|
||||||
|
description="Are you sure you want to reload this compose?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={data?.composeStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Reload the compose without rebuilding it</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
{data?.composeType === "docker-compose" &&
|
||||||
|
data?.composeStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Compose"
|
title="Start Compose"
|
||||||
description="Are you sure you want to deploy this compose?"
|
description="Are you sure you want to start this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await deploy({
|
await start({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose deployed successfully");
|
toast.success("Compose started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error deploying compose");
|
toast.error("Error starting compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="secondary"
|
||||||
isLoading={data?.composeStatus === "running"}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Rocket className="size-4 mr-1" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
Deploy
|
Start
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Downloads the source code and performs a complete build
|
Start the compose (requires a previous successful build)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
) : (
|
||||||
{canDeploy && (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Compose"
|
title="Stop Compose"
|
||||||
description="Are you sure you want to reload this compose?"
|
description="Are you sure you want to stop this compose?"
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await redeploy({
|
await stop({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose reloaded successfully");
|
toast.success("Compose stopped successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading compose");
|
toast.error("Error stopping compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="destructive"
|
||||||
isLoading={data?.composeStatus === "running"}
|
isLoading={isStopping}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<Ban className="size-4 mr-1" />
|
||||||
Reload
|
Stop
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Reload the compose without rebuilding it</p>
|
<p>Stop the currently running compose</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
{canDeploy &&
|
|
||||||
(data?.composeType === "docker-compose" &&
|
|
||||||
data?.composeStatus === "idle" ? (
|
|
||||||
<DialogAction
|
|
||||||
title="Start Compose"
|
|
||||||
description="Are you sure you want to start this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isStarting}
|
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
|
||||||
Start
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Start the compose (requires a previous successful build)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Compose"
|
|
||||||
description="Are you sure you want to stop this compose?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running compose</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
))}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -215,29 +205,27 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
{canUpdateService && (
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<Switch
|
||||||
<Switch
|
aria-label="Toggle autodeploy"
|
||||||
aria-label="Toggle autodeploy"
|
checked={data?.autoDeploy || false}
|
||||||
checked={data?.autoDeploy || false}
|
onCheckedChange={async (enabled) => {
|
||||||
onCheckedChange={async (enabled) => {
|
await update({
|
||||||
await update({
|
composeId,
|
||||||
composeId,
|
autoDeploy: enabled,
|
||||||
autoDeploy: enabled,
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Auto Deploy Updated");
|
||||||
|
await refetch();
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.catch(() => {
|
||||||
toast.success("Auto Deploy Updated");
|
toast.error("Error updating Auto Deploy");
|
||||||
await refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
toast.error("Error updating Auto Deploy");
|
/>
|
||||||
});
|
</div>
|
||||||
}}
|
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ const AddComposeFile = z.object({
|
|||||||
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
||||||
|
|
||||||
export const ComposeFileEditor = ({ composeId }: Props) => {
|
export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canUpdate = permissions?.service.create ?? false;
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -166,16 +164,14 @@ services:
|
|||||||
</Form>
|
</Form>
|
||||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end" />
|
<div className="w-full flex flex-col lg:flex-row gap-4 items-end" />
|
||||||
{canUpdate && (
|
<Button
|
||||||
<Button
|
type="submit"
|
||||||
type="submit"
|
form="hook-form-save-compose-file"
|
||||||
form="hook-form-save-compose-file"
|
isLoading={isPending}
|
||||||
isLoading={isPending}
|
className="lg:w-fit w-full"
|
||||||
className="lg:w-fit w-full"
|
>
|
||||||
>
|
Save
|
||||||
Save
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -230,8 +230,10 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger>
|
||||||
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||||
|
?
|
||||||
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -65,13 +65,7 @@ import { ScheduleFormField } from "../../application/schedules/handle-schedules"
|
|||||||
|
|
||||||
type CacheType = "cache" | "fetch";
|
type CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
type DatabaseType =
|
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||||
| "postgres"
|
|
||||||
| "mariadb"
|
|
||||||
| "mysql"
|
|
||||||
| "mongo"
|
|
||||||
| "web-server"
|
|
||||||
| "libsql";
|
|
||||||
|
|
||||||
const Schema = z
|
const Schema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -83,7 +77,7 @@ const Schema = z
|
|||||||
keepLatestCount: z.coerce.number().optional(),
|
keepLatestCount: z.coerce.number().optional(),
|
||||||
serviceName: z.string().nullable(),
|
serviceName: z.string().nullable(),
|
||||||
databaseType: z
|
databaseType: z
|
||||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]),
|
backupType: z.enum(["database", "compose"]),
|
||||||
metadata: z
|
metadata: z
|
||||||
@@ -215,12 +209,7 @@ export const HandleBackup = ({
|
|||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
database:
|
database: databaseType === "web-server" ? "dokploy" : "",
|
||||||
databaseType === "web-server"
|
|
||||||
? "dokploy"
|
|
||||||
: databaseType === "libsql"
|
|
||||||
? "iku.db"
|
|
||||||
: "",
|
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
prefix: "/",
|
prefix: "/",
|
||||||
@@ -257,9 +246,7 @@ export const HandleBackup = ({
|
|||||||
? backup?.database
|
? backup?.database
|
||||||
: databaseType === "web-server"
|
: databaseType === "web-server"
|
||||||
? "dokploy"
|
? "dokploy"
|
||||||
: databaseType === "libsql"
|
: "",
|
||||||
? "iku.db"
|
|
||||||
: "",
|
|
||||||
destinationId: backup?.destinationId ?? "",
|
destinationId: backup?.destinationId ?? "",
|
||||||
enabled: backup?.enabled ?? true,
|
enabled: backup?.enabled ?? true,
|
||||||
prefix: backup?.prefix ?? "/",
|
prefix: backup?.prefix ?? "/",
|
||||||
@@ -294,15 +281,11 @@ export const HandleBackup = ({
|
|||||||
? {
|
? {
|
||||||
mongoId: id,
|
mongoId: id,
|
||||||
}
|
}
|
||||||
: databaseType === "libsql"
|
: databaseType === "web-server"
|
||||||
? {
|
? {
|
||||||
libsqlId: id,
|
userId: id,
|
||||||
}
|
}
|
||||||
: databaseType === "web-server"
|
: undefined;
|
||||||
? {
|
|
||||||
userId: id,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await createBackup({
|
await createBackup({
|
||||||
destinationId: data.destinationId,
|
destinationId: data.destinationId,
|
||||||
@@ -585,10 +568,7 @@ export const HandleBackup = ({
|
|||||||
<FormLabel>Database</FormLabel>
|
<FormLabel>Database</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
disabled={
|
disabled={databaseType === "web-server"}
|
||||||
databaseType === "web-server" ||
|
|
||||||
databaseType === "libsql"
|
|
||||||
}
|
|
||||||
placeholder={"dokploy"}
|
placeholder={"dokploy"}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const RestoreBackupSchema = z
|
|||||||
message: "Database name is required",
|
message: "Database name is required",
|
||||||
}),
|
}),
|
||||||
databaseType: z
|
databaseType: z
|
||||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]).default("database"),
|
backupType: z.enum(["database", "compose"]).default("database"),
|
||||||
metadata: z
|
metadata: z
|
||||||
@@ -211,12 +211,7 @@ export const RestoreBackup = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
backupFile: "",
|
backupFile: "",
|
||||||
databaseName:
|
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||||
databaseType === "web-server"
|
|
||||||
? "dokploy"
|
|
||||||
: databaseType === "libsql"
|
|
||||||
? "iku.db"
|
|
||||||
: "",
|
|
||||||
databaseType:
|
databaseType:
|
||||||
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
||||||
backupType: backupType,
|
backupType: backupType,
|
||||||
@@ -528,10 +523,7 @@ export const RestoreBackup = ({
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Enter database name"
|
placeholder="Enter database name"
|
||||||
{...field}
|
{...field}
|
||||||
disabled={
|
disabled={databaseType === "web-server"}
|
||||||
databaseType === "web-server" ||
|
|
||||||
databaseType === "libsql"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -53,16 +53,14 @@ export const ShowBackups = ({
|
|||||||
const queryMap =
|
const queryMap =
|
||||||
backupType === "database"
|
backupType === "database"
|
||||||
? {
|
? {
|
||||||
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
mysql: () =>
|
||||||
|
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
mariadb: () =>
|
mariadb: () =>
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
mongo: () =>
|
mongo: () =>
|
||||||
api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
mysql: () =>
|
|
||||||
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
|
||||||
postgres: () =>
|
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
|
||||||
libsql: () =>
|
|
||||||
api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
"web-server": () => api.user.getBackups.useQuery(),
|
"web-server": () => api.user.getBackups.useQuery(),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -79,11 +77,10 @@ export const ShowBackups = ({
|
|||||||
const mutationMap =
|
const mutationMap =
|
||||||
backupType === "database"
|
backupType === "database"
|
||||||
? {
|
? {
|
||||||
|
postgres: api.backup.manualBackupPostgres.useMutation(),
|
||||||
|
mysql: api.backup.manualBackupMySql.useMutation(),
|
||||||
mariadb: api.backup.manualBackupMariadb.useMutation(),
|
mariadb: api.backup.manualBackupMariadb.useMutation(),
|
||||||
mongo: api.backup.manualBackupMongo.useMutation(),
|
mongo: api.backup.manualBackupMongo.useMutation(),
|
||||||
mysql: api.backup.manualBackupMySql.useMutation(),
|
|
||||||
postgres: api.backup.manualBackupPostgres.useMutation(),
|
|
||||||
libsql: api.backup.manualBackupLibsql.useMutation(),
|
|
||||||
"web-server": api.backup.manualBackupWebServer.useMutation(),
|
"web-server": api.backup.manualBackupWebServer.useMutation(),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
|||||||
@@ -1,613 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
type ColumnFiltersState,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getFilteredRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
type PaginationState,
|
|
||||||
type SortingState,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import type { inferRouterOutputs } from "@trpc/server";
|
|
||||||
import {
|
|
||||||
ArrowUpDown,
|
|
||||||
Boxes,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
ExternalLink,
|
|
||||||
Loader2,
|
|
||||||
Rocket,
|
|
||||||
Server,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import type { AppRouter } from "@/server/api/root";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
type DeploymentRow =
|
|
||||||
inferRouterOutputs<AppRouter>["deployment"]["allCentralized"][number];
|
|
||||||
|
|
||||||
const statusVariants: Record<
|
|
||||||
string,
|
|
||||||
| "default"
|
|
||||||
| "secondary"
|
|
||||||
| "destructive"
|
|
||||||
| "outline"
|
|
||||||
| "yellow"
|
|
||||||
| "green"
|
|
||||||
| "red"
|
|
||||||
> = {
|
|
||||||
running: "yellow",
|
|
||||||
done: "green",
|
|
||||||
error: "red",
|
|
||||||
cancelled: "outline",
|
|
||||||
};
|
|
||||||
|
|
||||||
function getServiceInfo(d: DeploymentRow) {
|
|
||||||
const app = d.application;
|
|
||||||
const comp = d.compose;
|
|
||||||
if (app?.environment?.project && app.environment) {
|
|
||||||
return {
|
|
||||||
type: "Application" as const,
|
|
||||||
name: app.name,
|
|
||||||
projectId: app.environment.project.projectId,
|
|
||||||
environmentId: app.environment.environmentId,
|
|
||||||
projectName: app.environment.project.name,
|
|
||||||
environmentName: app.environment.name,
|
|
||||||
serviceId: app.applicationId,
|
|
||||||
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (comp?.environment?.project && comp.environment) {
|
|
||||||
return {
|
|
||||||
type: "Compose" as const,
|
|
||||||
name: comp.name,
|
|
||||||
projectId: comp.environment.project.projectId,
|
|
||||||
environmentId: comp.environment.environmentId,
|
|
||||||
projectName: comp.environment.project.name,
|
|
||||||
environmentName: comp.environment.name,
|
|
||||||
serviceId: comp.composeId,
|
|
||||||
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShowDeploymentsTable() {
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([
|
|
||||||
{ id: "createdAt", desc: true },
|
|
||||||
]);
|
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
||||||
const [globalFilter, setGlobalFilter] = useState("");
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
|
||||||
const [typeFilter, setTypeFilter] = useState<string>("all");
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: deploymentsList, isLoading } =
|
|
||||||
api.deployment.allCentralized.useQuery(undefined, {
|
|
||||||
refetchInterval: 5000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
|
||||||
if (!deploymentsList) return [];
|
|
||||||
let list = deploymentsList;
|
|
||||||
if (statusFilter !== "all") {
|
|
||||||
list = list.filter((d) => d.status === statusFilter);
|
|
||||||
}
|
|
||||||
if (typeFilter === "application") {
|
|
||||||
list = list.filter((d) => d.applicationId != null);
|
|
||||||
} else if (typeFilter === "compose") {
|
|
||||||
list = list.filter((d) => d.composeId != null);
|
|
||||||
}
|
|
||||||
if (globalFilter.trim()) {
|
|
||||||
const q = globalFilter.toLowerCase();
|
|
||||||
list = list.filter((d) => {
|
|
||||||
const info = getServiceInfo(d);
|
|
||||||
const serverName =
|
|
||||||
d.server?.name ??
|
|
||||||
d.application?.server?.name ??
|
|
||||||
d.compose?.server?.name ??
|
|
||||||
"";
|
|
||||||
const buildServerName =
|
|
||||||
d.buildServer?.name ?? d.application?.buildServer?.name ?? "";
|
|
||||||
if (!info) return false;
|
|
||||||
return (
|
|
||||||
info.name.toLowerCase().includes(q) ||
|
|
||||||
info.projectName.toLowerCase().includes(q) ||
|
|
||||||
info.environmentName.toLowerCase().includes(q) ||
|
|
||||||
(d.title?.toLowerCase().includes(q) ?? false) ||
|
|
||||||
serverName.toLowerCase().includes(q) ||
|
|
||||||
buildServerName.toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}, [deploymentsList, statusFilter, typeFilter, globalFilter]);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
id: "serviceName",
|
|
||||||
accessorFn: (row: DeploymentRow) => getServiceInfo(row)?.name ?? "",
|
|
||||||
header: ({
|
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: {
|
|
||||||
getIsSorted: () => false | "asc" | "desc";
|
|
||||||
toggleSorting: (asc: boolean) => void;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="-ml-3 h-8"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Service
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => {
|
|
||||||
const info = getServiceInfo(row.original);
|
|
||||||
if (!info) return <span className="text-muted-foreground">—</span>;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{info.type === "Application" ? (
|
|
||||||
<Rocket className="size-4 text-muted-foreground shrink-0" />
|
|
||||||
) : (
|
|
||||||
<Boxes className="size-4 text-muted-foreground shrink-0" />
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<span className="font-medium truncate">{info.name}</span>
|
|
||||||
<Badge variant="outline" className="w-fit text-[10px]">
|
|
||||||
{info.type}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "projectName",
|
|
||||||
accessorFn: (row: DeploymentRow) =>
|
|
||||||
getServiceInfo(row)?.projectName ?? "",
|
|
||||||
header: ({
|
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: {
|
|
||||||
getIsSorted: () => false | "asc" | "desc";
|
|
||||||
toggleSorting: (asc: boolean) => void;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="-ml-3 h-8"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Project
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => {
|
|
||||||
const info = getServiceInfo(row.original);
|
|
||||||
return (
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{info?.projectName ?? "—"}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "environmentName",
|
|
||||||
accessorFn: (row: DeploymentRow) =>
|
|
||||||
getServiceInfo(row)?.environmentName ?? "",
|
|
||||||
header: ({
|
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: {
|
|
||||||
getIsSorted: () => false | "asc" | "desc";
|
|
||||||
toggleSorting: (asc: boolean) => void;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="-ml-3 h-8"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Environment
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => {
|
|
||||||
const info = getServiceInfo(row.original);
|
|
||||||
return (
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{info?.environmentName ?? "—"}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "serverName",
|
|
||||||
accessorFn: (row: DeploymentRow) =>
|
|
||||||
row.server?.name ??
|
|
||||||
row.application?.server?.name ??
|
|
||||||
row.compose?.server?.name ??
|
|
||||||
"",
|
|
||||||
header: ({
|
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: {
|
|
||||||
getIsSorted: () => false | "asc" | "desc";
|
|
||||||
toggleSorting: (asc: boolean) => void;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="-ml-3 h-8"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Server
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => {
|
|
||||||
const d = row.original;
|
|
||||||
const serverName =
|
|
||||||
d.server?.name ??
|
|
||||||
d.application?.server?.name ??
|
|
||||||
d.compose?.server?.name ??
|
|
||||||
null;
|
|
||||||
const serverType =
|
|
||||||
d.server?.serverType ??
|
|
||||||
d.application?.server?.serverType ??
|
|
||||||
d.compose?.server?.serverType ??
|
|
||||||
null;
|
|
||||||
const buildServerName =
|
|
||||||
d.buildServer?.name ?? d.application?.buildServer?.name ?? null;
|
|
||||||
const buildServerType =
|
|
||||||
d.buildServer?.serverType ??
|
|
||||||
d.application?.buildServer?.serverType ??
|
|
||||||
null;
|
|
||||||
const showBuild =
|
|
||||||
buildServerName != null && buildServerName !== serverName;
|
|
||||||
if (!serverName && !showBuild) {
|
|
||||||
return <span className="text-muted-foreground">—</span>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-0.5 text-sm">
|
|
||||||
{serverName && (
|
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
|
||||||
<Server className="size-3.5 text-muted-foreground shrink-0" />
|
|
||||||
<span className="truncate">{serverName}</span>
|
|
||||||
{serverType && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-[10px] font-normal"
|
|
||||||
>
|
|
||||||
{serverType}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showBuild && buildServerName && (
|
|
||||||
<div className="flex items-center gap-1.5 text-muted-foreground flex-wrap">
|
|
||||||
<span className="text-[10px]">Build:</span>
|
|
||||||
<span className="truncate text-xs">{buildServerName}</span>
|
|
||||||
{buildServerType && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-[10px] font-normal"
|
|
||||||
>
|
|
||||||
{buildServerType}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "title",
|
|
||||||
header: ({
|
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: {
|
|
||||||
getIsSorted: () => false | "asc" | "desc";
|
|
||||||
toggleSorting: (asc: boolean) => void;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="-ml-3 h-8"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Title
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => (
|
|
||||||
<span className="text-sm truncate max-w-[200px] block">
|
|
||||||
{row.original.title || "—"}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "status",
|
|
||||||
header: ({
|
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: {
|
|
||||||
getIsSorted: () => false | "asc" | "desc";
|
|
||||||
toggleSorting: (asc: boolean) => void;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="-ml-3 h-8"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => {
|
|
||||||
const status = row.original.status ?? "running";
|
|
||||||
return (
|
|
||||||
<Badge variant={statusVariants[status] ?? "secondary"}>
|
|
||||||
{status}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "createdAt",
|
|
||||||
header: ({
|
|
||||||
column,
|
|
||||||
}: {
|
|
||||||
column: {
|
|
||||||
getIsSorted: () => false | "asc" | "desc";
|
|
||||||
toggleSorting: (asc: boolean) => void;
|
|
||||||
};
|
|
||||||
}) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="-ml-3 h-8"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Created
|
|
||||||
<ArrowUpDown className="ml-2 size-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => (
|
|
||||||
<span className="text-muted-foreground text-sm whitespace-nowrap">
|
|
||||||
{row.original.createdAt
|
|
||||||
? new Date(row.original.createdAt).toLocaleString()
|
|
||||||
: "—"}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "",
|
|
||||||
id: "actions",
|
|
||||||
enableSorting: false,
|
|
||||||
cell: ({ row }: { row: { original: DeploymentRow } }) => {
|
|
||||||
const info = getServiceInfo(row.original);
|
|
||||||
if (!info) return null;
|
|
||||||
return (
|
|
||||||
<Button variant="ghost" size="sm" asChild>
|
|
||||||
<Link href={info.href} className="gap-1">
|
|
||||||
<ExternalLink className="size-4" />
|
|
||||||
Open
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: filteredData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
sorting,
|
|
||||||
columnFilters,
|
|
||||||
globalFilter,
|
|
||||||
pagination,
|
|
||||||
},
|
|
||||||
onSortingChange: setSorting,
|
|
||||||
onColumnFiltersChange: setColumnFilters,
|
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<Input
|
|
||||||
placeholder="Search by name, project, environment, server..."
|
|
||||||
value={globalFilter}
|
|
||||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
||||||
className="max-w-xs"
|
|
||||||
/>
|
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
||||||
<SelectTrigger className="w-[140px]">
|
|
||||||
<SelectValue placeholder="Status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All statuses</SelectItem>
|
|
||||||
<SelectItem value="running">Running</SelectItem>
|
|
||||||
<SelectItem value="done">Done</SelectItem>
|
|
||||||
<SelectItem value="error">Error</SelectItem>
|
|
||||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
|
||||||
<SelectTrigger className="w-[140px]">
|
|
||||||
<SelectValue placeholder="Type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All types</SelectItem>
|
|
||||||
<SelectItem value="application">Application</SelectItem>
|
|
||||||
<SelectItem value="compose">Compose</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="px-0">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex gap-4 w-full items-center justify-center min-h-[45vh] text-muted-foreground">
|
|
||||||
<Loader2 className="size-4 animate-spin" />
|
|
||||||
<span>Loading deployments...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="rounded-md border overflow-x-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<TableRow key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => (
|
|
||||||
<TableHead key={header.id}>
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext(),
|
|
||||||
)}
|
|
||||||
</TableHead>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{table.getRowModel().rows?.length ? (
|
|
||||||
table.getRowModel().rows.map((row) => (
|
|
||||||
<TableRow key={row.id}>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<TableCell key={cell.id}>
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext(),
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className=" text-center"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col min-h-[45vh] items-center justify-center gap-2 text-muted-foreground">
|
|
||||||
<Rocket className="size-8" />
|
|
||||||
<p className="font-medium">No deployments found</p>
|
|
||||||
<p className="text-sm">
|
|
||||||
Deployments from applications and compose will
|
|
||||||
appear here.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 px-4 py-4 border-t sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
Rows per page
|
|
||||||
</span>
|
|
||||||
<Select
|
|
||||||
value={String(pagination.pageSize)}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setPagination((p) => ({
|
|
||||||
...p,
|
|
||||||
pageSize: Number(value),
|
|
||||||
pageIndex: 0,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-8 w-[70px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent side="top">
|
|
||||||
{[10, 25, 50, 100].map((size) => (
|
|
||||||
<SelectItem key={size} value={String(size)}>
|
|
||||||
{size}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
Showing{" "}
|
|
||||||
{filteredData.length === 0
|
|
||||||
? 0
|
|
||||||
: pagination.pageIndex * pagination.pageSize + 1}{" "}
|
|
||||||
to{" "}
|
|
||||||
{Math.min(
|
|
||||||
(pagination.pageIndex + 1) * pagination.pageSize,
|
|
||||||
filteredData.length,
|
|
||||||
)}{" "}
|
|
||||||
of {filteredData.length} entries
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<ChevronLeft className="size-4" />
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-8"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
<ChevronRight className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { inferRouterOutputs } from "@trpc/server";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ArrowRight, ListTodo, Loader2, XCircle } from "lucide-react";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import type { AppRouter } from "@/server/api/root";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
type QueueRow =
|
|
||||||
inferRouterOutputs<AppRouter>["deployment"]["queueList"][number];
|
|
||||||
|
|
||||||
const stateVariants: Record<
|
|
||||||
string,
|
|
||||||
| "default"
|
|
||||||
| "secondary"
|
|
||||||
| "destructive"
|
|
||||||
| "outline"
|
|
||||||
| "yellow"
|
|
||||||
| "green"
|
|
||||||
| "red"
|
|
||||||
> = {
|
|
||||||
pending: "secondary",
|
|
||||||
waiting: "secondary",
|
|
||||||
active: "yellow",
|
|
||||||
delayed: "outline",
|
|
||||||
completed: "green",
|
|
||||||
failed: "destructive",
|
|
||||||
cancelled: "outline",
|
|
||||||
paused: "outline",
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatTs(ts?: number): string {
|
|
||||||
if (ts == null) return "—";
|
|
||||||
const d = new Date(ts);
|
|
||||||
return d.toLocaleString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getJobLabel(row: QueueRow): string {
|
|
||||||
const d = row.data as {
|
|
||||||
applicationType?: string;
|
|
||||||
applicationId?: string;
|
|
||||||
composeId?: string;
|
|
||||||
previewDeploymentId?: string;
|
|
||||||
titleLog?: string;
|
|
||||||
type?: string;
|
|
||||||
};
|
|
||||||
if (!d) return String(row.id);
|
|
||||||
const type = d.applicationType ?? "job";
|
|
||||||
const title = d.titleLog ?? "";
|
|
||||||
if (title) return title;
|
|
||||||
if (d.applicationId) return `Application ${d.applicationId.slice(0, 8)}…`;
|
|
||||||
if (d.composeId) return `Compose ${d.composeId.slice(0, 8)}…`;
|
|
||||||
if (d.previewDeploymentId)
|
|
||||||
return `Preview ${d.previewDeploymentId.slice(0, 8)}…`;
|
|
||||||
return `${type} ${String(row.id)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ShowQueueTable(props: { embedded?: boolean }) {
|
|
||||||
const { embedded: _embedded = false } = props;
|
|
||||||
const { data: queueList, isLoading } = api.deployment.queueList.useQuery(
|
|
||||||
undefined,
|
|
||||||
{ refetchInterval: 3000 },
|
|
||||||
);
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const {
|
|
||||||
mutateAsync: cancelApplicationDeployment,
|
|
||||||
isPending: isCancellingApp,
|
|
||||||
} = api.application.cancelDeployment.useMutation({
|
|
||||||
onSuccess: () => void utils.deployment.queueList.invalidate(),
|
|
||||||
});
|
|
||||||
const {
|
|
||||||
mutateAsync: cancelComposeDeployment,
|
|
||||||
isPending: isCancellingCompose,
|
|
||||||
} = api.compose.cancelDeployment.useMutation({
|
|
||||||
onSuccess: () => void utils.deployment.queueList.invalidate(),
|
|
||||||
});
|
|
||||||
const isCancelling = isCancellingApp || isCancellingCompose;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-0">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex gap-4 w-full items-center justify-center min-h-[30vh] text-muted-foreground">
|
|
||||||
<Loader2 className="size-4 animate-spin" />
|
|
||||||
<span>Loading queue...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-md border overflow-x-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Job ID</TableHead>
|
|
||||||
<TableHead>Label</TableHead>
|
|
||||||
<TableHead>Type</TableHead>
|
|
||||||
<TableHead>State</TableHead>
|
|
||||||
<TableHead>Added</TableHead>
|
|
||||||
<TableHead>Processed</TableHead>
|
|
||||||
<TableHead>Finished</TableHead>
|
|
||||||
<TableHead>Error</TableHead>
|
|
||||||
<TableHead className="w-[100px]">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{queueList?.length ? (
|
|
||||||
queueList.map((row) => {
|
|
||||||
const d = row.data as Record<string, unknown>;
|
|
||||||
const appType = d?.applicationType as string | undefined;
|
|
||||||
const pathInfo = row.servicePath;
|
|
||||||
const hasLink = pathInfo?.href != null;
|
|
||||||
return (
|
|
||||||
<TableRow key={String(row.id)}>
|
|
||||||
<TableCell className="font-mono text-xs">
|
|
||||||
{String(row.id)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="max-w-[200px] truncate">
|
|
||||||
{getJobLabel(row)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{appType ?? row.name ?? "—"}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={stateVariants[row.state] ?? "outline"}>
|
|
||||||
{row.state}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground text-xs">
|
|
||||||
{formatTs(row.timestamp)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground text-xs">
|
|
||||||
{formatTs(row.processedOn)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground text-xs">
|
|
||||||
{formatTs(row.finishedOn)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="max-w-[180px] truncate text-xs text-destructive">
|
|
||||||
{row.failedReason ?? "—"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{hasLink ? (
|
|
||||||
<Button variant="ghost" size="sm" asChild>
|
|
||||||
<Link href={pathInfo!.href!}>
|
|
||||||
<ArrowRight className="size-4 mr-1" />
|
|
||||||
Service
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground text-xs">
|
|
||||||
—
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{isCloud &&
|
|
||||||
row.state === "active" &&
|
|
||||||
(d?.applicationId != null ||
|
|
||||||
d?.composeId != null) && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-destructive hover:text-destructive"
|
|
||||||
disabled={isCancelling}
|
|
||||||
onClick={() => {
|
|
||||||
const appId =
|
|
||||||
typeof d.applicationId === "string"
|
|
||||||
? d.applicationId
|
|
||||||
: undefined;
|
|
||||||
const compId =
|
|
||||||
typeof d.composeId === "string"
|
|
||||||
? d.composeId
|
|
||||||
: undefined;
|
|
||||||
if (appId) {
|
|
||||||
void cancelApplicationDeployment({
|
|
||||||
applicationId: appId,
|
|
||||||
});
|
|
||||||
} else if (compId) {
|
|
||||||
void cancelComposeDeployment({
|
|
||||||
composeId: compId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<XCircle className="size-4 mr-1" />
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={9} className="text-center py-12">
|
|
||||||
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground min-h-[30vh]">
|
|
||||||
<ListTodo className="size-8" />
|
|
||||||
<p className="font-medium">Queue is empty</p>
|
|
||||||
<p className="text-sm">
|
|
||||||
Deployment jobs will appear here when they are queued.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { toast } from "sonner";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
containerId: string;
|
|
||||||
serverId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RemoveContainerDialog = ({ containerId, serverId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { mutateAsync, isPending } = api.docker.removeContainer.useMutation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Remove Container
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will permanently remove the container{" "}
|
|
||||||
<span className="font-semibold">{containerId}</span>. If the
|
|
||||||
container is running, it will be forcefully stopped and removed.
|
|
||||||
This action cannot be undone.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
disabled={isPending}
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({ containerId, serverId })
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("Container removed successfully");
|
|
||||||
await utils.docker.getContainers.invalidate();
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(err.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ShowContainerConfig } from "../config/show-container-config";
|
import { ShowContainerConfig } from "../config/show-container-config";
|
||||||
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
|
||||||
import { RemoveContainerDialog } from "../remove/remove-container";
|
|
||||||
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
|
||||||
import type { Container } from "./show-containers";
|
import type { Container } from "./show-containers";
|
||||||
|
|
||||||
@@ -128,10 +127,6 @@ export const columns: ColumnDef<Container>[] = [
|
|||||||
>
|
>
|
||||||
Terminal
|
Terminal
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<RemoveContainerDialog
|
|
||||||
containerId={container.containerId}
|
|
||||||
serverId={container.serverId}
|
|
||||||
/>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,12 +45,10 @@ import {
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
|
||||||
|
|
||||||
type User = typeof authClient.$Infer.Session.user;
|
type User = typeof authClient.$Infer.Session.user;
|
||||||
|
|
||||||
export const ImpersonationBar = () => {
|
export const ImpersonationBar = () => {
|
||||||
const { config: whitelabeling } = useWhitelabeling();
|
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
const [isImpersonating, setIsImpersonating] = useState(false);
|
const [isImpersonating, setIsImpersonating] = useState(false);
|
||||||
@@ -182,10 +180,7 @@ export const ImpersonationBar = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
||||||
<Logo
|
<Logo className="w-10 h-10" />
|
||||||
className="w-10 h-10"
|
|
||||||
logoUrl={whitelabeling?.logoUrl || undefined}
|
|
||||||
/>
|
|
||||||
{!isImpersonating ? (
|
{!isImpersonating ? (
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
|||||||
@@ -1,251 +0,0 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
|
||||||
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 { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const DockerProviderSchema = z.object({
|
|
||||||
externalPort: z.preprocess((a) => {
|
|
||||||
if (a === null || a === undefined || a === "") return null;
|
|
||||||
const parsed = Number.parseInt(String(a), 10);
|
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
|
||||||
}, z
|
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
externalGRPCPort: z.preprocess((a) => {
|
|
||||||
if (a === null || a === undefined || a === "") return null;
|
|
||||||
const parsed = Number.parseInt(String(a), 10);
|
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
|
||||||
}, z
|
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
externalAdminPort: z.preprocess((a) => {
|
|
||||||
if (a === null || a === undefined || a === "") return null;
|
|
||||||
const parsed = Number.parseInt(String(a), 10);
|
|
||||||
return Number.isNaN(parsed) ? null : parsed;
|
|
||||||
}, z
|
|
||||||
.number()
|
|
||||||
.gte(0, "Range must be 0 - 65535")
|
|
||||||
.lte(65535, "Range must be 0 - 65535")
|
|
||||||
.nullable()),
|
|
||||||
});
|
|
||||||
|
|
||||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
libsqlId: string;
|
|
||||||
}
|
|
||||||
export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
|
||||||
const { data, refetch } = api.libsql.one.useQuery({ libsqlId });
|
|
||||||
const { mutateAsync, isPending } = api.libsql.saveExternalPorts.useMutation();
|
|
||||||
const [connectionUrl, setConnectionUrl] = useState("");
|
|
||||||
const [connectionGRPCUrl, setGRPCConnectionUrl] = useState("");
|
|
||||||
const getIp = data?.server?.ipAddress || ip;
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {},
|
|
||||||
resolver: zodResolver(DockerProviderSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
externalPort: data.externalPort,
|
|
||||||
externalGRPCPort: data.externalGRPCPort,
|
|
||||||
externalAdminPort: data.externalAdminPort,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form.reset, data, form]);
|
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
|
||||||
await mutateAsync({
|
|
||||||
externalPort: values.externalPort,
|
|
||||||
externalGRPCPort: values.externalGRPCPort,
|
|
||||||
externalAdminPort: values.externalAdminPort,
|
|
||||||
libsqlId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success("External port/ports updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
|
||||||
.catch((error: Error) => {
|
|
||||||
toast.error(error?.message || "Error saving the external port/ports");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
|
||||||
setConnectionUrl(
|
|
||||||
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data?.sqldNode !== "replica") {
|
|
||||||
const grpcPort = form.watch("externalGRPCPort") || data?.externalGRPCPort;
|
|
||||||
setGRPCConnectionUrl(
|
|
||||||
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${grpcPort}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
data?.externalGRPCPort,
|
|
||||||
data?.databasePassword,
|
|
||||||
form,
|
|
||||||
data?.databaseUser,
|
|
||||||
getIp,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col gap-5">
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
In order to make the database reachable through the internet, you
|
|
||||||
must set a port and ensure that the port is not being used by
|
|
||||||
another application or database
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex w-full flex-col gap-4">
|
|
||||||
{!getIp && (
|
|
||||||
<AlertBlock type="warning">
|
|
||||||
You need to set an IP address in your{" "}
|
|
||||||
<Link href="/dashboard/settings/server" className="text-primary">
|
|
||||||
{data?.serverId
|
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
|
||||||
</Link>{" "}
|
|
||||||
to fix the database url connection.
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-4"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="col-span-2 space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="externalPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>External Port (Internet)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="8080"
|
|
||||||
{...field}
|
|
||||||
value={field.value as string}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!!data?.externalPort && (
|
|
||||||
<div className="grid w-full gap-8">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<Label>External Host</Label>
|
|
||||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="col-span-2 space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="externalAdminPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>External Admin Port (Internet)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="5000"
|
|
||||||
{...field}
|
|
||||||
value={field.value as string}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data?.sqldNode !== "replica" && (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="col-span-2 space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="externalGRPCPort"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>External GRPC Port (Internet)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="5001"
|
|
||||||
{...field}
|
|
||||||
value={field.value as string}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!!data?.externalGRPCPort && (
|
|
||||||
<div className="grid w-full gap-8">
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<Label>External GRPC Host</Label>
|
|
||||||
<ToggleVisibilityInput
|
|
||||||
value={connectionGRPCUrl}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button type="submit" isLoading={isPending}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
libsqlId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
|
||||||
const { data, refetch } = api.libsql.one.useQuery(
|
|
||||||
{
|
|
||||||
libsqlId,
|
|
||||||
},
|
|
||||||
{ enabled: !!libsqlId },
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: reload, isPending: isReloading } =
|
|
||||||
api.libsql.reload.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: start, isPending: isStarting } =
|
|
||||||
api.libsql.start.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: stop, isPending: isStopping } =
|
|
||||||
api.libsql.stop.useMutation();
|
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
|
||||||
api.libsql.deployWithLogs.useSubscription(
|
|
||||||
{
|
|
||||||
libsqlId: libsqlId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: isDeploying,
|
|
||||||
onData(log) {
|
|
||||||
if (!isDrawerOpen) {
|
|
||||||
setIsDrawerOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
|
||||||
setIsDeploying(false);
|
|
||||||
}
|
|
||||||
const parsedLogs = parseLogs(log);
|
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
console.error("Deployment logs error:", error);
|
|
||||||
setIsDeploying(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Deploy Libsql"
|
|
||||||
description="Are you sure you want to deploy this Libsql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Rocket className="size-4 mr-1" />
|
|
||||||
Deploy
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Downloads and sets up the Libsql database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Libsql"
|
|
||||||
description="Are you sure you want to reload this libsql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
libsqlId: libsqlId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Libsql reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Libsql");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isReloading}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Restart the Libsql service without rebuilding</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Start Libsql"
|
|
||||||
description="Are you sure you want to start this Libsql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
libsqlId: libsqlId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Libsql started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Libsql");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isStarting}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
|
||||||
Start
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Start the Libsql database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Libsql"
|
|
||||||
description="Are you sure you want to stop this Libsql?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
libsqlId: libsqlId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Libsql stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping Libsql");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running Libsql database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
<DockerTerminalModal
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
serverId={data?.serverId || ""}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Terminal className="size-4 mr-1" />
|
|
||||||
Open Terminal
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Open a terminal to the Libsql container</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DockerTerminalModal>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<DrawerLogs
|
|
||||||
isOpen={isDrawerOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setIsDrawerOpen(false);
|
|
||||||
setFilteredLogs([]);
|
|
||||||
setIsDeploying(false);
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
filteredLogs={filteredLogs}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { SelectGroup } from "@radix-ui/react-select";
|
|
||||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
libsqlId: string;
|
|
||||||
}
|
|
||||||
export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
|
||||||
const { data } = api.libsql.one.useQuery({ libsqlId });
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
|
||||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label>User</Label>
|
|
||||||
<Input disabled value={data?.databaseUser} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label>Sqld Node</Label>
|
|
||||||
<Select value={data?.sqldNode} disabled>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select Node type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{["primary", "replica"].map((node) => (
|
|
||||||
<SelectItem key={node} value={node}>
|
|
||||||
{node.charAt(0).toUpperCase() + node.slice(1)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label>Password</Label>
|
|
||||||
<div className="flex flex-row gap-4">
|
|
||||||
<ToggleVisibilityInput
|
|
||||||
disabled
|
|
||||||
value={data?.databasePassword}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<div className="w-full flex flex-col gap-2">
|
|
||||||
<Label>Internal Port (Container)</Label>
|
|
||||||
<Input disabled value="8080" />
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-2">
|
|
||||||
<Label>Internal GRPC Port (Container)</Label>
|
|
||||||
<Input disabled value="5001" />
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-2">
|
|
||||||
<Label>Internal Admin Port (Container)</Label>
|
|
||||||
<Input disabled value="5000" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label>Internal Host</Label>
|
|
||||||
<Input disabled value={data?.appName} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label>Enable Namespaces</Label>
|
|
||||||
<Select
|
|
||||||
disabled
|
|
||||||
defaultValue={
|
|
||||||
data?.enableNamespaces
|
|
||||||
? String(data?.enableNamespaces)
|
|
||||||
: "false"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={"false"} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
{["false", "true"].map((node) => (
|
|
||||||
<SelectItem key={node} value={node}>
|
|
||||||
{node.charAt(0).toUpperCase() + node.slice(1)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
|
||||||
<Label>Internal Connection URL </Label>
|
|
||||||
<ToggleVisibilityInput
|
|
||||||
disabled
|
|
||||||
value={`http://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:8080`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 md:col-span-2">
|
|
||||||
<Label>Internal Replication Connection URL </Label>
|
|
||||||
<ToggleVisibilityInput
|
|
||||||
disabled
|
|
||||||
value={`http://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5001`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
|
||||||
import { PenBoxIcon } from "lucide-react";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const updateLibsqlSchema = z.object({
|
|
||||||
name: z.string().min(1, {
|
|
||||||
message: "Name is required",
|
|
||||||
}),
|
|
||||||
description: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type UpdateLibsql = z.infer<typeof updateLibsqlSchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
libsqlId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UpdateLibsql = ({ libsqlId }: Props) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { mutateAsync, error, isError, isPending } =
|
|
||||||
api.libsql.update.useMutation();
|
|
||||||
const { data } = api.libsql.one.useQuery(
|
|
||||||
{
|
|
||||||
libsqlId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!libsqlId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const form = useForm<UpdateLibsql>({
|
|
||||||
defaultValues: {
|
|
||||||
description: data?.description ?? "",
|
|
||||||
name: data?.name ?? "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(updateLibsqlSchema),
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
description: data.description ?? "",
|
|
||||||
name: data.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data, form, form.reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (formData: UpdateLibsql) => {
|
|
||||||
await mutateAsync({
|
|
||||||
name: formData.name,
|
|
||||||
libsqlId: libsqlId,
|
|
||||||
description: formData.description || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Libsql updated successfully");
|
|
||||||
utils.libsql.one.invalidate({
|
|
||||||
libsqlId: libsqlId,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the Libsql");
|
|
||||||
})
|
|
||||||
.finally(() => {});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10 "
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Modify Libsql</DialogTitle>
|
|
||||||
<DialogDescription>Update the Libsql data</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid items-center gap-4">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
id="hook-form-update-libsql"
|
|
||||||
className="grid w-full gap-4 "
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Vandelay Industries" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="description"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Description</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Description about your project..."
|
|
||||||
className="resize-none"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isPending}
|
|
||||||
form="hook-form-update-libsql"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -21,8 +21,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDeploy = permissions?.deployment.create ?? false;
|
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
const { data, refetch } = api.mariadb.one.useQuery(
|
||||||
{
|
{
|
||||||
mariadbId,
|
mariadbId,
|
||||||
@@ -74,75 +72,154 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
{canDeploy && (
|
<TooltipProvider delayDuration={0}>
|
||||||
<TooltipProvider delayDuration={0}>
|
<DialogAction
|
||||||
<DialogAction
|
title="Deploy Mariadb"
|
||||||
title="Deploy Mariadb"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
description="Are you sure you want to deploy this mariadb?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
setIsDeploying(true);
|
||||||
setIsDeploying(true);
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
refetch();
|
||||||
refetch();
|
}}
|
||||||
}}
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Rocket className="size-4 mr-1" />
|
||||||
>
|
Deploy
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<Rocket className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Deploy
|
<p>Downloads and sets up the MariaDB database</p>
|
||||||
</div>
|
</TooltipContent>
|
||||||
</TooltipTrigger>
|
</TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</Button>
|
||||||
<p>Downloads and sets up the MariaDB database</p>
|
</DialogAction>
|
||||||
</TooltipContent>
|
</TooltipProvider>
|
||||||
</TooltipPrimitive.Portal>
|
<TooltipProvider delayDuration={0}>
|
||||||
</Tooltip>
|
<DialogAction
|
||||||
</Button>
|
title="Reload Mariadb"
|
||||||
</DialogAction>
|
description="Are you sure you want to reload this mariadb?"
|
||||||
</TooltipProvider>
|
type="default"
|
||||||
)}
|
onClick={async () => {
|
||||||
{canDeploy && (
|
await reload({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MariaDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mariadb"
|
title="Start Mariadb"
|
||||||
description="Are you sure you want to reload this mariadb?"
|
description="Are you sure you want to start this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await start({
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mariadb reloaded successfully");
|
toast.success("Mariadb started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mariadb");
|
toast.error("Error starting Mariadb");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
Reload
|
Start
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MariaDB service without rebuilding</p>
|
<p>
|
||||||
|
Start the MariaDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mariadb"
|
||||||
|
description="Are you sure you want to stop this mariadb?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MariaDB database</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -150,90 +227,6 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
{canDeploy &&
|
|
||||||
(data?.applicationStatus === "idle" ? (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Start Mariadb"
|
|
||||||
description="Are you sure you want to start this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
mariadbId: mariadbId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isStarting}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
|
||||||
Start
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Start the MariaDB database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Mariadb"
|
|
||||||
description="Are you sure you want to stop this mariadb?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
mariadbId: mariadbId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running MariaDB database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
))}
|
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDeploy = permissions?.deployment.create ?? false;
|
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
@@ -75,158 +73,153 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
{canDeploy && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Deploy Mongo"
|
||||||
title="Deploy Mongo"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
description="Are you sure you want to deploy this mongo?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
setIsDeploying(true);
|
||||||
setIsDeploying(true);
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
refetch();
|
||||||
refetch();
|
}}
|
||||||
}}
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Rocket className="size-4 mr-1" />
|
||||||
>
|
Deploy
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<Rocket className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Deploy
|
<p>Downloads and sets up the MongoDB database</p>
|
||||||
</div>
|
</TooltipContent>
|
||||||
</TooltipTrigger>
|
</TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</Button>
|
||||||
<p>Downloads and sets up the MongoDB database</p>
|
</DialogAction>
|
||||||
</TooltipContent>
|
<DialogAction
|
||||||
</TooltipPrimitive.Portal>
|
title="Reload Mongo"
|
||||||
</Tooltip>
|
description="Are you sure you want to reload this mongo?"
|
||||||
</Button>
|
type="default"
|
||||||
</DialogAction>
|
onClick={async () => {
|
||||||
)}
|
await reload({
|
||||||
{canDeploy && (
|
mongoId: mongoId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MongoDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Mongo"
|
title="Start Mongo"
|
||||||
description="Are you sure you want to reload this mongo?"
|
description="Are you sure you want to start this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await start({
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Mongo reloaded successfully");
|
toast.success("Mongo started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Mongo");
|
toast.error("Error starting Mongo");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
Reload
|
Start
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MongoDB service without rebuilding</p>
|
<p>
|
||||||
|
Start the MongoDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mongo"
|
||||||
|
description="Are you sure you want to stop this mongo?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mongoId: mongoId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MongoDB database</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
{canDeploy &&
|
|
||||||
(data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
|
||||||
title="Start Mongo"
|
|
||||||
description="Are you sure you want to start this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
mongoId: mongoId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mongo");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isStarting}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
|
||||||
Start
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Start the MongoDB database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Mongo"
|
|
||||||
description="Are you sure you want to stop this mongo?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
mongoId: mongoId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping Mongo");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running MongoDB database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
))}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDeploy = permissions?.deployment.create ?? false;
|
|
||||||
const { data, refetch } = api.mysql.one.useQuery(
|
const { data, refetch } = api.mysql.one.useQuery(
|
||||||
{
|
{
|
||||||
mysqlId,
|
mysqlId,
|
||||||
@@ -73,158 +71,153 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
{canDeploy && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Deploy MySQL"
|
||||||
title="Deploy MySQL"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
description="Are you sure you want to deploy this mysql?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
setIsDeploying(true);
|
||||||
setIsDeploying(true);
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
refetch();
|
||||||
refetch();
|
}}
|
||||||
}}
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Rocket className="size-4 mr-1" />
|
||||||
>
|
Deploy
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<Rocket className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Deploy
|
<p>Downloads and sets up the MySQL database</p>
|
||||||
</div>
|
</TooltipContent>
|
||||||
</TooltipTrigger>
|
</TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</Button>
|
||||||
<p>Downloads and sets up the MySQL database</p>
|
</DialogAction>
|
||||||
</TooltipContent>
|
<DialogAction
|
||||||
</TooltipPrimitive.Portal>
|
title="Reload MySQL"
|
||||||
</Tooltip>
|
description="Are you sure you want to reload this mysql?"
|
||||||
</Button>
|
type="default"
|
||||||
</DialogAction>
|
onClick={async () => {
|
||||||
)}
|
await reload({
|
||||||
{canDeploy && (
|
mysqlId: mysqlId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("MySQL reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading MySQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MySQL service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload MySQL"
|
title="Start MySQL"
|
||||||
description="Are you sure you want to reload this mysql?"
|
description="Are you sure you want to start this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await start({
|
||||||
mysqlId: mysqlId,
|
mysqlId: mysqlId,
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("MySQL reloaded successfully");
|
toast.success("MySQL started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading MySQL");
|
toast.error("Error starting MySQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
Reload
|
Start
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the MySQL service without rebuilding</p>
|
<p>
|
||||||
|
Start the MySQL database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop MySQL"
|
||||||
|
description="Are you sure you want to stop this mysql?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("MySQL stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping MySQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MySQL database</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
{canDeploy &&
|
|
||||||
(data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
|
||||||
title="Start MySQL"
|
|
||||||
description="Are you sure you want to start this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
mysqlId: mysqlId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("MySQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting MySQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isStarting}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
|
||||||
Start
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Start the MySQL database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop MySQL"
|
|
||||||
description="Are you sure you want to stop this mysql?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
mysqlId: mysqlId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("MySQL stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping MySQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running MySQL database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
))}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const organizationSchema = z.object({
|
const organizationSchema = z.object({
|
||||||
@@ -54,6 +55,8 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
const { mutateAsync, isPending } = organizationId
|
const { mutateAsync, isPending } = organizationId
|
||||||
? api.organization.update.useMutation()
|
? api.organization.update.useMutation()
|
||||||
: api.organization.create.useMutation();
|
: api.organization.create.useMutation();
|
||||||
|
const { refetch: refetchActiveOrganization } =
|
||||||
|
authClient.useActiveOrganization();
|
||||||
|
|
||||||
const form = useForm<OrganizationFormValues>({
|
const form = useForm<OrganizationFormValues>({
|
||||||
resolver: zodResolver(organizationSchema),
|
resolver: zodResolver(organizationSchema),
|
||||||
@@ -86,7 +89,7 @@ export function AddOrganization({ organizationId }: Props) {
|
|||||||
utils.organization.all.invalidate();
|
utils.organization.all.invalidate();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
utils.organization.one.invalidate({ organizationId });
|
utils.organization.one.invalidate({ organizationId });
|
||||||
utils.organization.active.invalidate();
|
refetchActiveOrganization();
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
|||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
|
||||||
mariadb: () =>
|
mariadb: () =>
|
||||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||||
application: () =>
|
application: () =>
|
||||||
@@ -57,7 +56,6 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
|||||||
postgres: () => api.postgres.update.useMutation(),
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
redis: () => api.redis.update.useMutation(),
|
redis: () => api.redis.update.useMutation(),
|
||||||
mysql: () => api.mysql.update.useMutation(),
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
libsql: () => api.libsql.update.useMutation(),
|
|
||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
application: () => api.application.update.useMutation(),
|
application: () => api.application.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
@@ -86,7 +84,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
dockerImage: data.dockerImage,
|
dockerImage: data.dockerImage,
|
||||||
command: data.command || "",
|
command: data.command || "",
|
||||||
args: (data as any).args?.map((arg: string) => ({ value: arg })) || [],
|
args: data.args?.map((arg) => ({ value: arg })) || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
@@ -97,7 +95,6 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
|||||||
postgresId: id || "",
|
postgresId: id || "",
|
||||||
redisId: id || "",
|
redisId: id || "",
|
||||||
mysqlId: id || "",
|
mysqlId: id || "",
|
||||||
libsqlId: id || "",
|
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
dockerImage: formData?.dockerImage,
|
dockerImage: formData?.dockerImage,
|
||||||
command: formData?.command,
|
command: formData?.command,
|
||||||
@@ -147,14 +144,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Command</FormLabel>
|
<FormLabel>Command</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="/bin/sh" {...field} />
|
||||||
placeholder={
|
|
||||||
type === "libsql"
|
|
||||||
? "sqld --db-path iku.db --http-listen-addr 0.0.0.0:8080 --grpc-listen-addr 0.0.0.0:5001 --admin-listen-addr 0.0.0.0:5000"
|
|
||||||
: "Custom command"
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDeploy = permissions?.deployment.create ?? false;
|
|
||||||
const { data, refetch } = api.postgres.one.useQuery(
|
const { data, refetch } = api.postgres.one.useQuery(
|
||||||
{
|
{
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
@@ -75,162 +73,153 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider disableHoverableContent={false}>
|
<TooltipProvider disableHoverableContent={false}>
|
||||||
{canDeploy && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Deploy PostgreSQL"
|
||||||
title="Deploy PostgreSQL"
|
description="Are you sure you want to deploy this postgres?"
|
||||||
description="Are you sure you want to deploy this postgres?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
setIsDeploying(true);
|
||||||
setIsDeploying(true);
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
refetch();
|
||||||
refetch();
|
}}
|
||||||
}}
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Rocket className="size-4 mr-1" />
|
||||||
>
|
Deploy
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<Rocket className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Deploy
|
<p>Downloads and sets up the PostgreSQL database</p>
|
||||||
</div>
|
</TooltipContent>
|
||||||
</TooltipTrigger>
|
</TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</Button>
|
||||||
<p>Downloads and sets up the PostgreSQL database</p>
|
</DialogAction>
|
||||||
</TooltipContent>
|
<DialogAction
|
||||||
</TooltipPrimitive.Portal>
|
title="Reload PostgreSQL"
|
||||||
</Tooltip>
|
description="Are you sure you want to reload this postgres?"
|
||||||
</Button>
|
type="default"
|
||||||
</DialogAction>
|
onClick={async () => {
|
||||||
)}
|
await reload({
|
||||||
{canDeploy && (
|
postgresId: postgresId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("PostgreSQL reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading PostgreSQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the PostgreSQL service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload PostgreSQL"
|
title="Start PostgreSQL"
|
||||||
description="Are you sure you want to reload this postgres?"
|
description="Are you sure you want to start this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await start({
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("PostgreSQL reloaded successfully");
|
toast.success("PostgreSQL started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading PostgreSQL");
|
toast.error("Error starting PostgreSQL");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
Reload
|
Start
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Restart the PostgreSQL service without rebuilding
|
Start the PostgreSQL database (requires a previous
|
||||||
|
successful setup)
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop PostgreSQL"
|
||||||
|
description="Are you sure you want to stop this postgres?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
postgresId: postgresId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("PostgreSQL stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping PostgreSQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running PostgreSQL database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
{canDeploy &&
|
|
||||||
(data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
|
||||||
title="Start PostgreSQL"
|
|
||||||
description="Are you sure you want to start this postgres?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
postgresId: postgresId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("PostgreSQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting PostgreSQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isStarting}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
|
||||||
Start
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Start the PostgreSQL database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop PostgreSQL"
|
|
||||||
description="Are you sure you want to stop this postgres?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
postgresId: postgresId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("PostgreSQL stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping PostgreSQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Stop the currently running PostgreSQL database
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
))}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
// Get environment data to extract projectId
|
// Get environment data to extract projectId
|
||||||
// const { data: environment } = api.environment.one.useQuery({ environmentId });
|
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||||
|
|
||||||
const hasServers = servers && servers.length > 0;
|
const hasServers = servers && servers.length > 0;
|
||||||
// Show dropdown logic based on cloud environment
|
// Show dropdown logic based on cloud environment
|
||||||
@@ -117,8 +117,6 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
await utils.environment.one.invalidate({
|
await utils.environment.one.invalidate({
|
||||||
environmentId,
|
environmentId,
|
||||||
});
|
});
|
||||||
// Invalidate the project query to refresh the project data for the advance-breadcrumb
|
|
||||||
await utils.project.all.invalidate();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating the compose");
|
toast.error("Error creating the compose");
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
LibsqlIcon,
|
|
||||||
MariadbIcon,
|
MariadbIcon,
|
||||||
MongodbIcon,
|
MongodbIcon,
|
||||||
MysqlIcon,
|
MysqlIcon,
|
||||||
@@ -56,7 +55,6 @@ import { api } from "@/utils/api";
|
|||||||
type DbType = z.infer<typeof mySchema>["type"];
|
type DbType = z.infer<typeof mySchema>["type"];
|
||||||
|
|
||||||
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||||
libsql: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
|
||||||
mongo: "mongo:7",
|
mongo: "mongo:7",
|
||||||
mariadb: "mariadb:11",
|
mariadb: "mariadb:11",
|
||||||
mysql: "mysql:8",
|
mysql: "mysql:8",
|
||||||
@@ -68,9 +66,8 @@ const databasesUserDefaultPlaceholder: Record<
|
|||||||
Exclude<DbType, "redis">,
|
Exclude<DbType, "redis">,
|
||||||
string
|
string
|
||||||
> = {
|
> = {
|
||||||
libsql: "libsql",
|
|
||||||
mariadb: "mariadb",
|
|
||||||
mongo: "mongo",
|
mongo: "mongo",
|
||||||
|
mariadb: "mariadb",
|
||||||
mysql: "mysql",
|
mysql: "mysql",
|
||||||
postgres: "postgres",
|
postgres: "postgres",
|
||||||
};
|
};
|
||||||
@@ -97,88 +94,56 @@ const baseDatabaseSchema = z.object({
|
|||||||
serverId: z.string().nullable(),
|
serverId: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mySchema = z
|
const mySchema = z.discriminatedUnion("type", [
|
||||||
.discriminatedUnion("type", [
|
z
|
||||||
z
|
.object({
|
||||||
.object({
|
type: z.literal("postgres"),
|
||||||
type: z.literal("libsql"),
|
databaseName: z.string().default("postgres"),
|
||||||
dockerImage: z
|
databaseUser: z.string().default("postgres"),
|
||||||
.string()
|
})
|
||||||
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
.merge(baseDatabaseSchema),
|
||||||
databaseUser: z.string().default("libsql"),
|
z
|
||||||
sqldNode: z.enum(["primary", "replica"]).default("primary"),
|
.object({
|
||||||
sqldPrimaryUrl: z.string().optional(),
|
type: z.literal("mongo"),
|
||||||
enableNamespaces: z.boolean().default(false),
|
databaseUser: z.string().default("mongo"),
|
||||||
})
|
replicaSets: z.boolean().default(false),
|
||||||
.merge(baseDatabaseSchema),
|
})
|
||||||
z
|
.merge(baseDatabaseSchema),
|
||||||
.object({
|
z
|
||||||
type: z.literal("mariadb"),
|
.object({
|
||||||
dockerImage: z.string().default("mariadb:4"),
|
type: z.literal("redis"),
|
||||||
databaseRootPassword: z
|
})
|
||||||
.string()
|
.merge(baseDatabaseSchema),
|
||||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
z
|
||||||
message:
|
.object({
|
||||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
type: z.literal("mysql"),
|
||||||
})
|
databaseRootPassword: z
|
||||||
.optional(),
|
.string()
|
||||||
databaseUser: z.string().default("mariadb"),
|
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||||
databaseName: z.string().default("mariadb"),
|
|
||||||
})
|
|
||||||
.merge(baseDatabaseSchema),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
type: z.literal("mongo"),
|
|
||||||
databaseUser: z.string().default("mongo"),
|
|
||||||
replicaSets: z.boolean().default(false),
|
|
||||||
})
|
|
||||||
.merge(baseDatabaseSchema),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
type: z.literal("mysql"),
|
|
||||||
databaseRootPassword: z
|
|
||||||
.string()
|
|
||||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
|
||||||
message:
|
|
||||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
databaseUser: z.string().default("mysql"),
|
|
||||||
databaseName: z.string().default("mysql"),
|
|
||||||
})
|
|
||||||
.merge(baseDatabaseSchema),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
type: z.literal("postgres"),
|
|
||||||
databaseName: z.string().default("postgres"),
|
|
||||||
databaseUser: z.string().default("postgres"),
|
|
||||||
})
|
|
||||||
.merge(baseDatabaseSchema),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
type: z.literal("redis"),
|
|
||||||
})
|
|
||||||
.merge(baseDatabaseSchema),
|
|
||||||
])
|
|
||||||
.superRefine((data, ctx) => {
|
|
||||||
if (data.type === "libsql") {
|
|
||||||
if (data.sqldNode === "replica" && !data.sqldPrimaryUrl) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
path: ["sqldPrimaryUrl"],
|
|
||||||
message: "sqldPrimaryUrl is required when sqldNode is 'replica'.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data.sqldNode !== "replica" && data.sqldPrimaryUrl) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
path: ["sqldPrimaryUrl"],
|
|
||||||
message:
|
message:
|
||||||
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
|
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||||
});
|
})
|
||||||
}
|
.optional(),
|
||||||
}
|
databaseUser: z.string().default("mysql"),
|
||||||
});
|
databaseName: z.string().default("mysql"),
|
||||||
|
})
|
||||||
|
.merge(baseDatabaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("mariadb"),
|
||||||
|
dockerImage: z.string().default("mariadb:4"),
|
||||||
|
databaseRootPassword: z
|
||||||
|
.string()
|
||||||
|
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||||
|
message:
|
||||||
|
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
databaseUser: z.string().default("mariadb"),
|
||||||
|
databaseName: z.string().default("mariadb"),
|
||||||
|
})
|
||||||
|
.merge(baseDatabaseSchema),
|
||||||
|
]);
|
||||||
|
|
||||||
const databasesMap = {
|
const databasesMap = {
|
||||||
postgres: {
|
postgres: {
|
||||||
@@ -201,10 +166,6 @@ const databasesMap = {
|
|||||||
icon: <RedisIcon />,
|
icon: <RedisIcon />,
|
||||||
label: "Redis",
|
label: "Redis",
|
||||||
},
|
},
|
||||||
libsql: {
|
|
||||||
icon: <LibsqlIcon className="size-10" />,
|
|
||||||
label: "libSQL",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type AddDatabase = z.infer<typeof mySchema>;
|
type AddDatabase = z.infer<typeof mySchema>;
|
||||||
@@ -220,12 +181,11 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
const slug = slugify(projectName);
|
const slug = slugify(projectName);
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
const libsqlMutation = api.libsql.create.useMutation();
|
|
||||||
const mariadbMutation = api.mariadb.create.useMutation();
|
|
||||||
const mongoMutation = api.mongo.create.useMutation();
|
|
||||||
const mysqlMutation = api.mysql.create.useMutation();
|
|
||||||
const postgresMutation = api.postgres.create.useMutation();
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
|
const mongoMutation = api.mongo.create.useMutation();
|
||||||
const redisMutation = api.redis.create.useMutation();
|
const redisMutation = api.redis.create.useMutation();
|
||||||
|
const mariadbMutation = api.mariadb.create.useMutation();
|
||||||
|
const mysqlMutation = api.mysql.create.useMutation();
|
||||||
|
|
||||||
// Get environment data to extract projectId
|
// Get environment data to extract projectId
|
||||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||||
@@ -250,15 +210,13 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
},
|
},
|
||||||
resolver: zodResolver(mySchema),
|
resolver: zodResolver(mySchema),
|
||||||
});
|
});
|
||||||
const sqldNode = form.watch("sqldNode");
|
|
||||||
const type = form.watch("type");
|
const type = form.watch("type");
|
||||||
const activeMutation = {
|
const activeMutation = {
|
||||||
libsql: libsqlMutation,
|
|
||||||
mariadb: mariadbMutation,
|
|
||||||
mongo: mongoMutation,
|
|
||||||
mysql: mysqlMutation,
|
|
||||||
postgres: postgresMutation,
|
postgres: postgresMutation,
|
||||||
|
mongo: mongoMutation,
|
||||||
redis: redisMutation,
|
redis: redisMutation,
|
||||||
|
mariadb: mariadbMutation,
|
||||||
|
mysql: mysqlMutation,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: AddDatabase) => {
|
const onSubmit = async (data: AddDatabase) => {
|
||||||
@@ -275,23 +233,12 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.type === "libsql") {
|
if (data.type === "postgres") {
|
||||||
promise = libsqlMutation.mutateAsync({
|
promise = postgresMutation.mutateAsync({
|
||||||
...commonParams,
|
|
||||||
sqldNode: data.sqldNode,
|
|
||||||
sqldPrimaryUrl: data.sqldPrimaryUrl ?? null,
|
|
||||||
enableNamespaces: data.enableNamespaces,
|
|
||||||
databasePassword: data.databasePassword,
|
|
||||||
databaseUser:
|
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
|
||||||
});
|
|
||||||
} else if (data.type === "mariadb") {
|
|
||||||
promise = mariadbMutation.mutateAsync({
|
|
||||||
...commonParams,
|
...commonParams,
|
||||||
databasePassword: data.databasePassword,
|
databasePassword: data.databasePassword,
|
||||||
databaseRootPassword: data.databaseRootPassword || "",
|
databaseName: data.databaseName || "postgres",
|
||||||
databaseName: data.databaseName || "mariadb",
|
|
||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||||
@@ -305,6 +252,22 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||||
replicaSets: data.replicaSets,
|
replicaSets: data.replicaSets,
|
||||||
});
|
});
|
||||||
|
} else if (data.type === "redis") {
|
||||||
|
promise = redisMutation.mutateAsync({
|
||||||
|
...commonParams,
|
||||||
|
databasePassword: data.databasePassword,
|
||||||
|
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||||
|
});
|
||||||
|
} else if (data.type === "mariadb") {
|
||||||
|
promise = mariadbMutation.mutateAsync({
|
||||||
|
...commonParams,
|
||||||
|
databasePassword: data.databasePassword,
|
||||||
|
databaseRootPassword: data.databaseRootPassword || "",
|
||||||
|
databaseName: data.databaseName || "mariadb",
|
||||||
|
databaseUser:
|
||||||
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
|
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||||
|
});
|
||||||
} else if (data.type === "mysql") {
|
} else if (data.type === "mysql") {
|
||||||
promise = mysqlMutation.mutateAsync({
|
promise = mysqlMutation.mutateAsync({
|
||||||
...commonParams,
|
...commonParams,
|
||||||
@@ -315,21 +278,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||||
databaseRootPassword: data.databaseRootPassword || "",
|
databaseRootPassword: data.databaseRootPassword || "",
|
||||||
});
|
});
|
||||||
} else if (data.type === "postgres") {
|
|
||||||
promise = postgresMutation.mutateAsync({
|
|
||||||
...commonParams,
|
|
||||||
databasePassword: data.databasePassword,
|
|
||||||
databaseName: data.databaseName || "postgres",
|
|
||||||
databaseUser:
|
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
|
||||||
});
|
|
||||||
} else if (data.type === "redis") {
|
|
||||||
promise = redisMutation.mutateAsync({
|
|
||||||
...commonParams,
|
|
||||||
databasePassword: data.databasePassword,
|
|
||||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
@@ -357,7 +305,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={visible} onOpenChange={setVisible}>
|
<Dialog open={visible} onOpenChange={setVisible}>
|
||||||
<DialogTrigger className="w-full">
|
<DialogTrigger className="w-full">
|
||||||
@@ -559,8 +506,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{(type === "mariadb" ||
|
{(type === "mysql" ||
|
||||||
type === "mysql" ||
|
type === "mariadb" ||
|
||||||
type === "postgres") && (
|
type === "postgres") && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -577,101 +524,10 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{(type === "mysql" ||
|
||||||
{type === "libsql" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sqldNode"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Sqld Node</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value || "primary"}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={"primary"} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
{["primary", "replica"].map((node) => (
|
|
||||||
<SelectItem key={node} value={node}>
|
|
||||||
{node.charAt(0).toUpperCase() + node.slice(1)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{type === "libsql" && sqldNode === "replica" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="sqldPrimaryUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Sqld Primary URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder={"https://<host>:<port>"}
|
|
||||||
autoComplete="off"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{type === "libsql" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="enableNamespaces"
|
|
||||||
render={({ field }) => {
|
|
||||||
console.log(field.value);
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Enable Namespaces</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) =>
|
|
||||||
field.onChange(Boolean(value))
|
|
||||||
}
|
|
||||||
defaultValue={
|
|
||||||
field.value ? String(field.value) : "false"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={"false"} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup>
|
|
||||||
{["false", "true"].map((node) => (
|
|
||||||
<SelectItem key={node} value={node}>
|
|
||||||
{node.charAt(0).toUpperCase() +
|
|
||||||
node.slice(1)}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{(type === "libsql" ||
|
|
||||||
type === "mariadb" ||
|
type === "mariadb" ||
|
||||||
type === "mongo" ||
|
type === "postgres" ||
|
||||||
type === "mysql" ||
|
type === "mongo") && (
|
||||||
type === "postgres") && (
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="databaseUser"
|
name="databaseUser"
|
||||||
@@ -712,7 +568,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{(type === "mariadb" || type === "mysql") && (
|
{(type === "mysql" || type === "mariadb") && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="databaseRootPassword"
|
name="databaseRootPassword"
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
viewMode === "detailed" && "border-b",
|
viewMode === "detailed" && "border-b",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/** biome-ignore lint/performance/noImgElement: this is a valid use for img tag */}
|
|
||||||
<img
|
<img
|
||||||
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
|
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -57,13 +57,19 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
// Get current user's permissions
|
// Get current user's permissions
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: currentUser } = api.user.get.useQuery();
|
||||||
|
|
||||||
// Check if user can create environments
|
// Check if user can create environments
|
||||||
const canCreateEnvironments = !!permissions?.environment.create;
|
const canCreateEnvironments =
|
||||||
|
currentUser?.role === "owner" ||
|
||||||
|
currentUser?.role === "admin" ||
|
||||||
|
currentUser?.canCreateEnvironments === true;
|
||||||
|
|
||||||
// Check if user can delete environments
|
// Check if user can delete environments
|
||||||
const canDeleteEnvironments = !!permissions?.environment.delete;
|
const canDeleteEnvironments =
|
||||||
|
currentUser?.role === "owner" ||
|
||||||
|
currentUser?.role === "admin" ||
|
||||||
|
currentUser?.canDeleteEnvironments === true;
|
||||||
|
|
||||||
const haveServices =
|
const haveServices =
|
||||||
selectedEnvironment &&
|
selectedEnvironment &&
|
||||||
@@ -92,8 +98,6 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
|
|
||||||
toast.success("Environment created successfully");
|
toast.success("Environment created successfully");
|
||||||
utils.environment.byProjectId.invalidate({ projectId });
|
utils.environment.byProjectId.invalidate({ projectId });
|
||||||
// Invalidate the project query to refresh the project data for the advance-breadcrumb
|
|
||||||
utils.project.all.invalidate();
|
|
||||||
setIsCreateDialogOpen(false);
|
setIsCreateDialogOpen(false);
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
|||||||
@@ -28,14 +28,13 @@ export type Services = {
|
|||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
type:
|
type:
|
||||||
| "application"
|
|
||||||
| "compose"
|
|
||||||
| "libsql"
|
|
||||||
| "mariadb"
|
| "mariadb"
|
||||||
| "mongo"
|
| "application"
|
||||||
| "mysql"
|
|
||||||
| "postgres"
|
| "postgres"
|
||||||
| "redis";
|
| "mysql"
|
||||||
|
| "mongo"
|
||||||
|
| "redis"
|
||||||
|
| "compose";
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canRead = permissions?.environmentEnvVars.read ?? false;
|
|
||||||
const canWrite = permissions?.environmentEnvVars.write ?? false;
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isPending } =
|
const { mutateAsync, error, isError, isPending } =
|
||||||
@@ -100,10 +97,6 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [form, onSubmit, isPending, isOpen]);
|
}, [form, onSubmit, isPending, isOpen]);
|
||||||
|
|
||||||
if (!canRead) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -148,7 +141,6 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
language="properties"
|
language="properties"
|
||||||
readOnly={!canWrite}
|
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`NODE_ENV=development
|
placeholder={`NODE_ENV=development
|
||||||
DATABASE_URL=postgresql://localhost:5432/mydb
|
DATABASE_URL=postgresql://localhost:5432/mydb
|
||||||
@@ -165,13 +157,11 @@ API_KEY=your-api-key-here
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{canWrite && (
|
<DialogFooter>
|
||||||
<DialogFooter>
|
<Button isLoading={isPending} type="submit">
|
||||||
<Button isLoading={isPending} type="submit">
|
Update
|
||||||
Update
|
</Button>
|
||||||
</Button>
|
</DialogFooter>
|
||||||
</DialogFooter>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useForm } from "react-hook-form";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { TagSelector } from "@/components/shared/tag-selector";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -63,7 +62,6 @@ interface Props {
|
|||||||
export const HandleProject = ({ projectId }: Props) => {
|
export const HandleProject = ({ projectId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const { mutateAsync, error, isError } = projectId
|
const { mutateAsync, error, isError } = projectId
|
||||||
? api.project.update.useMutation()
|
? api.project.update.useMutation()
|
||||||
@@ -77,10 +75,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
enabled: !!projectId,
|
enabled: !!projectId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: availableTags = [] } = api.tag.all.useQuery();
|
|
||||||
const bulkAssignMutation = api.tag.bulkAssign.useMutation();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<AddProject>({
|
const form = useForm<AddProject>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -95,13 +89,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
description: data?.description ?? "",
|
description: data?.description ?? "",
|
||||||
name: data?.name ?? "",
|
name: data?.name ?? "",
|
||||||
});
|
});
|
||||||
// Load existing tags when editing a project
|
|
||||||
if (data?.projectTags) {
|
|
||||||
const tagIds = data.projectTags.map((pt) => pt.tagId);
|
|
||||||
setSelectedTagIds(tagIds);
|
|
||||||
} else {
|
|
||||||
setSelectedTagIds([]);
|
|
||||||
}
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddProject) => {
|
const onSubmit = async (data: AddProject) => {
|
||||||
@@ -111,26 +98,12 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
projectId: projectId || "",
|
projectId: projectId || "",
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
// Assign tags to the project (both create and update)
|
|
||||||
const projectIdToUse =
|
|
||||||
projectId ||
|
|
||||||
(data && "project" in data ? data.project.projectId : undefined);
|
|
||||||
|
|
||||||
if (projectIdToUse) {
|
|
||||||
try {
|
|
||||||
await bulkAssignMutation.mutateAsync({
|
|
||||||
projectId: projectIdToUse,
|
|
||||||
tagIds: selectedTagIds,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
toast.error("Failed to assign tags to project");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.project.all.invalidate();
|
await utils.project.all.invalidate();
|
||||||
toast.success(projectId ? "Project Updated" : "Project Created");
|
toast.success(projectId ? "Project Updated" : "Project Created");
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
|
const projectIdToUse =
|
||||||
|
data && "project" in data ? data.project.projectId : undefined;
|
||||||
const environmentIdToUse =
|
const environmentIdToUse =
|
||||||
data && "environment" in data
|
data && "environment" in data
|
||||||
? data.environment.environmentId
|
? data.environment.environmentId
|
||||||
@@ -217,20 +190,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<FormLabel>Tags</FormLabel>
|
|
||||||
<TagSelector
|
|
||||||
tags={availableTags.map((tag) => ({
|
|
||||||
id: tag.tagId,
|
|
||||||
name: tag.name,
|
|
||||||
color: tag.color ?? undefined,
|
|
||||||
}))}
|
|
||||||
selectedTags={selectedTagIds}
|
|
||||||
onTagsChange={setSelectedTagIds}
|
|
||||||
placeholder="Select tags..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canRead = permissions?.projectEnvVars.read ?? false;
|
|
||||||
const canWrite = permissions?.projectEnvVars.write ?? false;
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isPending } =
|
const { mutateAsync, error, isError, isPending } =
|
||||||
@@ -99,10 +96,6 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [form, onSubmit, isPending, isOpen]);
|
}, [form, onSubmit, isPending, isOpen]);
|
||||||
|
|
||||||
if (!canRead) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -146,7 +139,6 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
language="properties"
|
language="properties"
|
||||||
readOnly={!canWrite}
|
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
@@ -162,13 +154,11 @@ PORT=3000
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{canWrite && (
|
<DialogFooter>
|
||||||
<DialogFooter>
|
<Button isLoading={isPending} type="submit">
|
||||||
<Button isLoading={isPending} type="submit">
|
Update
|
||||||
Update
|
</Button>
|
||||||
</Button>
|
</DialogFooter>
|
||||||
</DialogFooter>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import { toast } from "sonner";
|
|||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||||
import { TagBadge } from "@/components/shared/tag-badge";
|
|
||||||
import { TagFilter } from "@/components/shared/tag-filter";
|
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -51,6 +49,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { TimeBadge } from "@/components/ui/time-badge";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useDebounce } from "@/utils/hooks/use-debounce";
|
import { useDebounce } from "@/utils/hooks/use-debounce";
|
||||||
import { HandleProject } from "./handle-project";
|
import { HandleProject } from "./handle-project";
|
||||||
@@ -62,9 +61,7 @@ export const ShowProjects = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isPending } = api.project.all.useQuery();
|
const { data, isPending } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
const { data: availableTags } = api.tag.all.useQuery();
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState(
|
const [searchQuery, setSearchQuery] = useState(
|
||||||
router.isReady && typeof router.query.q === "string" ? router.query.q : "",
|
router.isReady && typeof router.query.q === "string" ? router.query.q : "",
|
||||||
@@ -78,31 +75,10 @@ export const ShowProjects = () => {
|
|||||||
return "createdAt-desc";
|
return "createdAt-desc";
|
||||||
});
|
});
|
||||||
|
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>(() => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const saved = localStorage.getItem("projectsTagFilter");
|
|
||||||
return saved ? JSON.parse(saved) : [];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("projectsSort", sortBy);
|
localStorage.setItem("projectsSort", sortBy);
|
||||||
}, [sortBy]);
|
}, [sortBy]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem("projectsTagFilter", JSON.stringify(selectedTagIds));
|
|
||||||
}, [selectedTagIds]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!availableTags) return;
|
|
||||||
const validIds = new Set(availableTags.map((t) => t.tagId));
|
|
||||||
setSelectedTagIds((prev) => {
|
|
||||||
const filtered = prev.filter((id) => validIds.has(id));
|
|
||||||
return filtered.length === prev.length ? prev : filtered;
|
|
||||||
});
|
|
||||||
}, [availableTags]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.isReady) return;
|
if (!router.isReady) return;
|
||||||
const urlQuery = typeof router.query.q === "string" ? router.query.q : "";
|
const urlQuery = typeof router.query.q === "string" ? router.query.q : "";
|
||||||
@@ -130,7 +106,7 @@ export const ShowProjects = () => {
|
|||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
let filtered = data.filter(
|
const filtered = data.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.name
|
project.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -140,15 +116,6 @@ export const ShowProjects = () => {
|
|||||||
.includes(debouncedSearchQuery.toLowerCase()),
|
.includes(debouncedSearchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter by selected tags (OR logic: show projects with ANY selected tag)
|
|
||||||
if (selectedTagIds.length > 0) {
|
|
||||||
filtered = filtered.filter((project) =>
|
|
||||||
project.projectTags?.some((pt) =>
|
|
||||||
selectedTagIds.includes(pt.tag.tagId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then sort the filtered results
|
// Then sort the filtered results
|
||||||
const [field, direction] = sortBy.split("-");
|
const [field, direction] = sortBy.split("-");
|
||||||
return [...filtered].sort((a, b) => {
|
return [...filtered].sort((a, b) => {
|
||||||
@@ -194,13 +161,18 @@ export const ShowProjects = () => {
|
|||||||
}
|
}
|
||||||
return direction === "asc" ? comparison : -comparison;
|
return direction === "asc" ? comparison : -comparison;
|
||||||
});
|
});
|
||||||
}, [data, debouncedSearchQuery, sortBy, selectedTagIds]);
|
}, [data, debouncedSearchQuery, sortBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||||
/>
|
/>
|
||||||
|
{!isCloud && (
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<TimeBadge />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
@@ -214,7 +186,9 @@ export const ShowProjects = () => {
|
|||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{permissions?.project.create && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canCreateProjects) && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -240,44 +214,29 @@ export const ShowProjects = () => {
|
|||||||
|
|
||||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
||||||
<TagFilter
|
<ArrowUpDown className="size-4 text-muted-foreground" />
|
||||||
tags={
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
availableTags?.map((tag) => ({
|
<SelectTrigger className="w-full">
|
||||||
id: tag.tagId,
|
<SelectValue placeholder="Sort by..." />
|
||||||
name: tag.name,
|
</SelectTrigger>
|
||||||
color: tag.color || undefined,
|
<SelectContent>
|
||||||
})) || []
|
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||||
}
|
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
||||||
selectedTags={selectedTagIds}
|
<SelectItem value="createdAt-desc">
|
||||||
onTagsChange={setSelectedTagIds}
|
Newest first
|
||||||
/>
|
</SelectItem>
|
||||||
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
<SelectItem value="createdAt-asc">
|
||||||
<ArrowUpDown className="size-4 text-muted-foreground" />
|
Oldest first
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
</SelectItem>
|
||||||
<SelectTrigger className="w-full">
|
<SelectItem value="services-desc">
|
||||||
<SelectValue placeholder="Sort by..." />
|
Most services
|
||||||
</SelectTrigger>
|
</SelectItem>
|
||||||
<SelectContent>
|
<SelectItem value="services-asc">
|
||||||
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
Least services
|
||||||
<SelectItem value="name-desc">
|
</SelectItem>
|
||||||
Name (Z-A)
|
</SelectContent>
|
||||||
</SelectItem>
|
</Select>
|
||||||
<SelectItem value="createdAt-desc">
|
|
||||||
Newest first
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="createdAt-asc">
|
|
||||||
Oldest first
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="services-desc">
|
|
||||||
Most services
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="services-asc">
|
|
||||||
Least services
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filteredProjects?.length === 0 && (
|
{filteredProjects?.length === 0 && (
|
||||||
@@ -294,27 +253,26 @@ export const ShowProjects = () => {
|
|||||||
.map(
|
.map(
|
||||||
(env) =>
|
(env) =>
|
||||||
env.applications.length === 0 &&
|
env.applications.length === 0 &&
|
||||||
env.compose.length === 0 &&
|
|
||||||
env.libsql.length === 0 &&
|
|
||||||
env.mariadb.length === 0 &&
|
env.mariadb.length === 0 &&
|
||||||
env.mongo.length === 0 &&
|
env.mongo.length === 0 &&
|
||||||
env.mysql.length === 0 &&
|
env.mysql.length === 0 &&
|
||||||
env.postgres.length === 0 &&
|
env.postgres.length === 0 &&
|
||||||
env.redis.length === 0,
|
env.redis.length === 0 &&
|
||||||
|
env.applications.length === 0 &&
|
||||||
|
env.compose.length === 0,
|
||||||
)
|
)
|
||||||
.every(Boolean);
|
.every(Boolean);
|
||||||
|
|
||||||
const totalServices = project?.environments
|
const totalServices = project?.environments
|
||||||
.map(
|
.map(
|
||||||
(env) =>
|
(env) =>
|
||||||
env.applications.length +
|
|
||||||
env.compose.length +
|
|
||||||
env.libsql.length +
|
|
||||||
env.mariadb.length +
|
env.mariadb.length +
|
||||||
env.mongo.length +
|
env.mongo.length +
|
||||||
env.mysql.length +
|
env.mysql.length +
|
||||||
env.postgres.length +
|
env.postgres.length +
|
||||||
env.redis.length,
|
env.redis.length +
|
||||||
|
env.applications.length +
|
||||||
|
env.compose.length,
|
||||||
)
|
)
|
||||||
.reduce((acc, curr) => acc + curr, 0);
|
.reduce((acc, curr) => acc + curr, 0);
|
||||||
|
|
||||||
@@ -357,19 +315,6 @@ export const ShowProjects = () => {
|
|||||||
{project.description}
|
{project.description}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{project.projectTags &&
|
|
||||||
project.projectTags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
||||||
{project.projectTags.map((pt) => (
|
|
||||||
<TagBadge
|
|
||||||
key={pt.tag.tagId}
|
|
||||||
name={pt.tag.name}
|
|
||||||
color={pt.tag.color}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasNoEnvironments && (
|
{hasNoEnvironments && (
|
||||||
<div className="flex flex-row gap-2 items-center rounded-lg bg-yellow-50 p-2 mt-2 dark:bg-yellow-950">
|
<div className="flex flex-row gap-2 items-center rounded-lg bg-yellow-50 p-2 mt-2 dark:bg-yellow-950">
|
||||||
<AlertTriangle className="size-4 text-yellow-600 dark:text-yellow-400 shrink-0" />
|
<AlertTriangle className="size-4 text-yellow-600 dark:text-yellow-400 shrink-0" />
|
||||||
@@ -416,7 +361,8 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{permissions?.project.delete && (
|
{(auth?.role === "owner" ||
|
||||||
|
auth?.canDeleteProjects) && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -490,7 +436,7 @@ export const ShowProjects = () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="pt-4">
|
<CardFooter className="pt-4">
|
||||||
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||||
<DateTooltip date={project.createdAt}>
|
<DateTooltip date={project.createdAt}>
|
||||||
Created
|
Created
|
||||||
</DateTooltip>
|
</DateTooltip>
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const canDeploy = permissions?.deployment.create ?? false;
|
|
||||||
const { data, refetch } = api.redis.one.useQuery(
|
const { data, refetch } = api.redis.one.useQuery(
|
||||||
{
|
{
|
||||||
redisId,
|
redisId,
|
||||||
@@ -74,158 +72,153 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
{canDeploy && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Deploy Redis"
|
||||||
title="Deploy Redis"
|
description="Are you sure you want to deploy this redis?"
|
||||||
description="Are you sure you want to deploy this redis?"
|
type="default"
|
||||||
type="default"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
setIsDeploying(true);
|
||||||
setIsDeploying(true);
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
refetch();
|
||||||
refetch();
|
}}
|
||||||
}}
|
>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
isLoading={data?.applicationStatus === "running"}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Button
|
<Tooltip>
|
||||||
variant="default"
|
<TooltipTrigger asChild>
|
||||||
isLoading={data?.applicationStatus === "running"}
|
<div className="flex items-center">
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Rocket className="size-4 mr-1" />
|
||||||
>
|
Deploy
|
||||||
<Tooltip>
|
</div>
|
||||||
<TooltipTrigger asChild>
|
</TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<TooltipPrimitive.Portal>
|
||||||
<Rocket className="size-4 mr-1" />
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
Deploy
|
<p>Downloads and sets up the Redis database</p>
|
||||||
</div>
|
</TooltipContent>
|
||||||
</TooltipTrigger>
|
</TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Portal>
|
</Tooltip>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</Button>
|
||||||
<p>Downloads and sets up the Redis database</p>
|
</DialogAction>
|
||||||
</TooltipContent>
|
<DialogAction
|
||||||
</TooltipPrimitive.Portal>
|
title="Reload Redis"
|
||||||
</Tooltip>
|
description="Are you sure you want to reload this redis?"
|
||||||
</Button>
|
type="default"
|
||||||
</DialogAction>
|
onClick={async () => {
|
||||||
)}
|
await reload({
|
||||||
{canDeploy && (
|
redisId: redisId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the Redis service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
{data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Reload Redis"
|
title="Start Redis"
|
||||||
description="Are you sure you want to reload this redis?"
|
description="Are you sure you want to start this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await reload({
|
await start({
|
||||||
redisId: redisId,
|
redisId: redisId,
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Redis reloaded successfully");
|
toast.success("Redis started successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error reloading Redis");
|
toast.error("Error starting Redis");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isStarting}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
Reload
|
Start
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Restart the Redis service without rebuilding</p>
|
<p>
|
||||||
|
Start the Redis database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Redis"
|
||||||
|
description="Are you sure you want to stop this redis?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
redisId: redisId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running Redis database</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
{canDeploy &&
|
|
||||||
(data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
|
||||||
title="Start Redis"
|
|
||||||
description="Are you sure you want to start this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await start({
|
|
||||||
redisId: redisId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isStarting}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
|
||||||
Start
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>
|
|
||||||
Start the Redis database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Redis"
|
|
||||||
description="Are you sure you want to stop this redis?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
redisId: redisId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running Redis database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
))}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { StatusTooltip } from "../shared/status-tooltip";
|
import { StatusTooltip } from "../shared/status-tooltip";
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ export const SearchCommand = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
const { data: session } = api.user.session.useQuery();
|
const { data: session } = authClient.useSession();
|
||||||
const { data } = api.project.all.useQuery(undefined, {
|
const { data } = api.project.all.useQuery(undefined, {
|
||||||
enabled: !!session,
|
enabled: !!session,
|
||||||
});
|
});
|
||||||
@@ -173,14 +174,6 @@ export const SearchCommand = () => {
|
|||||||
>
|
>
|
||||||
Projects
|
Projects
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
|
||||||
onSelect={() => {
|
|
||||||
router.push("/dashboard/deployments");
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Deployments
|
|
||||||
</CommandItem>
|
|
||||||
{!isCloud && (
|
{!isCloud && (
|
||||||
<>
|
<>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|||||||
@@ -91,10 +91,7 @@ export const ShowBilling = () => {
|
|||||||
api.stripe.upgradeSubscription.useMutation();
|
api.stripe.upgradeSubscription.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
const [serverQuantity, setServerQuantity] = useState(3);
|
||||||
const [startupServerQuantity, setStartupServerQuantity] = useState(
|
|
||||||
STARTUP_SERVERS_INCLUDED,
|
|
||||||
);
|
|
||||||
const [isAnnual, setIsAnnual] = useState(false);
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
||||||
null,
|
null,
|
||||||
@@ -114,12 +111,6 @@ export const ShowBilling = () => {
|
|||||||
productId: string,
|
productId: string,
|
||||||
) => {
|
) => {
|
||||||
const stripe = await stripePromise;
|
const stripe = await stripePromise;
|
||||||
const serverQuantity =
|
|
||||||
tier === "startup"
|
|
||||||
? startupServerQuantity
|
|
||||||
: tier === "hobby"
|
|
||||||
? hobbyServerQuantity
|
|
||||||
: hobbyServerQuantity;
|
|
||||||
if (data && data.subscriptions.length === 0) {
|
if (data && data.subscriptions.length === 0) {
|
||||||
createCheckoutSession({
|
createCheckoutSession({
|
||||||
tier,
|
tier,
|
||||||
@@ -688,7 +679,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceHobby(
|
{calculatePriceHobby(
|
||||||
hobbyServerQuantity,
|
serverQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -701,8 +692,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceHobby(hobbyServerQuantity, true) /
|
calculatePriceHobby(serverQuantity, true) / 12
|
||||||
12
|
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -734,19 +724,19 @@ export const ShowBilling = () => {
|
|||||||
Servers:
|
Servers:
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
disabled={hobbyServerQuantity <= 1}
|
disabled={serverQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
setServerQuantity((q) => Math.max(1, q - 1))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={hobbyServerQuantity}
|
value={serverQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setHobbyServerQuantity(
|
setServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
Number(
|
Number(
|
||||||
@@ -760,7 +750,7 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
onClick={() => setServerQuantity((q) => q + 1)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -785,7 +775,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCheckout("hobby", data!.hobbyProductId!)
|
handleCheckout("hobby", data!.hobbyProductId!)
|
||||||
}
|
}
|
||||||
disabled={hobbyServerQuantity < 1}
|
disabled={serverQuantity < 1}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
@@ -816,7 +806,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceStartup(
|
{calculatePriceStartup(
|
||||||
startupServerQuantity,
|
serverQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -829,10 +819,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceStartup(
|
calculatePriceStartup(serverQuantity, true) / 12
|
||||||
startupServerQuantity,
|
|
||||||
true,
|
|
||||||
) / 12
|
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -869,14 +856,13 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
startupServerQuantity <=
|
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
||||||
STARTUP_SERVERS_INCLUDED
|
|
||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setStartupServerQuantity((q) =>
|
setServerQuantity((q) =>
|
||||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -884,9 +870,9 @@ export const ShowBilling = () => {
|
|||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={startupServerQuantity}
|
value={serverQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStartupServerQuantity(
|
setServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
STARTUP_SERVERS_INCLUDED,
|
STARTUP_SERVERS_INCLUDED,
|
||||||
Number(
|
Number(
|
||||||
@@ -901,9 +887,7 @@ export const ShowBilling = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() =>
|
onClick={() => setServerQuantity((q) => q + 1)}
|
||||||
setStartupServerQuantity((q) => q + 1)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -933,7 +917,7 @@ export const ShowBilling = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
serverQuantity < STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
@@ -1025,7 +1009,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(
|
{calculatePrice(
|
||||||
hobbyServerQuantity,
|
serverQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
@@ -1034,10 +1018,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
${" "}
|
${" "}
|
||||||
{(
|
{(
|
||||||
calculatePrice(
|
calculatePrice(serverQuantity, isAnnual) / 12
|
||||||
hobbyServerQuantity,
|
|
||||||
isAnnual,
|
|
||||||
) / 12
|
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
/ Month USD
|
/ Month USD
|
||||||
</p>
|
</p>
|
||||||
@@ -1045,10 +1026,9 @@ export const ShowBilling = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(
|
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
||||||
hobbyServerQuantity,
|
2,
|
||||||
isAnnual,
|
)}{" "}
|
||||||
).toFixed(2)}{" "}
|
|
||||||
USD
|
USD
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1091,28 +1071,26 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex flex-col gap-2 mt-4">
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{hobbyServerQuantity} Servers
|
{serverQuantity} Servers
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={hobbyServerQuantity <= 1}
|
disabled={serverQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (hobbyServerQuantity <= 1) return;
|
if (serverQuantity <= 1) return;
|
||||||
|
|
||||||
setHobbyServerQuantity(
|
setServerQuantity(serverQuantity - 1);
|
||||||
hobbyServerQuantity - 1,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={hobbyServerQuantity}
|
value={serverQuantity}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setHobbyServerQuantity(
|
setServerQuantity(
|
||||||
e.target.value as unknown as number,
|
e.target.value as unknown as number,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -1121,9 +1099,7 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHobbyServerQuantity(
|
setServerQuantity(serverQuantity + 1);
|
||||||
hobbyServerQuantity + 1,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@@ -1149,7 +1125,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handleCheckout("legacy", product.id);
|
handleCheckout("legacy", product.id);
|
||||||
}}
|
}}
|
||||||
disabled={hobbyServerQuantity < 1}
|
disabled={serverQuantity < 1}
|
||||||
>
|
>
|
||||||
Subscribe
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export const ShowCertificates = () => {
|
|||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.certificates.remove.useMutation();
|
api.certificates.remove.useMutation();
|
||||||
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -54,7 +53,7 @@ export const ShowCertificates = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any certificates created
|
You don't have any certificates created
|
||||||
</span>
|
</span>
|
||||||
{permissions?.certificate.create && <AddCertificate />}
|
<AddCertificate />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -102,52 +101,47 @@ export const ShowCertificates = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{permissions?.certificate.delete && (
|
<div className="flex flex-row gap-1">
|
||||||
<div className="flex flex-row gap-1">
|
<DialogAction
|
||||||
<DialogAction
|
title="Delete Certificate"
|
||||||
title="Delete Certificate"
|
description="Are you sure you want to delete this certificate?"
|
||||||
description="Are you sure you want to delete this certificate?"
|
type="destructive"
|
||||||
type="destructive"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await mutateAsync({
|
||||||
await mutateAsync({
|
certificateId: certificate.certificateId,
|
||||||
certificateId:
|
})
|
||||||
certificate.certificateId,
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"Certificate deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success(
|
toast.error(
|
||||||
"Certificate deleted successfully",
|
"Error deleting certificate",
|
||||||
);
|
);
|
||||||
refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error(
|
<Button
|
||||||
"Error deleting certificate",
|
variant="ghost"
|
||||||
);
|
size="icon"
|
||||||
});
|
className="group hover:bg-red-500/10 "
|
||||||
}}
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className="group hover:bg-red-500/10 "
|
</div>
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{permissions?.certificate.create && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
<AddCertificate />
|
||||||
<AddCertificate />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ export const extractExpirationDate = (certData: string): Date | null => {
|
|||||||
|
|
||||||
// Helper: read ASN.1 length field
|
// Helper: read ASN.1 length field
|
||||||
function readLength(pos: number): { length: number; offset: number } {
|
function readLength(pos: number): { length: number; offset: number } {
|
||||||
// biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation
|
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||||
let len = der[pos++];
|
let len = der[pos++];
|
||||||
if (len & 0x80) {
|
if (len & 0x80) {
|
||||||
const bytes = len & 0x7f;
|
const bytes = len & 0x7f;
|
||||||
len = 0;
|
len = 0;
|
||||||
for (let i = 0; i < bytes; i++) {
|
for (let i = 0; i < bytes; i++) {
|
||||||
// biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation
|
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||||
len = (len << 8) + der[pos++];
|
len = (len << 8) + der[pos++];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Loader2, Package, Trash2 } from "lucide-react";
|
import { Package, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { WhaleLoader } from "@/components/shared/whale-loader";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -16,7 +17,6 @@ export const ShowRegistry = () => {
|
|||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.registry.remove.useMutation();
|
api.registry.remove.useMutation();
|
||||||
const { data, isPending, refetch } = api.registry.all.useQuery();
|
const { data, isPending, refetch } = api.registry.all.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -32,10 +32,9 @@ export const ShowRegistry = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
{isPending ? (
|
{isLoading ? (
|
||||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
<div className="flex flex-row items-center justify-center min-h-[25vh]">
|
||||||
<span>Loading...</span>
|
<WhaleLoader />
|
||||||
<Loader2 className="animate-spin size-4" />
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -45,7 +44,7 @@ export const ShowRegistry = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any registry configurations
|
You don't have any registry configurations
|
||||||
</span>
|
</span>
|
||||||
{permissions?.registry.create && <HandleRegistry />}
|
<HandleRegistry />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -74,49 +73,45 @@ export const ShowRegistry = () => {
|
|||||||
registryId={registry.registryId}
|
registryId={registry.registryId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{permissions?.registry.delete && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Delete Registry"
|
||||||
title="Delete Registry"
|
description="Are you sure you want to delete this registry configuration?"
|
||||||
description="Are you sure you want to delete this registry configuration?"
|
type="destructive"
|
||||||
type="destructive"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await mutateAsync({
|
||||||
await mutateAsync({
|
registryId: registry.registryId,
|
||||||
registryId: registry.registryId,
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"Registry configuration deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success(
|
toast.error(
|
||||||
"Registry configuration deleted successfully",
|
"Error deleting registry configuration",
|
||||||
);
|
);
|
||||||
refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error(
|
<Button
|
||||||
"Error deleting registry configuration",
|
variant="ghost"
|
||||||
);
|
size="icon"
|
||||||
});
|
className="group hover:bg-red-500/10 "
|
||||||
}}
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className="group hover:bg-red-500/10 "
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{permissions?.registry.create && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
<HandleRegistry />
|
||||||
<HandleRegistry />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export const ShowDestinations = () => {
|
|||||||
const { data, isPending, refetch } = api.destination.all.useQuery();
|
const { data, isPending, refetch } = api.destination.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.destination.remove.useMutation();
|
api.destination.remove.useMutation();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
@@ -46,7 +45,7 @@ export const ShowDestinations = () => {
|
|||||||
To create a backup it is required to set at least 1
|
To create a backup it is required to set at least 1
|
||||||
provider.
|
provider.
|
||||||
</span>
|
</span>
|
||||||
{permissions?.destination.create && <HandleDestinations />}
|
<HandleDestinations />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -72,49 +71,43 @@ export const ShowDestinations = () => {
|
|||||||
<HandleDestinations
|
<HandleDestinations
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
/>
|
/>
|
||||||
{permissions?.destination.delete && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Delete Destination"
|
||||||
title="Delete Destination"
|
description="Are you sure you want to delete this destination?"
|
||||||
description="Are you sure you want to delete this destination?"
|
type="destructive"
|
||||||
type="destructive"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await mutateAsync({
|
||||||
await mutateAsync({
|
destinationId: destination.destinationId,
|
||||||
destinationId: destination.destinationId,
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"Destination deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success(
|
toast.error("Error deleting destination");
|
||||||
"Destination deleted successfully",
|
});
|
||||||
);
|
}}
|
||||||
refetch();
|
>
|
||||||
})
|
<Button
|
||||||
.catch(() => {
|
variant="ghost"
|
||||||
toast.error(
|
size="icon"
|
||||||
"Error deleting destination",
|
className="group hover:bg-red-500/10 "
|
||||||
);
|
isLoading={isRemoving}
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className="group hover:bg-red-500/10 "
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{permissions?.destination.create && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
<HandleDestinations />
|
||||||
<HandleDestinations />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
export const AddGithubProvider = () => {
|
export const AddGithubProvider = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
const { data: session } = api.user.session.useQuery();
|
|
||||||
const { data } = api.user.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const [manifest, setManifest] = useState("");
|
const [manifest, setManifest] = useState("");
|
||||||
const [isOrganization, setIsOrganization] = useState(false);
|
const [isOrganization, setIsOrganization] = useState(false);
|
||||||
@@ -52,7 +52,7 @@ export const AddGithubProvider = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setManifest(manifest);
|
setManifest(manifest);
|
||||||
}, [activeOrganization?.id, session?.user?.id]);
|
}, [data?.id, activeOrganization?.id, session?.user?.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
@@ -98,8 +98,8 @@ export const AddGithubProvider = () => {
|
|||||||
<form
|
<form
|
||||||
action={
|
action={
|
||||||
isOrganization
|
isOrganization
|
||||||
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||||
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
||||||
}
|
}
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
@@ -131,7 +131,11 @@ export const AddGithubProvider = () => {
|
|||||||
Unsure if you already have an app?
|
Unsure if you already have an app?
|
||||||
</a>
|
</a>
|
||||||
<Button
|
<Button
|
||||||
disabled={isOrganization && organizationName.length < 1}
|
disabled={
|
||||||
|
(isOrganization && organizationName.length < 1) ||
|
||||||
|
!activeOrganization?.id ||
|
||||||
|
!session?.user?.id
|
||||||
|
}
|
||||||
type="submit"
|
type="submit"
|
||||||
className="self-end"
|
className="self-end"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
MattermostIcon,
|
|
||||||
GotifyIcon,
|
GotifyIcon,
|
||||||
LarkIcon,
|
LarkIcon,
|
||||||
NtfyIcon,
|
NtfyIcon,
|
||||||
@@ -135,14 +134,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
priority: z.number().min(1).max(5).default(3),
|
priority: z.number().min(1).max(5).default(3),
|
||||||
})
|
})
|
||||||
.merge(notificationBaseSchema),
|
.merge(notificationBaseSchema),
|
||||||
z
|
|
||||||
.object({
|
|
||||||
type: z.literal("mattermost"),
|
|
||||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
|
||||||
channel: z.string().optional(),
|
|
||||||
username: z.string().optional(),
|
|
||||||
})
|
|
||||||
.merge(notificationBaseSchema),
|
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("pushover"),
|
type: z.literal("pushover"),
|
||||||
@@ -219,10 +210,6 @@ export const notificationsMap = {
|
|||||||
icon: <NtfyIcon />,
|
icon: <NtfyIcon />,
|
||||||
label: "ntfy",
|
label: "ntfy",
|
||||||
},
|
},
|
||||||
mattermost: {
|
|
||||||
icon: <MattermostIcon />,
|
|
||||||
label: "Mattermost",
|
|
||||||
},
|
|
||||||
pushover: {
|
pushover: {
|
||||||
icon: <PushoverIcon />,
|
icon: <PushoverIcon />,
|
||||||
label: "Pushover",
|
label: "Pushover",
|
||||||
@@ -266,16 +253,14 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
api.notification.testGotifyConnection.useMutation();
|
api.notification.testGotifyConnection.useMutation();
|
||||||
const { mutateAsync: testNtfyConnection, isPending: isLoadingNtfy } =
|
const { mutateAsync: testNtfyConnection, isPending: isLoadingNtfy } =
|
||||||
api.notification.testNtfyConnection.useMutation();
|
api.notification.testNtfyConnection.useMutation();
|
||||||
const {
|
|
||||||
mutateAsync: testMattermostConnection,
|
|
||||||
isPending: isLoadingMattermost,
|
|
||||||
} = api.notification.testMattermostConnection.useMutation();
|
|
||||||
const { mutateAsync: testLarkConnection, isPending: isLoadingLark } =
|
const { mutateAsync: testLarkConnection, isPending: isLoadingLark } =
|
||||||
api.notification.testLarkConnection.useMutation();
|
api.notification.testLarkConnection.useMutation();
|
||||||
const { mutateAsync: testTeamsConnection, isPending: isLoadingTeams } =
|
const { mutateAsync: testTeamsConnection, isPending: isLoadingTeams } =
|
||||||
api.notification.testTeamsConnection.useMutation();
|
api.notification.testTeamsConnection.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: testCustomConnection, isPending: isLoadingCustom } =
|
const { mutateAsync: testCustomConnection, isPending: isLoadingCustom } =
|
||||||
api.notification.testCustomConnection.useMutation();
|
api.notification.testCustomConnection.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: testPushoverConnection, isPending: isLoadingPushover } =
|
const { mutateAsync: testPushoverConnection, isPending: isLoadingPushover } =
|
||||||
api.notification.testPushoverConnection.useMutation();
|
api.notification.testPushoverConnection.useMutation();
|
||||||
|
|
||||||
@@ -303,9 +288,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
const ntfyMutation = notificationId
|
const ntfyMutation = notificationId
|
||||||
? api.notification.updateNtfy.useMutation()
|
? api.notification.updateNtfy.useMutation()
|
||||||
: api.notification.createNtfy.useMutation();
|
: api.notification.createNtfy.useMutation();
|
||||||
const mattermostMutation = notificationId
|
|
||||||
? api.notification.updateMattermost.useMutation()
|
|
||||||
: api.notification.createMattermost.useMutation();
|
|
||||||
const larkMutation = notificationId
|
const larkMutation = notificationId
|
||||||
? api.notification.updateLark.useMutation()
|
? api.notification.updateLark.useMutation()
|
||||||
: api.notification.createLark.useMutation();
|
: api.notification.createLark.useMutation();
|
||||||
@@ -456,21 +438,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
dockerCleanup: notification.dockerCleanup,
|
dockerCleanup: notification.dockerCleanup,
|
||||||
serverThreshold: notification.serverThreshold,
|
serverThreshold: notification.serverThreshold,
|
||||||
});
|
});
|
||||||
} else if (notification.notificationType === "mattermost") {
|
|
||||||
form.reset({
|
|
||||||
appBuildError: notification.appBuildError,
|
|
||||||
appDeploy: notification.appDeploy,
|
|
||||||
dokployRestart: notification.dokployRestart,
|
|
||||||
databaseBackup: notification.databaseBackup,
|
|
||||||
volumeBackup: notification.volumeBackup,
|
|
||||||
type: notification.notificationType,
|
|
||||||
webhookUrl: notification.mattermost?.webhookUrl,
|
|
||||||
channel: notification.mattermost?.channel || "",
|
|
||||||
username: notification.mattermost?.username || "",
|
|
||||||
name: notification.name,
|
|
||||||
dockerCleanup: notification.dockerCleanup,
|
|
||||||
serverThreshold: notification.serverThreshold,
|
|
||||||
});
|
|
||||||
} else if (notification.notificationType === "lark") {
|
} else if (notification.notificationType === "lark") {
|
||||||
form.reset({
|
form.reset({
|
||||||
appBuildError: notification.appBuildError,
|
appBuildError: notification.appBuildError,
|
||||||
@@ -549,7 +516,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
resend: resendMutation,
|
resend: resendMutation,
|
||||||
gotify: gotifyMutation,
|
gotify: gotifyMutation,
|
||||||
ntfy: ntfyMutation,
|
ntfy: ntfyMutation,
|
||||||
mattermost: mattermostMutation,
|
|
||||||
lark: larkMutation,
|
lark: larkMutation,
|
||||||
teams: teamsMutation,
|
teams: teamsMutation,
|
||||||
custom: customMutation,
|
custom: customMutation,
|
||||||
@@ -680,22 +646,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
notificationId: notificationId || "",
|
notificationId: notificationId || "",
|
||||||
ntfyId: notification?.ntfyId || "",
|
ntfyId: notification?.ntfyId || "",
|
||||||
});
|
});
|
||||||
} else if (data.type === "mattermost") {
|
|
||||||
promise = mattermostMutation.mutateAsync({
|
|
||||||
appBuildError: appBuildError,
|
|
||||||
appDeploy: appDeploy,
|
|
||||||
dokployRestart: dokployRestart,
|
|
||||||
databaseBackup: databaseBackup,
|
|
||||||
volumeBackup: volumeBackup,
|
|
||||||
webhookUrl: data.webhookUrl,
|
|
||||||
channel: data.channel || undefined,
|
|
||||||
username: data.username || undefined,
|
|
||||||
name: data.name,
|
|
||||||
dockerCleanup: dockerCleanup,
|
|
||||||
notificationId: notificationId || "",
|
|
||||||
mattermostId: notification?.mattermostId || "",
|
|
||||||
serverThreshold: serverThreshold,
|
|
||||||
});
|
|
||||||
} else if (data.type === "lark") {
|
} else if (data.type === "lark") {
|
||||||
promise = larkMutation.mutateAsync({
|
promise = larkMutation.mutateAsync({
|
||||||
appBuildError: appBuildError,
|
appBuildError: appBuildError,
|
||||||
@@ -787,9 +737,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
await utils.notification.all.invalidate();
|
await utils.notification.all.invalidate();
|
||||||
if (notificationId) {
|
|
||||||
await utils.notification.one.invalidate({ notificationId });
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
@@ -1456,62 +1403,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "mattermost" && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="webhookUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="https://your-mattermost.com/hooks/xxx-generatedkey-xxx"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="channel"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Channel</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="deployments" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Optional. Channel to post to (without #).
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Dokploy" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Optional. Display name for the webhook.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type === "custom" && (
|
{type === "custom" && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -1598,7 +1489,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "lark" && (
|
{type === "lark" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -1959,7 +1849,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
isLoadingResend ||
|
isLoadingResend ||
|
||||||
isLoadingGotify ||
|
isLoadingGotify ||
|
||||||
isLoadingNtfy ||
|
isLoadingNtfy ||
|
||||||
isLoadingMattermost ||
|
|
||||||
isLoadingLark ||
|
isLoadingLark ||
|
||||||
isLoadingTeams ||
|
isLoadingTeams ||
|
||||||
isLoadingCustom ||
|
isLoadingCustom ||
|
||||||
@@ -2019,12 +1908,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
accessToken: data.accessToken || "",
|
accessToken: data.accessToken || "",
|
||||||
priority: data.priority ?? 0,
|
priority: data.priority ?? 0,
|
||||||
});
|
});
|
||||||
} else if (data.type === "mattermost") {
|
|
||||||
await testMattermostConnection({
|
|
||||||
webhookUrl: data.webhookUrl,
|
|
||||||
channel: data.channel || undefined,
|
|
||||||
username: data.username || undefined,
|
|
||||||
});
|
|
||||||
} else if (data.type === "lark") {
|
} else if (data.type === "lark") {
|
||||||
await testLarkConnection({
|
await testLarkConnection({
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
GotifyIcon,
|
GotifyIcon,
|
||||||
LarkIcon,
|
LarkIcon,
|
||||||
MattermostIcon,
|
|
||||||
NtfyIcon,
|
NtfyIcon,
|
||||||
ResendIcon,
|
ResendIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
@@ -27,7 +26,6 @@ export const ShowNotifications = () => {
|
|||||||
const { data, isPending, refetch } = api.notification.all.useQuery();
|
const { data, isPending, refetch } = api.notification.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.notification.remove.useMutation();
|
api.notification.remove.useMutation();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -58,9 +56,7 @@ export const ShowNotifications = () => {
|
|||||||
To send notifications it is required to set at least 1
|
To send notifications it is required to set at least 1
|
||||||
provider.
|
provider.
|
||||||
</span>
|
</span>
|
||||||
{permissions?.notification.create && (
|
<HandleNotifications />
|
||||||
<HandleNotifications />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -122,12 +118,6 @@ export const ShowNotifications = () => {
|
|||||||
<TeamsIcon className="size-7 text-muted-foreground" />
|
<TeamsIcon className="size-7 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{notification.notificationType ===
|
|
||||||
"mattermost" && (
|
|
||||||
<div className="flex items-center justify-center rounded-lg">
|
|
||||||
<MattermostIcon className="size-7" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{notification.name}
|
{notification.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -136,50 +126,45 @@ export const ShowNotifications = () => {
|
|||||||
notificationId={notification.notificationId}
|
notificationId={notification.notificationId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{permissions?.notification.delete && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Delete Notification"
|
||||||
title="Delete Notification"
|
description="Are you sure you want to delete this notification?"
|
||||||
description="Are you sure you want to delete this notification?"
|
type="destructive"
|
||||||
type="destructive"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await mutateAsync({
|
||||||
await mutateAsync({
|
notificationId: notification.notificationId,
|
||||||
notificationId:
|
})
|
||||||
notification.notificationId,
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"Notification deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success(
|
toast.error(
|
||||||
"Notification deleted successfully",
|
"Error deleting notification",
|
||||||
);
|
);
|
||||||
refetch();
|
});
|
||||||
})
|
}}
|
||||||
.catch(() => {
|
>
|
||||||
toast.error(
|
<Button
|
||||||
"Error deleting notification",
|
variant="ghost"
|
||||||
);
|
size="icon"
|
||||||
});
|
className="group hover:bg-red-500/10 "
|
||||||
}}
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className="group hover:bg-red-500/10 "
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{permissions?.notification.create && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
<HandleNotifications />
|
||||||
<HandleNotifications />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -410,10 +409,7 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="root" {...field} />
|
<Input placeholder="root" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
|
||||||
Use "root" or a non-root user with passwordless
|
|
||||||
sudo access.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -118,10 +118,9 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div id="hook-form-add-gitlab" className="grid w-full gap-4">
|
<div id="hook-form-add-gitlab" className="grid w-full gap-4">
|
||||||
<AlertBlock type="info">
|
<AlertBlock type="warning">
|
||||||
You can connect as root or as a non-root user with passwordless
|
Using a root user is required to ensure everything works as
|
||||||
sudo access. If using a non-root user, ensure passwordless sudo is
|
expected.
|
||||||
configured.
|
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
|
|
||||||
<Tabs defaultValue="ssh-keys">
|
<Tabs defaultValue="ssh-keys">
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export const ShowServers = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: canCreateMoreServers } =
|
const { data: canCreateMoreServers } =
|
||||||
api.stripe.canCreateMoreServers.useQuery();
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -116,7 +115,7 @@ export const ShowServers = () => {
|
|||||||
Start adding servers to deploy your applications
|
Start adding servers to deploy your applications
|
||||||
remotely.
|
remotely.
|
||||||
</span>
|
</span>
|
||||||
{permissions?.server.create && <HandleServers />}
|
<HandleServers />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -363,71 +362,66 @@ export const ShowServers = () => {
|
|||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
{permissions?.server.delete && (
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div>
|
||||||
<div>
|
<DialogAction
|
||||||
<DialogAction
|
disabled={!canDelete}
|
||||||
disabled={!canDelete}
|
title={
|
||||||
title={
|
canDelete
|
||||||
canDelete
|
? "Delete Server"
|
||||||
? "Delete Server"
|
: "Server has active services"
|
||||||
: "Server has active services"
|
}
|
||||||
}
|
description={
|
||||||
description={
|
canDelete ? (
|
||||||
canDelete ? (
|
"This will delete the server and all associated data"
|
||||||
"This will delete the server and all associated data"
|
) : (
|
||||||
) : (
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
You can not delete this
|
||||||
You can not delete this
|
server because it has
|
||||||
server because it has
|
active services.
|
||||||
active services.
|
<AlertBlock type="warning">
|
||||||
<AlertBlock type="warning">
|
You have active services
|
||||||
You have active
|
associated with this
|
||||||
services associated
|
server, please delete
|
||||||
with this server,
|
them first.
|
||||||
please delete them
|
</AlertBlock>
|
||||||
first.
|
</div>
|
||||||
</AlertBlock>
|
)
|
||||||
</div>
|
}
|
||||||
)
|
onClick={async () => {
|
||||||
}
|
await mutateAsync({
|
||||||
onClick={async () => {
|
serverId: server.serverId,
|
||||||
await mutateAsync({
|
})
|
||||||
serverId: server.serverId,
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
`Server ${server.name} deleted successfully`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch((err) => {
|
||||||
refetch();
|
toast.error(err.message);
|
||||||
toast.success(
|
});
|
||||||
`Server ${server.name} deleted successfully`,
|
}}
|
||||||
);
|
>
|
||||||
})
|
<Button
|
||||||
.catch((err) => {
|
variant="ghost"
|
||||||
toast.error(
|
size="icon"
|
||||||
err.message,
|
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="h-4 w-4" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
</div>
|
||||||
>
|
</TooltipTrigger>
|
||||||
<Trash2 className="h-4 w-4" />
|
<TooltipContent>
|
||||||
</Button>
|
<p>
|
||||||
</DialogAction>
|
{canDelete
|
||||||
</div>
|
? "Delete Server"
|
||||||
</TooltipTrigger>
|
: "Cannot delete - has active services"}
|
||||||
<TooltipContent>
|
</p>
|
||||||
<p>
|
</TooltipContent>
|
||||||
{canDelete
|
</Tooltip>
|
||||||
? "Delete Server"
|
|
||||||
: "Cannot delete - has active services"}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -437,15 +431,13 @@ export const ShowServers = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{permissions?.server.create && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
{data && data?.length > 0 && (
|
||||||
{data && data?.length > 0 && (
|
<div>
|
||||||
<div>
|
<HandleServers />
|
||||||
<HandleServers />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -163,29 +163,6 @@ export const ValidateServer = ({ serverId }: Props) => {
|
|||||||
: "Not Created"
|
: "Not Created"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StatusRow
|
|
||||||
label="Privilege Mode"
|
|
||||||
isEnabled={
|
|
||||||
data?.privilegeMode === "root" ||
|
|
||||||
data?.privilegeMode === "sudo"
|
|
||||||
}
|
|
||||||
description={
|
|
||||||
data?.privilegeMode === "root"
|
|
||||||
? "Running as root"
|
|
||||||
: data?.privilegeMode === "sudo"
|
|
||||||
? "Running with sudo"
|
|
||||||
: "No sudo access (required for non-root)"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StatusRow
|
|
||||||
label="Docker Group"
|
|
||||||
isEnabled={data?.dockerGroupMember}
|
|
||||||
description={
|
|
||||||
data?.dockerGroupMember
|
|
||||||
? "User is in docker group"
|
|
||||||
: "User is not in docker group"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export const ShowDestinations = () => {
|
|||||||
const { data, isPending, refetch } = api.sshKey.all.useQuery();
|
const { data, isPending, refetch } = api.sshKey.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.sshKey.remove.useMutation();
|
api.sshKey.remove.useMutation();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -47,7 +46,7 @@ export const ShowDestinations = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any SSH keys
|
You don't have any SSH keys
|
||||||
</span>
|
</span>
|
||||||
{permissions?.sshKeys.create && <HandleSSHKeys />}
|
<HandleSSHKeys />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -85,47 +84,43 @@ export const ShowDestinations = () => {
|
|||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
||||||
|
|
||||||
{permissions?.sshKeys.delete && (
|
<DialogAction
|
||||||
<DialogAction
|
title="Delete SSH Key"
|
||||||
title="Delete SSH Key"
|
description="Are you sure you want to delete this SSH Key?"
|
||||||
description="Are you sure you want to delete this SSH Key?"
|
type="destructive"
|
||||||
type="destructive"
|
onClick={async () => {
|
||||||
onClick={async () => {
|
await mutateAsync({
|
||||||
await mutateAsync({
|
sshKeyId: sshKey.sshKeyId,
|
||||||
sshKeyId: sshKey.sshKeyId,
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
"SSH Key deleted successfully",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.catch(() => {
|
||||||
toast.success(
|
toast.error("Error deleting SSH Key");
|
||||||
"SSH Key deleted successfully",
|
});
|
||||||
);
|
}}
|
||||||
refetch();
|
>
|
||||||
})
|
<Button
|
||||||
.catch(() => {
|
variant="ghost"
|
||||||
toast.error("Error deleting SSH Key");
|
size="icon"
|
||||||
});
|
className="group hover:bg-red-500/10 "
|
||||||
}}
|
isLoading={isRemoving}
|
||||||
>
|
>
|
||||||
<Button
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
variant="ghost"
|
</Button>
|
||||||
size="icon"
|
</DialogAction>
|
||||||
className="group hover:bg-red-500/10 "
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{permissions?.sshKeys.create && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
<HandleSSHKeys />
|
||||||
<HandleSSHKeys />
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Palette, PenBoxIcon, PlusIcon } from "lucide-react";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { TagBadge } from "@/components/shared/tag-badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const TagSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(1, "Tag name is required")
|
|
||||||
.max(50, "Tag name must be less than 50 characters")
|
|
||||||
.refine(
|
|
||||||
(name) => {
|
|
||||||
const trimmedName = name.trim();
|
|
||||||
const validNameRegex =
|
|
||||||
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_.-]*[\p{L}\p{N}_-]$/u;
|
|
||||||
return validNameRegex.test(trimmedName);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"Tag name must start and end with a letter, number, hyphen or underscore. Spaces are allowed in between.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.transform((name) => name.trim()),
|
|
||||||
color: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Tag = z.infer<typeof TagSchema>;
|
|
||||||
|
|
||||||
interface HandleTagProps {
|
|
||||||
tagId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HandleTag = ({ tagId }: HandleTagProps) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const { mutateAsync, error, isError } = tagId
|
|
||||||
? api.tag.update.useMutation()
|
|
||||||
: api.tag.create.useMutation();
|
|
||||||
|
|
||||||
const { data: tag } = api.tag.one.useQuery(
|
|
||||||
{
|
|
||||||
tagId: tagId || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!tagId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const form = useForm<Tag>({
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
color: "#3b82f6",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(TagSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (tag) {
|
|
||||||
form.reset({
|
|
||||||
name: tag.name ?? "",
|
|
||||||
color: tag.color ?? "#3b82f6",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
form.reset({
|
|
||||||
name: "",
|
|
||||||
color: "#3b82f6",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, tag]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: Tag) => {
|
|
||||||
await mutateAsync({
|
|
||||||
name: data.name,
|
|
||||||
color: data.color,
|
|
||||||
tagId: tagId || "",
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.tag.all.invalidate();
|
|
||||||
toast.success(tagId ? "Tag Updated" : "Tag Created");
|
|
||||||
setIsOpen(false);
|
|
||||||
form.reset();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(tagId ? "Error updating tag" : "Error creating tag");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorValue = form.watch("color");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
{tagId ? (
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
||||||
<PenBoxIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Create Tag
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{tagId ? "Update" : "Create"} Tag</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{tagId
|
|
||||||
? "Update the tag name and color"
|
|
||||||
: "Create a new tag to organize your projects"}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-tag"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="e.g., Production, Client, Internal"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="color"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Color (Optional)</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<FormLabel
|
|
||||||
className="relative flex items-center justify-center w-12 h-12 rounded-md border-2 cursor-pointer hover:opacity-80 transition-opacity"
|
|
||||||
style={{
|
|
||||||
backgroundColor: field.value || "#3b82f6",
|
|
||||||
}}
|
|
||||||
onClick={() => colorInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
{!field.value && (
|
|
||||||
<Palette className="h-5 w-5 text-white" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref={colorInputRef}
|
|
||||||
type="color"
|
|
||||||
className="absolute opacity-0 pointer-events-none w-12 h-12 top-0 left-0"
|
|
||||||
value={field.value || "#3b82f6"}
|
|
||||||
onChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Input
|
|
||||||
placeholder="#3b82f6"
|
|
||||||
{...field}
|
|
||||||
value={field.value || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (value.startsWith("#") || value === "") {
|
|
||||||
field.onChange(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormDescription className="mt-1">
|
|
||||||
Choose a color to easily identify this tag
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{colorValue && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm text-muted-foreground">Preview:</span>
|
|
||||||
<TagBadge
|
|
||||||
name={form.watch("name") || "Tag Name"}
|
|
||||||
color={colorValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form-tag"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{tagId ? "Update" : "Create"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import { Loader2, TagIcon, Trash2 } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { TagBadge } from "@/components/shared/tag-badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { HandleTag } from "./handle-tag";
|
|
||||||
|
|
||||||
export const TagManager = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { data: tags, isPending } = api.tag.all.useQuery();
|
|
||||||
const { mutateAsync: deleteTag, isPending: isRemoving } =
|
|
||||||
api.tag.remove.useMutation();
|
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
|
||||||
<div className="rounded-xl bg-background shadow-md">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
|
||||||
<TagIcon className="size-6 text-muted-foreground self-center" />
|
|
||||||
Tags
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Create and manage tags to organize your projects
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
|
||||||
{isPending ? (
|
|
||||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
|
||||||
<span>Loading...</span>
|
|
||||||
<Loader2 className="animate-spin size-4" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{!tags || tags.length === 0 ? (
|
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
|
||||||
<TagIcon className="size-6 text-muted-foreground" />
|
|
||||||
<span className="text-base text-muted-foreground text-center">
|
|
||||||
No tags yet. Create your first tag to start organizing
|
|
||||||
projects.
|
|
||||||
</span>
|
|
||||||
{permissions?.tag.create && <HandleTag />}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
|
||||||
<div className="flex flex-col gap-4 rounded-lg">
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<div
|
|
||||||
key={tag.tagId}
|
|
||||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<TagBadge name={tag.name} color={tag.color} />
|
|
||||||
{tag.color && (
|
|
||||||
<span className="text-xs text-muted-foreground font-mono">
|
|
||||||
{tag.color}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row gap-1 items-center">
|
|
||||||
{permissions?.tag.update && (
|
|
||||||
<HandleTag tagId={tag.tagId} />
|
|
||||||
)}
|
|
||||||
{permissions?.tag.delete && (
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Tag"
|
|
||||||
description={`Are you sure you want to delete the tag "${tag.name}"? This will remove the tag from all projects. This action cannot be undone.`}
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteTag({
|
|
||||||
tagId: tag.tagId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.tag.all.invalidate();
|
|
||||||
toast.success(
|
|
||||||
"Tag deleted successfully",
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting tag");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{permissions?.tag.create && (
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
|
||||||
<HandleTag />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const addInvitation = z.object({
|
const addInvitation = z.object({
|
||||||
@@ -39,7 +40,7 @@ const addInvitation = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, "Email is required")
|
.min(1, "Email is required")
|
||||||
.email({ message: "Invalid email" }),
|
.email({ message: "Invalid email" }),
|
||||||
role: z.string().min(1, "Role is required"),
|
role: z.enum(["member", "admin"]),
|
||||||
notificationId: z.string().optional(),
|
notificationId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,14 +49,13 @@ type AddInvitation = z.infer<typeof addInvitation>;
|
|||||||
export const AddInvitation = () => {
|
export const AddInvitation = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: emailProviders } =
|
const { data: emailProviders } =
|
||||||
api.notification.getEmailProviders.useQuery();
|
api.notification.getEmailProviders.useQuery();
|
||||||
const { mutateAsync: inviteMember, isPending: isInviting } =
|
|
||||||
api.organization.inviteMember.useMutation();
|
|
||||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||||
const { data: customRoles } = api.customRole.all.useQuery();
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
|
|
||||||
const form = useForm<AddInvitation>({
|
const form = useForm<AddInvitation>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -70,15 +70,19 @@ export const AddInvitation = () => {
|
|||||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddInvitation) => {
|
const onSubmit = async (data: AddInvitation) => {
|
||||||
try {
|
setIsLoading(true);
|
||||||
const result = await inviteMember({
|
const result = await authClient.organization.inviteMember({
|
||||||
email: data.email.toLowerCase(),
|
email: data.email.toLowerCase(),
|
||||||
role: data.role,
|
role: data.role,
|
||||||
});
|
organizationId: activeOrganization?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error.message || "");
|
||||||
|
} else {
|
||||||
if (!isCloud && data.notificationId) {
|
if (!isCloud && data.notificationId) {
|
||||||
await sendInvitation({
|
await sendInvitation({
|
||||||
invitationId: result!.id,
|
invitationId: result.data.id,
|
||||||
notificationId: data.notificationId || "",
|
notificationId: data.notificationId || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -92,11 +96,10 @@ export const AddInvitation = () => {
|
|||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
} catch (error: any) {
|
|
||||||
setError(error.message || "Failed to create invitation");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.organization.allInvitations.invalidate();
|
utils.organization.allInvitations.invalidate();
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -156,11 +159,6 @@ export const AddInvitation = () => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
{customRoles?.map((role) => (
|
|
||||||
<SelectItem key={role.role} value={role.role}>
|
|
||||||
{role.role}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -214,7 +212,7 @@ export const AddInvitation = () => {
|
|||||||
)}
|
)}
|
||||||
<DialogFooter className="flex w-full flex-row">
|
<DialogFooter className="flex w-full flex-row">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isInviting}
|
isLoading={isLoading}
|
||||||
form="hook-form-add-invitation"
|
form="hook-form-add-invitation"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ export type Services = {
|
|||||||
| "mysql"
|
| "mysql"
|
||||||
| "mongo"
|
| "mongo"
|
||||||
| "redis"
|
| "redis"
|
||||||
| "compose"
|
| "compose";
|
||||||
| "libsql";
|
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -137,18 +136,6 @@ export const extractServices = (data: Environment | undefined) => {
|
|||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
})) ?? []) as Services[];
|
})) ?? []) as Services[];
|
||||||
|
|
||||||
const libsql: Services[] =
|
|
||||||
data?.libsql?.map((item) => ({
|
|
||||||
appName: item.appName,
|
|
||||||
name: item.name,
|
|
||||||
type: "libsql" as const,
|
|
||||||
id: item.libsqlId,
|
|
||||||
createdAt: item.createdAt,
|
|
||||||
status: item.applicationStatus,
|
|
||||||
description: item.description,
|
|
||||||
serverId: item.serverId,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
applications.push(
|
applications.push(
|
||||||
...mysql,
|
...mysql,
|
||||||
...redis,
|
...redis,
|
||||||
@@ -156,7 +143,6 @@ export const extractServices = (data: Environment | undefined) => {
|
|||||||
...postgres,
|
...postgres,
|
||||||
...mariadb,
|
...mariadb,
|
||||||
...compose,
|
...compose,
|
||||||
...libsql,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
applications.sort((a, b) => {
|
applications.sort((a, b) => {
|
||||||
@@ -187,11 +173,9 @@ type AddPermissions = z.infer<typeof addPermissions>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userId: string;
|
userId: string;
|
||||||
role?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddUserPermissions = ({ userId, role }: Props) => {
|
export const AddUserPermissions = ({ userId }: Props) => {
|
||||||
const isCustomRole = !!role && !["owner", "admin", "member"].includes(role);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
@@ -300,237 +284,226 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
||||||
>
|
>
|
||||||
{isCustomRole && (
|
<FormField
|
||||||
<div className="md:col-span-2 rounded-lg border p-3 bg-muted/50 text-sm text-muted-foreground">
|
control={form.control}
|
||||||
This user has a custom role assigned. Capabilities are defined
|
name="canCreateProjects"
|
||||||
by the role. You can still manage which projects, environments,
|
render={({ field }) => (
|
||||||
and services they can access below.
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</div>
|
<div className="space-y-0.5">
|
||||||
)}
|
<FormLabel>Create Projects</FormLabel>
|
||||||
{!isCustomRole && (
|
<FormDescription>
|
||||||
<>
|
Allow the user to create projects
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canCreateProjects"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Create Projects</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to create projects
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canDeleteProjects"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Delete Projects</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to delete projects
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canDeleteProjects"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Delete Projects</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to delete projects
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canCreateServices"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Create Services</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to create services
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canCreateServices"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Create Services</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to create services
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canDeleteServices"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Delete Services</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to delete services
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canDeleteServices"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Delete Services</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to delete services
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canCreateEnvironments"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Create Environments</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to create environments
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canCreateEnvironments"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Create Environments</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to create environments
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canDeleteEnvironments"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Delete Environments</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to delete environments
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canDeleteEnvironments"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Delete Environments</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to delete environments
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canAccessToTraefikFiles"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Access to Traefik Files</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to access to the Traefik Tab Files
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canAccessToTraefikFiles"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Access to Traefik Files</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to access to the Traefik Tab Files
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canAccessToDocker"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Access to Docker</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to access to the Docker Tab
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canAccessToDocker"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Access to Docker</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to access to the Docker Tab
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canAccessToAPI"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Access to API/CLI</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow the user to access to the API/CLI
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canAccessToAPI"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Access to API/CLI</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow the user to access to the API/CLI
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canAccessToSSHKeys"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Access to SSH Keys</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow to users to access to the SSH Keys section
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canAccessToSSHKeys"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Access to SSH Keys</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow to users to access to the SSH Keys section
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
<FormField
|
||||||
<Switch
|
control={form.control}
|
||||||
checked={field.value}
|
name="canAccessToGitProviders"
|
||||||
onCheckedChange={field.onChange}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
</FormControl>
|
<div className="space-y-0.5">
|
||||||
</FormItem>
|
<FormLabel>Access to Git Providers</FormLabel>
|
||||||
)}
|
<FormDescription>
|
||||||
/>
|
Allow to users to access to the Git Providers section
|
||||||
<FormField
|
</FormDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="canAccessToGitProviders"
|
<FormControl>
|
||||||
render={({ field }) => (
|
<Switch
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
checked={field.value}
|
||||||
<div className="space-y-0.5">
|
onCheckedChange={field.onChange}
|
||||||
<FormLabel>Access to Git Providers</FormLabel>
|
/>
|
||||||
<FormDescription>
|
</FormControl>
|
||||||
Allow to users to access to the Git Providers section
|
</FormItem>
|
||||||
</FormDescription>
|
)}
|
||||||
</div>
|
/>
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accessedProjects"
|
name="accessedProjects"
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const changeRoleSchema = z.object({
|
const changeRoleSchema = z.object({
|
||||||
role: z.string().min(1),
|
role: z.enum(["admin", "member"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memberId: string;
|
memberId: string;
|
||||||
currentRole: string;
|
currentRole: "admin" | "member";
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,10 +49,6 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { data: customRoles } = api.customRole.all.useQuery(undefined, {
|
|
||||||
enabled: isOpen,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isPending } =
|
const { mutateAsync, isError, error, isPending } =
|
||||||
api.organization.updateMemberRole.useMutation();
|
api.organization.updateMemberRole.useMutation();
|
||||||
|
|
||||||
@@ -129,14 +125,6 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
{customRoles?.map((customRole) => (
|
|
||||||
<SelectItem
|
|
||||||
key={customRole.role}
|
|
||||||
value={customRole.role}
|
|
||||||
>
|
|
||||||
{customRole.role}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -144,13 +132,6 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
<br />
|
<br />
|
||||||
<strong>Member:</strong> Limited permissions, can be
|
<strong>Member:</strong> Limited permissions, can be
|
||||||
customized.
|
customized.
|
||||||
{customRoles && customRoles.length > 0 && (
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
<strong>Custom roles:</strong> Enterprise-defined
|
|
||||||
permissions.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<br />
|
<br />
|
||||||
<em className="text-muted-foreground text-xs">
|
<em className="text-muted-foreground text-xs">
|
||||||
Note: Owner role is intransferible.
|
Note: Owner role is intransferible.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -36,19 +35,9 @@ export const ShowUsers = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isPending, refetch } = api.user.all.useQuery();
|
const { data, isPending, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync } = api.user.remove.useMutation();
|
const { mutateAsync } = api.user.remove.useMutation();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const { data: hasValidLicense } =
|
|
||||||
api.licenseKey.haveValidLicenseKey.useQuery();
|
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: session } = api.user.session.useQuery();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
const FREE_ROLES = ["owner", "admin", "member"];
|
|
||||||
const membersWithCustomRoles = data?.filter(
|
|
||||||
(member) => !FREE_ROLES.includes(member.role),
|
|
||||||
);
|
|
||||||
const hasCustomRolesWithoutLicense =
|
|
||||||
!hasValidLicense && (membersWithCustomRoles?.length ?? 0) > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -80,18 +69,6 @@ export const ShowUsers = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
{hasCustomRolesWithoutLicense && (
|
|
||||||
<AlertBlock type="warning">
|
|
||||||
You have{" "}
|
|
||||||
{membersWithCustomRoles?.length === 1
|
|
||||||
? "1 user"
|
|
||||||
: `${membersWithCustomRoles?.length} users`}{" "}
|
|
||||||
assigned to custom roles. Custom roles will not work
|
|
||||||
without a valid Enterprise license. Please activate your
|
|
||||||
license or change these users to a free role (Admin or
|
|
||||||
Member).
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -112,39 +89,40 @@ export const ShowUsers = () => {
|
|||||||
)?.role;
|
)?.role;
|
||||||
|
|
||||||
// Owner never has "Edit Permissions" (they're absolute owner)
|
// Owner never has "Edit Permissions" (they're absolute owner)
|
||||||
// Other users can edit permissions if target is not themselves and target is a member/custom role
|
// Other users can edit permissions if target is not themselves and target is a member
|
||||||
const isStaticAdminOrOwner =
|
|
||||||
member.role === "owner" || member.role === "admin";
|
|
||||||
const canEditPermissions =
|
const canEditPermissions =
|
||||||
!isStaticAdminOrOwner &&
|
member.role !== "owner" &&
|
||||||
|
member.role === "member" &&
|
||||||
member.user.id !== session?.user?.id;
|
member.user.id !== session?.user?.id;
|
||||||
|
|
||||||
// Can change role based on hierarchy:
|
// Can change role based on hierarchy:
|
||||||
// - Owner: Can change anyone's role (except themselves and other owners)
|
// - Owner: Can change anyone's role (except themselves and other owners)
|
||||||
// - Admin: Can only change member/custom roles (not other admins or owners)
|
// - Admin: Can only change member roles (not other admins or owners)
|
||||||
// - Owner role is intransferible
|
// - Owner role is intransferible
|
||||||
const canChangeRole =
|
const canChangeRole =
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
member.user.id !== session?.user?.id &&
|
member.user.id !== session?.user?.id &&
|
||||||
(currentUserRole === "owner" ||
|
(currentUserRole === "owner" ||
|
||||||
(currentUserRole === "admin" &&
|
(currentUserRole === "admin" &&
|
||||||
member.role !== "admin"));
|
member.role === "member"));
|
||||||
|
|
||||||
const canDeleteMember =
|
// Delete/Unlink follow same hierarchy as role changes
|
||||||
permissions?.member.delete ?? false;
|
// - Owner: Can delete/unlink anyone (except themselves and owner can't be deleted)
|
||||||
|
// - Admin: Can only delete/unlink members (not other admins or owner)
|
||||||
|
const canDelete =
|
||||||
|
member.role !== "owner" &&
|
||||||
|
!isCloud &&
|
||||||
|
member.user.id !== session?.user?.id &&
|
||||||
|
(currentUserRole === "owner" ||
|
||||||
|
(currentUserRole === "admin" &&
|
||||||
|
member.role === "member"));
|
||||||
|
|
||||||
// Self-hosted: "Delete User" removes the user entirely
|
const canUnlink =
|
||||||
// Cloud: "Unlink User" removes from the organization only
|
|
||||||
const canRemove =
|
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
member.user.id !== session?.user?.id &&
|
member.user.id !== session?.user?.id &&
|
||||||
(currentUserRole === "owner" ||
|
(currentUserRole === "owner" ||
|
||||||
(currentUserRole === "admin" &&
|
(currentUserRole === "admin" &&
|
||||||
member.role !== "admin") ||
|
member.role === "member"));
|
||||||
(canDeleteMember && !isStaticAdminOrOwner));
|
|
||||||
|
|
||||||
const canDelete = canRemove && !isCloud;
|
|
||||||
const canUnlink = canRemove && !!isCloud;
|
|
||||||
|
|
||||||
const hasAnyAction =
|
const hasAnyAction =
|
||||||
canEditPermissions ||
|
canEditPermissions ||
|
||||||
@@ -156,11 +134,6 @@ export const ShowUsers = () => {
|
|||||||
<TableRow key={member.id}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[100px]">
|
||||||
{member.user.email}
|
{member.user.email}
|
||||||
{member.user.id === session?.user?.id && (
|
|
||||||
<span className="text-muted-foreground ml-1">
|
|
||||||
(You)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
@@ -206,7 +179,9 @@ export const ShowUsers = () => {
|
|||||||
{canChangeRole && (
|
{canChangeRole && (
|
||||||
<ChangeRole
|
<ChangeRole
|
||||||
memberId={member.id}
|
memberId={member.id}
|
||||||
currentRole={member.role}
|
currentRole={
|
||||||
|
member.role as "admin" | "member"
|
||||||
|
}
|
||||||
userEmail={member.user.email}
|
userEmail={member.user.email}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -214,7 +189,6 @@ export const ShowUsers = () => {
|
|||||||
{canEditPermissions && (
|
{canEditPermissions && (
|
||||||
<AddUserPermissions
|
<AddUserPermissions
|
||||||
userId={member.user.id}
|
userId={member.user.id}
|
||||||
role={member.role}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { HardDriveDownload, Loader2 } from "lucide-react";
|
||||||
AlertTriangle,
|
|
||||||
CheckCircle2,
|
|
||||||
HardDriveDownload,
|
|
||||||
Loader2,
|
|
||||||
RefreshCw,
|
|
||||||
XCircle,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -22,70 +15,11 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
type ServiceStatus = {
|
|
||||||
status: "healthy" | "unhealthy";
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type HealthResult = {
|
|
||||||
postgres: ServiceStatus;
|
|
||||||
redis: ServiceStatus;
|
|
||||||
traefik: ServiceStatus;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ModalState = "idle" | "checking" | "results" | "updating";
|
|
||||||
|
|
||||||
const ServiceStatusItem = ({
|
|
||||||
name,
|
|
||||||
service,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
service: ServiceStatus;
|
|
||||||
}) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{service.status === "healthy" ? (
|
|
||||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
|
||||||
) : (
|
|
||||||
<XCircle className="h-4 w-4 text-red-500" />
|
|
||||||
)}
|
|
||||||
<span className="text-sm font-medium">{name}</span>
|
|
||||||
{service.status === "unhealthy" && service.message && (
|
|
||||||
<span className="text-xs text-muted-foreground">— {service.message}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const UpdateWebServer = () => {
|
export const UpdateWebServer = () => {
|
||||||
const [modalState, setModalState] = useState<ModalState>("idle");
|
const [updating, setUpdating] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [healthResult, setHealthResult] = useState<HealthResult | null>(null);
|
|
||||||
|
|
||||||
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
|
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
|
||||||
const { refetch: checkHealth } =
|
|
||||||
api.settings.checkInfrastructureHealth.useQuery(undefined, {
|
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleVerify = async () => {
|
|
||||||
setModalState("checking");
|
|
||||||
setHealthResult(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await checkHealth();
|
|
||||||
if (result.data) {
|
|
||||||
setHealthResult(result.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// checkHealth failed entirely
|
|
||||||
}
|
|
||||||
setModalState("results");
|
|
||||||
};
|
|
||||||
|
|
||||||
const allHealthy =
|
|
||||||
healthResult &&
|
|
||||||
healthResult.postgres.status === "healthy" &&
|
|
||||||
healthResult.redis.status === "healthy" &&
|
|
||||||
healthResult.traefik.status === "healthy";
|
|
||||||
|
|
||||||
const checkIsUpdateFinished = async () => {
|
const checkIsUpdateFinished = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -99,24 +33,28 @@ export const UpdateWebServer = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
// Allow seeing the toast before reloading
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch {
|
} catch {
|
||||||
|
// Delay each request
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
// Keep running until it returns 200
|
||||||
void checkIsUpdateFinished();
|
void checkIsUpdateFinished();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
setModalState("updating");
|
setUpdating(true);
|
||||||
await updateServer();
|
await updateServer();
|
||||||
|
|
||||||
|
// Give some time for docker service restart before starting to check status
|
||||||
await new Promise((resolve) => setTimeout(resolve, 8000));
|
await new Promise((resolve) => setTimeout(resolve, 8000));
|
||||||
|
|
||||||
await checkIsUpdateFinished();
|
await checkIsUpdateFinished();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setModalState("results");
|
setUpdating(false);
|
||||||
console.error("Error updating server:", error);
|
console.error("Error updating server:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
"An error occurred while updating the server, please try again.",
|
"An error occurred while updating the server, please try again.",
|
||||||
@@ -124,14 +62,6 @@ export const UpdateWebServer = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
if (modalState !== "updating") {
|
|
||||||
setOpen(false);
|
|
||||||
setModalState("idle");
|
|
||||||
setHealthResult(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open}>
|
<AlertDialog open={open}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
@@ -151,111 +81,36 @@ export const UpdateWebServer = () => {
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
{modalState === "idle" && "Are you absolutely sure?"}
|
{updating
|
||||||
{modalState === "checking" && "Verifying Services..."}
|
? "Server update in progress"
|
||||||
{modalState === "results" &&
|
: "Are you absolutely sure?"}
|
||||||
(allHealthy ? "Ready to Update" : "Service Issues Detected")}
|
|
||||||
{modalState === "updating" && "Server update in progress"}
|
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription asChild>
|
<AlertDialogDescription>
|
||||||
<div>
|
{updating ? (
|
||||||
{modalState === "idle" && (
|
<span className="flex items-center gap-1">
|
||||||
<span>
|
<Loader2 className="animate-spin" />
|
||||||
This will update the web server to the new version. You will
|
The server is being updated, please wait...
|
||||||
not be able to use the panel during the update process. The
|
</span>
|
||||||
page will be reloaded once the update is finished.
|
) : (
|
||||||
<br />
|
<>
|
||||||
<br />
|
This action cannot be undone. This will update the web server to
|
||||||
We recommend verifying that all services are running before
|
the new version. You will not be able to use the panel during
|
||||||
updating.
|
the update process. The page will be reloaded once the update is
|
||||||
</span>
|
finished.
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
{modalState === "checking" && (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<Loader2 className="animate-spin h-4 w-4" />
|
|
||||||
Checking PostgreSQL, Redis and Traefik...
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalState === "results" && healthResult && (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<ServiceStatusItem
|
|
||||||
name="PostgreSQL"
|
|
||||||
service={healthResult.postgres}
|
|
||||||
/>
|
|
||||||
<ServiceStatusItem
|
|
||||||
name="Redis"
|
|
||||||
service={healthResult.redis}
|
|
||||||
/>
|
|
||||||
<ServiceStatusItem
|
|
||||||
name="Traefik"
|
|
||||||
service={healthResult.traefik}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!allHealthy && (
|
|
||||||
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
|
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
|
||||||
Some services are not healthy. You can still proceed
|
|
||||||
with the update.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allHealthy && (
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
All services are running. You can proceed with the update.
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalState === "results" && !healthResult && (
|
|
||||||
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
|
|
||||||
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
|
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
|
||||||
Could not verify services. You can still proceed with the
|
|
||||||
update.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{modalState === "updating" && (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<Loader2 className="animate-spin h-4 w-4" />
|
|
||||||
The server is being updated, please wait...
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
{modalState === "idle" && (
|
{!updating && (
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
|
<AlertDialogCancel onClick={() => setOpen(false)}>
|
||||||
<Button variant="secondary" onClick={handleVerify}>
|
Cancel
|
||||||
<RefreshCw className="h-4 w-4" />
|
</AlertDialogCancel>
|
||||||
Verify Status
|
|
||||||
</Button>
|
|
||||||
<AlertDialogAction onClick={handleConfirm}>
|
<AlertDialogAction onClick={handleConfirm}>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
)}
|
)}
|
||||||
{modalState === "results" && (
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
|
|
||||||
<Button variant="secondary" onClick={handleVerify}>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
Re-check
|
|
||||||
</Button>
|
|
||||||
<AlertDialogAction onClick={handleConfirm}>
|
|
||||||
{allHealthy ? "Confirm" : "Confirm Anyway"}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
)}
|
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,18 +17,17 @@ import { api } from "@/utils/api";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RebuildDatabase = ({ id, type }: Props) => {
|
export const RebuildDatabase = ({ id, type }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const mutationMap = {
|
const mutationMap = {
|
||||||
libsql: () => api.libsql.rebuild.useMutation(),
|
postgres: () => api.postgres.rebuild.useMutation(),
|
||||||
|
mysql: () => api.mysql.rebuild.useMutation(),
|
||||||
mariadb: () => api.mariadb.rebuild.useMutation(),
|
mariadb: () => api.mariadb.rebuild.useMutation(),
|
||||||
mongo: () => api.mongo.rebuild.useMutation(),
|
mongo: () => api.mongo.rebuild.useMutation(),
|
||||||
mysql: () => api.mysql.rebuild.useMutation(),
|
|
||||||
postgres: () => api.postgres.rebuild.useMutation(),
|
|
||||||
redis: () => api.redis.rebuild.useMutation(),
|
redis: () => api.redis.rebuild.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,11 +36,10 @@ export const RebuildDatabase = ({ id, type }: Props) => {
|
|||||||
const handleRebuild = async () => {
|
const handleRebuild = async () => {
|
||||||
try {
|
try {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
libsqlId: type === "libsql" ? id : "",
|
postgresId: type === "postgres" ? id : "",
|
||||||
|
mysqlId: type === "mysql" ? id : "",
|
||||||
mariadbId: type === "mariadb" ? id : "",
|
mariadbId: type === "mariadb" ? id : "",
|
||||||
mongoId: type === "mongo" ? id : "",
|
mongoId: type === "mongo" ? id : "",
|
||||||
mysqlId: type === "mysql" ? id : "",
|
|
||||||
postgresId: type === "postgres" ? id : "",
|
|
||||||
redisId: type === "redis" ? id : "",
|
redisId: type === "redis" ? id : "",
|
||||||
});
|
});
|
||||||
toast.success("Database rebuilt successfully");
|
toast.success("Database rebuilt successfully");
|
||||||
|
|||||||
@@ -6,20 +6,14 @@ import { RebuildDatabase } from "./rebuild-database";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
|
export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5">
|
<div className="flex w-full flex-col gap-5">
|
||||||
<ShowCustomCommand id={id} type={type} />
|
<ShowCustomCommand id={id} type={type} />
|
||||||
{type === "mariadb" ||
|
<ShowClusterSettings id={id} type={type} />
|
||||||
type === "mongo" ||
|
|
||||||
type === "mysql" ||
|
|
||||||
type === "postgres" ||
|
|
||||||
type === "redis" ? (
|
|
||||||
<ShowClusterSettings id={id} type={type} />
|
|
||||||
) : null}
|
|
||||||
<ShowVolumes id={id} type={type} />
|
<ShowVolumes id={id} type={type} />
|
||||||
<ShowResources id={id} type={type} />
|
<ShowResources id={id} type={type} />
|
||||||
<RebuildDatabase id={id} type={type} />
|
<RebuildDatabase id={id} type={type} />
|
||||||
|
|||||||
@@ -156,61 +156,6 @@ export const RedisIcon = ({ className }: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LibsqlIcon = ({ className }: Props) => {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
aria-label="libsql"
|
|
||||||
height="1em"
|
|
||||||
width="1em"
|
|
||||||
viewBox="0 0 217.2 217.2"
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
|
|
||||||
d="M118,87.2c.7,0,1.3,0,1.9.3.4.1.8.3,1.2.5.2.1.4.2.6.3.4.3.7.5,1.1.9l5.2,5.2c2.7,2.7,2.7,7,0,9.7l-95.1,95.1c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l85.1-85.1,10-10c.3-.3.7-.6,1.1-.9.2-.1.4-.2.6-.3.4-.2.8-.4,1.2-.5.6-.2,1.3-.3,1.9-.3M118,71.2c-2.2,0-4.4.3-6.5.9-1.4.4-2.8,1-4.1,1.7-.7.4-1.3.7-2,1.2-1.3.8-2.5,1.8-3.6,2.9l-10,10L6.7,173c-8.9,8.9-8.9,23.4,0,32.3l5.2,5.2c4.3,4.3,10.1,6.7,16.2,6.7s11.8-2.4,16.2-6.7l95.1-95.1c4.3-4.3,6.7-10.1,6.7-16.2s-2.4-11.8-6.7-16.2l-5.2-5.2c-1.1-1.1-2.3-2.1-3.6-2.9-.6-.4-1.3-.8-1.9-1.2-1.3-.7-2.7-1.3-4.1-1.7-2.1-.6-4.3-.9-6.5-.9h0Z"
|
|
||||||
/>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
|
|
||||||
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,0c-8.4,0-16.3,3.3-22.3,9.2L17.4,88.9c-8.5,8.5-11.4,20.8-7.6,32.1l16.5,49.9c3.1,9.4,10.6,16.9,20,20l49.9,16.5c3.2,1.1,6.5,1.6,9.9,1.6,8.4,0,16.3-3.3,22.3-9.2l79.7-79.7c6.3-6.3,9.7-15.1,9.2-24.1l-3.8-66.5c-.8-13.9-11.9-24.9-25.7-25.7L121.2,0c-.6,0-1.2,0-1.8,0h0Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#a8f7d9", strokeWidth: "0px" }}
|
|
||||||
d="M24.9,116.1l16.5,49.9c1.5,4.7,5.2,8.3,9.8,9.8l49.9,16.5c5.6,1.8,11.7.4,15.8-3.8l79.7-79.7c3.1-3.1,4.8-7.4,4.5-11.8l-3.8-66.5c-.3-5.8-4.9-10.3-10.7-10.7l-66.5-3.8c-4.4-.3-8.7,1.4-11.8,4.5L28.7,100.3c-4.1,4.1-5.6,10.3-3.8,15.8Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
|
||||||
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,6c-6.8,0-13.2,2.7-18,7.5L21.6,93.2c-6.8,6.8-9.2,16.8-6.2,26l16.5,49.9c2.5,7.6,8.6,13.7,16.2,16.2l49.9,16.5c2.6.9,5.3,1.3,8,1.3,6.8,0,13.2-2.7,18-7.5l79.7-79.7c5.1-5.1,7.8-12.2,7.4-19.5l-3.8-66.5c-.6-10.8-9.3-19.5-20.1-20.1l-66.5-3.8c-.5,0-1,0-1.5,0h0Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
|
||||||
d="M136.7,173.7l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
|
||||||
d="M166.5,143.9l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
|
||||||
d="M43.5,80.5l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
|
||||||
d="M73.3,50.7l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#79ac91", strokeWidth: "0px" }}
|
|
||||||
d="M130.6,101.5l-97.7,97.7c-2.7,2.7-7,2.7-9.7,0l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c2.8-.3,5.1,2.1,4.9,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5Z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
|
||||||
d="M129.5,83.3c2.6,0,4.7,2.2,4.4,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5l-97.7,97.7c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c.1,0,.3,0,.4,0M129.5,73.3h0c-.5,0-.9,0-1.4,0l-7.9.8c-4.4.4-8.5,2.4-11.5,5.5L10.9,177.3c-6.6,6.6-6.6,17.3,0,23.8l5.2,5.2c3.2,3.2,7.4,4.9,11.9,4.9s8.7-1.8,11.9-4.9l97.7-97.7c3.1-3.1,5-7.2,5.5-11.5l.8-7.9c.4-4-.9-8.1-3.7-11.1-2.7-3-6.6-4.7-10.7-4.7h0Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GitlabIcon = ({ className }: Props) => {
|
export const GitlabIcon = ({ className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -88,21 +88,6 @@ export const DiscordIcon = ({ className }: Props) => {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MattermostIcon = ({ className }: Props) => {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
fill="#0061ff"
|
|
||||||
viewBox="0 0 501 501"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className={cn("size-8", className)}
|
|
||||||
>
|
|
||||||
<path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z" />
|
|
||||||
<path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TeamsIcon = ({ className }: Props) => {
|
export const TeamsIcon = ({ className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
|
||||||
import { GithubIcon } from "../icons/data-tools-icons";
|
import { GithubIcon } from "../icons/data-tools-icons";
|
||||||
import { Logo } from "../shared/logo";
|
import { Logo } from "../shared/logo";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
@@ -10,28 +9,23 @@ interface Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
export const OnboardingLayout = ({ children }: Props) => {
|
export const OnboardingLayout = ({ children }: Props) => {
|
||||||
const { config: whitelabeling } = useWhitelabelingPublic();
|
|
||||||
const appName = whitelabeling?.appName || "Dokploy";
|
|
||||||
const appDescription =
|
|
||||||
whitelabeling?.appDescription ||
|
|
||||||
"\u201CThe Open Source alternative to Netlify, Vercel, Heroku.\u201D";
|
|
||||||
const logoUrl =
|
|
||||||
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
|
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
|
||||||
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
|
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
|
||||||
<div className="absolute inset-0 bg-muted" />
|
<div className="absolute inset-0 bg-muted" />
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="https://dokploy.com"
|
||||||
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
||||||
>
|
>
|
||||||
<Logo className="size-10" logoUrl={logoUrl} />
|
<Logo className="size-10" />
|
||||||
{appName}
|
Dokploy
|
||||||
</Link>
|
</Link>
|
||||||
<div className="relative z-20 mt-auto">
|
<div className="relative z-20 mt-auto">
|
||||||
<blockquote className="space-y-2">
|
<blockquote className="space-y-2">
|
||||||
<p className="text-lg text-primary">{appDescription}</p>
|
<p className="text-lg text-primary">
|
||||||
|
“The Open Source alternative to Netlify, Vercel,
|
||||||
|
Heroku.”
|
||||||
|
</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
ClipboardList,
|
|
||||||
Clock,
|
Clock,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
@@ -25,13 +24,10 @@ import {
|
|||||||
LogIn,
|
LogIn,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
Palette,
|
|
||||||
PieChart,
|
PieChart,
|
||||||
Rocket,
|
|
||||||
Server,
|
Server,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
Star,
|
Star,
|
||||||
Tags,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
@@ -94,21 +90,13 @@ import { UserNav } from "./user-nav";
|
|||||||
|
|
||||||
// The types of the queries we are going to use
|
// The types of the queries we are going to use
|
||||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
||||||
type PermissionsOutput =
|
|
||||||
inferRouterOutputs<AppRouter>["user"]["getPermissions"];
|
|
||||||
|
|
||||||
type EnabledOpts = {
|
|
||||||
auth?: AuthQueryOutput;
|
|
||||||
permissions?: PermissionsOutput;
|
|
||||||
isCloud: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SingleNavItem = {
|
type SingleNavItem = {
|
||||||
isSingle?: true;
|
isSingle?: true;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isEnabled?: (opts: EnabledOpts) => boolean;
|
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NavItem type
|
// NavItem type
|
||||||
@@ -122,7 +110,10 @@ type NavItem =
|
|||||||
title: string;
|
title: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
items: SingleNavItem[];
|
items: SingleNavItem[];
|
||||||
isEnabled?: (opts: EnabledOpts) => boolean;
|
isEnabled?: (opts: {
|
||||||
|
auth?: AuthQueryOutput;
|
||||||
|
isCloud: boolean;
|
||||||
|
}) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ExternalLink type
|
// ExternalLink type
|
||||||
@@ -131,7 +122,7 @@ type ExternalLink = {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
isEnabled?: (opts: EnabledOpts) => boolean;
|
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Menu type
|
// Menu type
|
||||||
@@ -154,21 +145,13 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/projects",
|
url: "/dashboard/projects",
|
||||||
icon: Folder,
|
icon: Folder,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
isSingle: true,
|
|
||||||
title: "Deployments",
|
|
||||||
url: "/dashboard/deployments",
|
|
||||||
icon: Rocket,
|
|
||||||
isEnabled: ({ permissions }) => !!permissions?.deployment.read,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Monitoring",
|
title: "Monitoring",
|
||||||
url: "/dashboard/monitoring",
|
url: "/dashboard/monitoring",
|
||||||
icon: BarChartHorizontalBigIcon,
|
icon: BarChartHorizontalBigIcon,
|
||||||
// Only enabled in non-cloud environments and if user has monitoring.read
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud, permissions }) =>
|
isEnabled: ({ isCloud }) => !isCloud,
|
||||||
!isCloud && !!permissions?.monitoring.read,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -176,44 +159,64 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/schedules",
|
url: "/dashboard/schedules",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud, permissions }) =>
|
isEnabled: ({ isCloud, auth }) =>
|
||||||
!isCloud && !!permissions?.organization.update,
|
!isCloud && (auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
url: "/dashboard/traefik",
|
url: "/dashboard/traefik",
|
||||||
icon: GalleryVerticalEnd,
|
icon: GalleryVerticalEnd,
|
||||||
// Only enabled for users with access to Traefik files in non-cloud environments
|
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(permissions?.traefikFiles.read && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToTraefikFiles) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Docker",
|
title: "Docker",
|
||||||
url: "/dashboard/docker",
|
url: "/dashboard/docker",
|
||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(permissions?.docker.read && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Swarm",
|
title: "Swarm",
|
||||||
url: "/dashboard/swarm",
|
url: "/dashboard/swarm",
|
||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(permissions?.docker.read && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Requests",
|
title: "Requests",
|
||||||
url: "/dashboard/requests",
|
url: "/dashboard/requests",
|
||||||
icon: Forward,
|
icon: Forward,
|
||||||
// Only enabled for users with access to Docker in non-cloud environments
|
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(permissions?.docker.read && !isCloud),
|
!!(
|
||||||
|
(auth?.role === "owner" ||
|
||||||
|
auth?.role === "admin" ||
|
||||||
|
auth?.canAccessToDocker) &&
|
||||||
|
!isCloud
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legacy unused menu, adjusted to the new structure
|
// Legacy unused menu, adjusted to the new structure
|
||||||
@@ -280,8 +283,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/server",
|
url: "/dashboard/settings/server",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(permissions?.organization.update && !isCloud),
|
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -294,66 +297,70 @@ const MENU: Menu = {
|
|||||||
title: "Remote Servers",
|
title: "Remote Servers",
|
||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
isEnabled: ({ permissions }) => !!permissions?.server.read,
|
// Only enabled for admins
|
||||||
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Users",
|
title: "Users",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
// Only enabled for users with member.read permission
|
// Only enabled for admins
|
||||||
isEnabled: ({ permissions }) => !!permissions?.member.read,
|
isEnabled: ({ auth }) =>
|
||||||
},
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
{
|
|
||||||
isSingle: true,
|
|
||||||
title: "Audit Logs",
|
|
||||||
icon: ClipboardList,
|
|
||||||
url: "/dashboard/settings/audit-logs",
|
|
||||||
isEnabled: ({ permissions }) => !!permissions?.auditLog.read,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "SSH Keys",
|
title: "SSH Keys",
|
||||||
icon: KeyRound,
|
icon: KeyRound,
|
||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
// Only enabled for users with access to SSH keys
|
// Only enabled for admins and users with access to SSH keys
|
||||||
isEnabled: ({ permissions }) => !!permissions?.sshKeys.read,
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(
|
||||||
|
auth?.role === "owner" ||
|
||||||
|
auth?.canAccessToSSHKeys ||
|
||||||
|
auth?.role === "admin"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI",
|
title: "AI",
|
||||||
icon: BotIcon,
|
icon: BotIcon,
|
||||||
url: "/dashboard/settings/ai",
|
url: "/dashboard/settings/ai",
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
isEnabled: ({ auth }) =>
|
||||||
},
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
{
|
|
||||||
isSingle: true,
|
|
||||||
title: "Tags",
|
|
||||||
url: "/dashboard/settings/tags",
|
|
||||||
icon: Tags,
|
|
||||||
isEnabled: ({ permissions }) => !!permissions?.tag.read,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Git",
|
title: "Git",
|
||||||
url: "/dashboard/settings/git-providers",
|
url: "/dashboard/settings/git-providers",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
// Only enabled for users with access to Git providers
|
// Only enabled for admins and users with access to Git providers
|
||||||
isEnabled: ({ permissions }) => !!permissions?.gitProviders.read,
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(
|
||||||
|
auth?.role === "owner" ||
|
||||||
|
auth?.canAccessToGitProviders ||
|
||||||
|
auth?.role === "admin"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Registry",
|
title: "Registry",
|
||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
isEnabled: ({ permissions }) => !!permissions?.registry.read,
|
// Only enabled for admins
|
||||||
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "S3 Destinations",
|
title: "S3 Destinations",
|
||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
isEnabled: ({ permissions }) => !!permissions?.destination.read,
|
// Only enabled for admins
|
||||||
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -361,7 +368,9 @@ const MENU: Menu = {
|
|||||||
title: "Certificates",
|
title: "Certificates",
|
||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
isEnabled: ({ permissions }) => !!permissions?.certificate.read,
|
// Only enabled for admins
|
||||||
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -369,23 +378,24 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ permissions, isCloud }) =>
|
isEnabled: ({ auth, isCloud }) =>
|
||||||
!!(permissions?.organization.update && !isCloud),
|
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Notifications",
|
title: "Notifications",
|
||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
// Only enabled for users with access to notifications
|
// Only enabled for admins
|
||||||
isEnabled: ({ permissions }) => !!permissions?.notification.read,
|
isEnabled: ({ auth }) =>
|
||||||
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Billing",
|
title: "Billing",
|
||||||
url: "/dashboard/settings/billing",
|
url: "/dashboard/settings/billing",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
// Only enabled for owners in cloud environments
|
// Only enabled for admins in cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -393,7 +403,7 @@ const MENU: Menu = {
|
|||||||
title: "License",
|
title: "License",
|
||||||
url: "/dashboard/settings/license",
|
url: "/dashboard/settings/license",
|
||||||
icon: Key,
|
icon: Key,
|
||||||
// Only enabled for owners
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -402,15 +412,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/sso",
|
url: "/dashboard/settings/sso",
|
||||||
icon: LogIn,
|
icon: LogIn,
|
||||||
// Enabled for admins in both cloud and self-hosted (enterprise)
|
// Enabled for admins in both cloud and self-hosted (enterprise)
|
||||||
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
isEnabled: ({ auth }) =>
|
||||||
},
|
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||||
{
|
|
||||||
isSingle: true,
|
|
||||||
title: "Whitelabeling",
|
|
||||||
url: "/dashboard/settings/whitelabeling",
|
|
||||||
icon: Palette,
|
|
||||||
// Only enabled for owners in non-cloud environments (enterprise)
|
|
||||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -434,45 +437,39 @@ const MENU: Menu = {
|
|||||||
*/
|
*/
|
||||||
function createMenuForAuthUser(opts: {
|
function createMenuForAuthUser(opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
permissions?: PermissionsOutput;
|
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
whitelabeling?: {
|
|
||||||
docsUrl?: string | null;
|
|
||||||
supportUrl?: string | null;
|
|
||||||
} | null;
|
|
||||||
}): Menu {
|
}): Menu {
|
||||||
const filterEnabled = <
|
return {
|
||||||
T extends {
|
// Filter the home items based on the user's role and permissions
|
||||||
isEnabled?: (o: EnabledOpts) => boolean;
|
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
||||||
},
|
home: MENU.home.filter((item) =>
|
||||||
>(
|
|
||||||
items: readonly T[],
|
|
||||||
): T[] =>
|
|
||||||
items.filter((item) =>
|
|
||||||
!item.isEnabled
|
!item.isEnabled
|
||||||
? true
|
? true
|
||||||
: item.isEnabled({
|
: item.isEnabled({
|
||||||
auth: opts.auth,
|
auth: opts.auth,
|
||||||
permissions: opts.permissions,
|
|
||||||
isCloud: opts.isCloud,
|
isCloud: opts.isCloud,
|
||||||
}),
|
}),
|
||||||
) as T[];
|
),
|
||||||
|
// Filter the settings items based on the user's role and permissions
|
||||||
// Apply whitelabeling URL overrides to help items
|
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
||||||
const helpItems = filterEnabled(MENU.help).map((item) => {
|
settings: MENU.settings.filter((item) =>
|
||||||
if (opts.whitelabeling?.docsUrl && item.name === "Documentation") {
|
!item.isEnabled
|
||||||
return { ...item, url: opts.whitelabeling.docsUrl };
|
? true
|
||||||
}
|
: item.isEnabled({
|
||||||
if (opts.whitelabeling?.supportUrl && item.name === "Support") {
|
auth: opts.auth,
|
||||||
return { ...item, url: opts.whitelabeling.supportUrl };
|
isCloud: opts.isCloud,
|
||||||
}
|
}),
|
||||||
return item;
|
),
|
||||||
});
|
// Filter the help items based on the user's role and permissions
|
||||||
|
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
||||||
return {
|
help: MENU.help.filter((item) =>
|
||||||
home: filterEnabled(MENU.home),
|
!item.isEnabled
|
||||||
settings: filterEnabled(MENU.settings),
|
? true
|
||||||
help: helpItems,
|
: item.isEnabled({
|
||||||
|
auth: opts.auth,
|
||||||
|
isCloud: opts.isCloud,
|
||||||
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,7 +539,7 @@ function SidebarLogo() {
|
|||||||
const { state } = useSidebar();
|
const { state } = useSidebar();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: user } = api.user.get.useQuery();
|
const { data: user } = api.user.get.useQuery();
|
||||||
const { data: session } = api.user.session.useQuery();
|
const { data: session } = authClient.useSession();
|
||||||
const {
|
const {
|
||||||
data: organizations,
|
data: organizations,
|
||||||
refetch,
|
refetch,
|
||||||
@@ -553,8 +550,8 @@ function SidebarLogo() {
|
|||||||
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
|
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
|
||||||
api.organization.setDefault.useMutation();
|
api.organization.setDefault.useMutation();
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const isCollapsed = state === "collapsed" && !isMobile;
|
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
const _utils = api.useUtils();
|
||||||
|
|
||||||
const { data: invitations, refetch: refetchInvitations } =
|
const { data: invitations, refetch: refetchInvitations } =
|
||||||
api.user.getInvitations.useQuery();
|
api.user.getInvitations.useQuery();
|
||||||
@@ -579,7 +576,9 @@ function SidebarLogo() {
|
|||||||
<SidebarMenu
|
<SidebarMenu
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-2",
|
"flex gap-2",
|
||||||
isCollapsed ? "flex-col" : "flex-row justify-between items-center",
|
state === "collapsed"
|
||||||
|
? "flex-col"
|
||||||
|
: "flex-row justify-between items-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Organization Logo and Selector */}
|
{/* Organization Logo and Selector */}
|
||||||
@@ -587,17 +586,17 @@ function SidebarLogo() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size={isCollapsed ? "sm" : "lg"}
|
size={state === "collapsed" ? "sm" : "lg"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
||||||
isCollapsed &&
|
state === "collapsed" &&
|
||||||
"flex justify-center items-center p-2 h-10 w-10 mx-auto",
|
"flex justify-center items-center p-2 h-10 w-10 mx-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2",
|
"flex items-center gap-2",
|
||||||
isCollapsed && "justify-center",
|
state === "collapsed" && "justify-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -609,7 +608,7 @@ function SidebarLogo() {
|
|||||||
<Logo
|
<Logo
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all",
|
"transition-all",
|
||||||
isCollapsed ? "size-4" : "size-5",
|
state === "collapsed" ? "size-4" : "size-5",
|
||||||
)}
|
)}
|
||||||
logoUrl={activeOrganization?.logo || undefined}
|
logoUrl={activeOrganization?.logo || undefined}
|
||||||
/>
|
/>
|
||||||
@@ -617,7 +616,7 @@ function SidebarLogo() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start",
|
"flex flex-col items-start",
|
||||||
isCollapsed && "hidden",
|
state === "collapsed" && "hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium leading-none">
|
<p className="text-sm font-medium leading-none">
|
||||||
@@ -626,7 +625,7 @@ function SidebarLogo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown
|
<ChevronsUpDown
|
||||||
className={cn("ml-auto", isCollapsed && "hidden")}
|
className={cn("ml-auto", state === "collapsed" && "hidden")}
|
||||||
/>
|
/>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -775,7 +774,7 @@ function SidebarLogo() {
|
|||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|
||||||
{/* Notification Bell */}
|
{/* Notification Bell */}
|
||||||
<SidebarMenuItem className={cn(isCollapsed && "mt-2")}>
|
<SidebarMenuItem className={cn(state === "collapsed" && "mt-2")}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -783,7 +782,7 @@ function SidebarLogo() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative",
|
"relative",
|
||||||
isCollapsed && "h-8 w-8 p-1.5 mx-auto",
|
state === "collapsed" && "h-8 w-8 p-1.5 mx-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Bell className="size-4" />
|
<Bell className="size-4" />
|
||||||
@@ -879,12 +878,7 @@ export default function Page({ children }: Props) {
|
|||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
const { data: whitelabeling } = api.whitelabeling.get.useQuery(undefined, {
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const includesProjects = pathname?.includes("/dashboard/project");
|
const includesProjects = pathname?.includes("/dashboard/project");
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
@@ -893,12 +887,7 @@ export default function Page({ children }: Props) {
|
|||||||
home: filteredHome,
|
home: filteredHome,
|
||||||
settings: filteredSettings,
|
settings: filteredSettings,
|
||||||
help,
|
help,
|
||||||
} = createMenuForAuthUser({
|
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
|
||||||
auth,
|
|
||||||
permissions,
|
|
||||||
isCloud: !!isCloud,
|
|
||||||
whitelabeling,
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeItem = findActiveNavItem(
|
const activeItem = findActiveNavItem(
|
||||||
[...filteredHome, ...filteredSettings],
|
[...filteredHome, ...filteredSettings],
|
||||||
@@ -916,7 +905,6 @@ export default function Page({ children }: Props) {
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
setDefaultOpen(open);
|
setDefaultOpen(open);
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noDocumentCookie: this sets the cookie to keep the sidebar state.
|
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`;
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`;
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
@@ -1139,7 +1127,7 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu className="flex flex-col gap-2">
|
<SidebarMenu className="flex flex-col gap-2">
|
||||||
{!isCloud && permissions?.organization.update && (
|
{!isCloud && (auth?.role === "owner" || auth?.role === "admin") && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UpdateServerButton />
|
<UpdateServerButton />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -1147,15 +1135,15 @@ export default function Page({ children }: Props) {
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
{whitelabeling?.footerText && (
|
|
||||||
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
|
||||||
{whitelabeling.footerText}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{dokployVersion && (
|
{dokployVersion && (
|
||||||
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
<>
|
||||||
Version {dokployVersion}
|
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||||
</div>
|
Version {dokployVersion}
|
||||||
|
</div>
|
||||||
|
<div className="hidden text-xs text-muted-foreground text-center group-data-[collapsible=icon]:block">
|
||||||
|
{dokployVersion}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
|||||||
export const UserNav = () => {
|
export const UserNav = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.user.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
// const { mutateAsync } = api.auth.logout.useMutation();
|
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||||
@@ -95,7 +94,9 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{permissions?.traefikFiles.read && (
|
{(data?.role === "owner" ||
|
||||||
|
data?.role === "admin" ||
|
||||||
|
data?.canAccessToTraefikFiles) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -105,7 +106,9 @@ export const UserNav = () => {
|
|||||||
Traefik
|
Traefik
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{permissions?.docker.read && (
|
{(data?.role === "owner" ||
|
||||||
|
data?.role === "admin" ||
|
||||||
|
data?.canAccessToDocker) && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -119,7 +122,7 @@ export const UserNav = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
permissions?.organization.update && (
|
(data?.role === "owner" || data?.role === "admin") && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { AuditLog } from "@dokploy/server/db/schema";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import {
|
|
||||||
ArrowUpDown,
|
|
||||||
FileJson,
|
|
||||||
LogIn,
|
|
||||||
LogOut,
|
|
||||||
PlusCircle,
|
|
||||||
RefreshCw,
|
|
||||||
RotateCcw,
|
|
||||||
Trash2,
|
|
||||||
Upload,
|
|
||||||
XCircle,
|
|
||||||
} from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
const ACTION_CONFIG: Record<
|
|
||||||
string,
|
|
||||||
{ label: string; icon: React.ElementType; className: string }
|
|
||||||
> = {
|
|
||||||
create: {
|
|
||||||
label: "Created",
|
|
||||||
icon: PlusCircle,
|
|
||||||
className:
|
|
||||||
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
label: "Updated",
|
|
||||||
icon: RefreshCw,
|
|
||||||
className:
|
|
||||||
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
label: "Deleted",
|
|
||||||
icon: Trash2,
|
|
||||||
className: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
|
|
||||||
},
|
|
||||||
deploy: {
|
|
||||||
label: "Deployed",
|
|
||||||
icon: Upload,
|
|
||||||
className:
|
|
||||||
"bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
|
|
||||||
},
|
|
||||||
cancel: {
|
|
||||||
label: "Cancelled",
|
|
||||||
icon: XCircle,
|
|
||||||
className:
|
|
||||||
"bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20",
|
|
||||||
},
|
|
||||||
redeploy: {
|
|
||||||
label: "Redeployed",
|
|
||||||
icon: RotateCcw,
|
|
||||||
className:
|
|
||||||
"bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
|
|
||||||
},
|
|
||||||
login: {
|
|
||||||
label: "Login",
|
|
||||||
icon: LogIn,
|
|
||||||
className:
|
|
||||||
"bg-teal-500/10 text-teal-600 dark:text-teal-400 border-teal-500/20",
|
|
||||||
},
|
|
||||||
logout: {
|
|
||||||
label: "Logout",
|
|
||||||
icon: LogOut,
|
|
||||||
className:
|
|
||||||
"bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const RESOURCE_LABELS: Record<string, string> = {
|
|
||||||
project: "Project",
|
|
||||||
service: "Service",
|
|
||||||
environment: "Environment",
|
|
||||||
deployment: "Deployment",
|
|
||||||
user: "User",
|
|
||||||
customRole: "Custom Role",
|
|
||||||
domain: "Domain",
|
|
||||||
certificate: "Certificate",
|
|
||||||
registry: "Registry",
|
|
||||||
server: "Server",
|
|
||||||
sshKey: "SSH Key",
|
|
||||||
gitProvider: "Git Provider",
|
|
||||||
notification: "Notification",
|
|
||||||
settings: "Settings",
|
|
||||||
session: "Session",
|
|
||||||
};
|
|
||||||
|
|
||||||
function MetadataCell({ metadata }: { metadata: string | null }) {
|
|
||||||
if (!metadata)
|
|
||||||
return <span className="text-muted-foreground text-sm">—</span>;
|
|
||||||
|
|
||||||
const formatted = React.useMemo(() => {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(JSON.parse(metadata), null, 2);
|
|
||||||
} catch {
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="ghost" size="sm" className="h-7 gap-1.5 text-xs">
|
|
||||||
<FileJson className="h-3.5 w-3.5" />
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Metadata</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<CodeEditor
|
|
||||||
value={formatted}
|
|
||||||
language="json"
|
|
||||||
lineNumbers={false}
|
|
||||||
readOnly
|
|
||||||
className="min-h-[200px] max-h-[400px] overflow-auto rounded-md"
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const columns: ColumnDef<AuditLog>[] = [
|
|
||||||
{
|
|
||||||
accessorKey: "createdAt",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Date
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
|
||||||
{format(new Date(row.getValue("createdAt")), "MMM d, yyyy HH:mm")}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "userEmail",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
User
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-sm">{row.getValue("userEmail")}</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "action",
|
|
||||||
header: ({ column }) => (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Action
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const action = row.getValue("action") as string;
|
|
||||||
const config = ACTION_CONFIG[action];
|
|
||||||
if (!config) {
|
|
||||||
return <span className="text-xs text-muted-foreground">{action}</span>;
|
|
||||||
}
|
|
||||||
const Icon = config.icon;
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${config.className}`}
|
|
||||||
>
|
|
||||||
<Icon className="size-3" />
|
|
||||||
{config.label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "resourceType",
|
|
||||||
header: "Resource",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{RESOURCE_LABELS[row.getValue("resourceType") as string] ??
|
|
||||||
row.getValue("resourceType")}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "resourceName",
|
|
||||||
header: "Name",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{(row.getValue("resourceName") as string) ?? "—"}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "userRole",
|
|
||||||
header: "Role",
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<span className="text-sm text-muted-foreground capitalize">
|
|
||||||
{row.getValue("userRole")}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "metadata",
|
|
||||||
header: "Metadata",
|
|
||||||
cell: ({ row }) => <MetadataCell metadata={row.getValue("metadata")} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { AuditLog } from "@dokploy/server/db/schema";
|
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
|
||||||
import {
|
|
||||||
type ColumnDef,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
type SortingState,
|
|
||||||
useReactTable,
|
|
||||||
type VisibilityState,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { CalendarIcon, ChevronDown, X } from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
import type { DateRange } from "react-day-picker";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
const ACTION_OPTIONS = [
|
|
||||||
{ value: "create", label: "Created" },
|
|
||||||
{ value: "update", label: "Updated" },
|
|
||||||
{ value: "delete", label: "Deleted" },
|
|
||||||
{ value: "deploy", label: "Deployed" },
|
|
||||||
{ value: "cancel", label: "Cancelled" },
|
|
||||||
{ value: "redeploy", label: "Redeployed" },
|
|
||||||
{ value: "login", label: "Login" },
|
|
||||||
{ value: "logout", label: "Logout" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const RESOURCE_OPTIONS = [
|
|
||||||
{ value: "project", label: "Projects" },
|
|
||||||
{ value: "service", label: "Applications / Services" },
|
|
||||||
{ value: "environment", label: "Environments" },
|
|
||||||
{ value: "deployment", label: "Deployments" },
|
|
||||||
{ value: "user", label: "Users" },
|
|
||||||
{ value: "customRole", label: "Custom Roles" },
|
|
||||||
{ value: "domain", label: "Domains" },
|
|
||||||
{ value: "certificate", label: "Certificates" },
|
|
||||||
{ value: "registry", label: "Registries" },
|
|
||||||
{ value: "server", label: "Remote Servers" },
|
|
||||||
{ value: "sshKey", label: "SSH Keys" },
|
|
||||||
{ value: "gitProvider", label: "Git Providers" },
|
|
||||||
{ value: "notification", label: "Notifications" },
|
|
||||||
{ value: "settings", label: "Settings" },
|
|
||||||
{ value: "session", label: "Sessions (Login/Logout)" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const PAGE_SIZE_OPTIONS = [25, 50, 100, 200];
|
|
||||||
|
|
||||||
type AuditAction =
|
|
||||||
| "create"
|
|
||||||
| "update"
|
|
||||||
| "delete"
|
|
||||||
| "deploy"
|
|
||||||
| "cancel"
|
|
||||||
| "redeploy"
|
|
||||||
| "login"
|
|
||||||
| "logout";
|
|
||||||
type AuditResourceType =
|
|
||||||
| "project"
|
|
||||||
| "service"
|
|
||||||
| "environment"
|
|
||||||
| "deployment"
|
|
||||||
| "user"
|
|
||||||
| "customRole"
|
|
||||||
| "domain"
|
|
||||||
| "certificate"
|
|
||||||
| "registry"
|
|
||||||
| "server"
|
|
||||||
| "sshKey"
|
|
||||||
| "gitProvider"
|
|
||||||
| "notification"
|
|
||||||
| "settings"
|
|
||||||
| "session";
|
|
||||||
|
|
||||||
export interface AuditLogFilters {
|
|
||||||
userEmail: string;
|
|
||||||
resourceName: string;
|
|
||||||
action: AuditAction | "";
|
|
||||||
resourceType: AuditResourceType | "";
|
|
||||||
dateRange: DateRange | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataTableProps {
|
|
||||||
columns: ColumnDef<AuditLog>[];
|
|
||||||
data: AuditLog[];
|
|
||||||
total: number;
|
|
||||||
pageIndex: number;
|
|
||||||
pageSize: number;
|
|
||||||
filters: AuditLogFilters;
|
|
||||||
onPageChange: (page: number) => void;
|
|
||||||
onPageSizeChange: (size: number) => void;
|
|
||||||
onFilterChange: <K extends keyof AuditLogFilters>(
|
|
||||||
key: K,
|
|
||||||
value: AuditLogFilters[K],
|
|
||||||
) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DataTable({
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
total,
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
filters,
|
|
||||||
onPageChange,
|
|
||||||
onPageSizeChange,
|
|
||||||
onFilterChange,
|
|
||||||
isLoading,
|
|
||||||
}: DataTableProps) {
|
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([
|
|
||||||
{ id: "createdAt", desc: true },
|
|
||||||
]);
|
|
||||||
const [columnVisibility, setColumnVisibility] =
|
|
||||||
React.useState<VisibilityState>({});
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
onSortingChange: setSorting,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
|
||||||
manualPagination: true,
|
|
||||||
manualFiltering: true,
|
|
||||||
rowCount: total,
|
|
||||||
state: {
|
|
||||||
sorting,
|
|
||||||
columnVisibility,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageCount = Math.ceil(total / pageSize);
|
|
||||||
const hasFilters =
|
|
||||||
filters.userEmail ||
|
|
||||||
filters.resourceName ||
|
|
||||||
filters.action ||
|
|
||||||
filters.resourceType ||
|
|
||||||
filters.dateRange;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4 w-full">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<Input
|
|
||||||
placeholder="Filter by user..."
|
|
||||||
value={filters.userEmail}
|
|
||||||
onChange={(e) => onFilterChange("userEmail", e.target.value)}
|
|
||||||
className="max-w-xs"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="Filter by name..."
|
|
||||||
value={filters.resourceName}
|
|
||||||
onChange={(e) => onFilterChange("resourceName", e.target.value)}
|
|
||||||
className="max-w-xs"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
value={filters.action || "__all__"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
onFilterChange(
|
|
||||||
"action",
|
|
||||||
value === "__all__" ? "" : (value as AuditAction),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[160px]">
|
|
||||||
<SelectValue placeholder="All actions" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__all__">All actions</SelectItem>
|
|
||||||
{ACTION_OPTIONS.map((opt) => (
|
|
||||||
<SelectItem key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select
|
|
||||||
value={filters.resourceType || "__all__"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
onFilterChange(
|
|
||||||
"resourceType",
|
|
||||||
value === "__all__" ? "" : (value as AuditResourceType),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[200px]">
|
|
||||||
<SelectValue placeholder="All resources" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__all__">All resources</SelectItem>
|
|
||||||
{RESOURCE_OPTIONS.map((opt) => (
|
|
||||||
<SelectItem key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-9 gap-1.5 text-sm font-normal"
|
|
||||||
>
|
|
||||||
<CalendarIcon className="h-4 w-4" />
|
|
||||||
{filters.dateRange?.from ? (
|
|
||||||
filters.dateRange.to ? (
|
|
||||||
`${format(filters.dateRange.from, "MMM d")} – ${format(filters.dateRange.to, "MMM d, yyyy")}`
|
|
||||||
) : (
|
|
||||||
format(filters.dateRange.from, "MMM d, yyyy")
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">Date range</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<Calendar
|
|
||||||
mode="range"
|
|
||||||
selected={filters.dateRange}
|
|
||||||
onSelect={(range) => onFilterChange("dateRange", range)}
|
|
||||||
numberOfMonths={2}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
{hasFilters && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
onFilterChange("userEmail", "");
|
|
||||||
onFilterChange("resourceName", "");
|
|
||||||
onFilterChange("action", "");
|
|
||||||
onFilterChange("resourceType", "");
|
|
||||||
onFilterChange("dateRange", undefined);
|
|
||||||
}}
|
|
||||||
className="text-muted-foreground"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 mr-1" />
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" className="ml-auto">
|
|
||||||
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
{table
|
|
||||||
.getAllColumns()
|
|
||||||
.filter((col) => col.getCanHide())
|
|
||||||
.map((col) => (
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
key={col.id}
|
|
||||||
className="capitalize"
|
|
||||||
checked={col.getIsVisible()}
|
|
||||||
onCheckedChange={(value) => col.toggleVisibility(!!value)}
|
|
||||||
>
|
|
||||||
{col.id}
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-md border overflow-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<TableRow key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => (
|
|
||||||
<TableHead key={header.id}>
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext(),
|
|
||||||
)}
|
|
||||||
</TableHead>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{isLoading ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Loading...
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : table.getRowModel().rows.length ? (
|
|
||||||
table.getRowModel().rows.map((row) => (
|
|
||||||
<TableRow key={row.id}>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<TableCell key={cell.id}>
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext(),
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
No audit logs found.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
||||||
<span>
|
|
||||||
{total} {total === 1 ? "entry" : "entries"} total
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm whitespace-nowrap">Rows per page</span>
|
|
||||||
<Select
|
|
||||||
value={String(pageSize)}
|
|
||||||
onValueChange={(value) => onPageSizeChange(Number(value))}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[80px] h-8">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
|
||||||
<SelectItem key={size} value={String(size)}>
|
|
||||||
{size}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
Page {pageIndex + 1} of {Math.max(1, pageCount)}
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onPageChange(pageIndex - 1)}
|
|
||||||
disabled={pageIndex === 0}
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onPageChange(pageIndex + 1)}
|
|
||||||
disabled={pageIndex + 1 >= pageCount}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user