mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-10 08:25:22 +02:00
Compare commits
296 Commits
v0.28.3
...
2702-add-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f1bf2b14e | ||
|
|
2683ac2a1b | ||
|
|
4e11334940 | ||
|
|
82893598e0 | ||
|
|
86905fc5bf | ||
|
|
c7814bb752 | ||
|
|
c0d6eac35d | ||
|
|
6dfa762934 | ||
|
|
0e3bc444b9 | ||
|
|
fb7b7cff66 | ||
|
|
5e999f1c3c | ||
|
|
9e52b722f0 | ||
|
|
70418dd09b | ||
|
|
df95766807 | ||
|
|
e5aae15310 | ||
|
|
964773b44c | ||
|
|
7224436610 | ||
|
|
d6885c32ea | ||
|
|
4da3c468eb | ||
|
|
38a711776b | ||
|
|
4030049ee8 | ||
|
|
06b18aca08 | ||
|
|
86ba597d67 | ||
|
|
5978c4135e | ||
|
|
e9202bfb15 | ||
|
|
365e055005 | ||
|
|
9b108480a8 | ||
|
|
450d591c1a | ||
|
|
d90722a174 | ||
|
|
f9de42610c | ||
|
|
780406f9ef | ||
|
|
f49988498f | ||
|
|
565bc16f24 | ||
|
|
c7b5e73d1c | ||
|
|
8053ee7724 | ||
|
|
c4aca74aef | ||
|
|
dab13a52d6 | ||
|
|
4a7e9a200e | ||
|
|
f83ab2923d | ||
|
|
9a1bee5287 | ||
|
|
6d17f62942 | ||
|
|
815b8136fa | ||
|
|
290a03ccfb | ||
|
|
63aa60f7e2 | ||
|
|
fe9b0ebcea | ||
|
|
8ccdb66ced | ||
|
|
e38f07d286 | ||
|
|
035d39e3b7 | ||
|
|
82a908a865 | ||
|
|
4bbb2ece49 | ||
|
|
8ee374dc6b | ||
|
|
ddfcd1a671 | ||
|
|
401b177a4e | ||
|
|
88b56ca0a2 | ||
|
|
3d48b25f71 | ||
|
|
b7e30d7ec3 | ||
|
|
b1ef5dc2c6 | ||
|
|
3846e41d7f | ||
|
|
ac76f2d97a | ||
|
|
d6056972f4 | ||
|
|
58b9a0d3d0 | ||
|
|
fe78f282f8 | ||
|
|
4941a80b50 | ||
|
|
5ea2ee5dcd | ||
|
|
76d6de5337 | ||
|
|
3374737db6 | ||
|
|
27a67af190 | ||
|
|
7e6a7d2cd4 | ||
|
|
4f5f1ad841 | ||
|
|
fe8d2732fc | ||
|
|
88ad551297 | ||
|
|
f36d011286 | ||
|
|
fb5ee5d6b3 | ||
|
|
3d50cb0ac9 | ||
|
|
c752cf3f9e | ||
|
|
cf25c17c20 | ||
|
|
ae439bcd13 | ||
|
|
b8f069704c | ||
|
|
d4bf6246c3 | ||
|
|
4b6f2c84ac | ||
|
|
116e9d85b7 | ||
|
|
dce1454d4d | ||
|
|
49d79fcd37 | ||
|
|
fa028dcf1e | ||
|
|
a09d7d5663 | ||
|
|
b9aa275759 | ||
|
|
b61ca31981 | ||
|
|
0b08fa9a59 | ||
|
|
ffd7b80410 | ||
|
|
3854dfaade | ||
|
|
bb56a0bae8 | ||
|
|
a03ec76b6f | ||
|
|
9cc8231188 | ||
|
|
ee2240898c | ||
|
|
6fb4a13a18 | ||
|
|
8a8688c011 | ||
|
|
bd18461242 | ||
|
|
7f60000641 | ||
|
|
1d7509dfc2 | ||
|
|
8304513501 | ||
|
|
2809cd690a | ||
|
|
fff91157c4 | ||
|
|
aca1c6f621 | ||
|
|
e9650de794 | ||
|
|
b3579d1321 | ||
|
|
43f9c114c8 | ||
|
|
bc11e8741b | ||
|
|
837373fdc5 | ||
|
|
7d2d7fc005 | ||
|
|
72c15ac18c | ||
|
|
51d744ba45 | ||
|
|
81ecf214f1 | ||
|
|
c2d37631ba | ||
|
|
7c55eba506 | ||
|
|
7878bf29ba | ||
|
|
1b70763ba5 | ||
|
|
e47263ae5f | ||
|
|
b139d6f277 | ||
|
|
cddb06f515 | ||
|
|
d0c92d84ef | ||
|
|
72974e00a6 | ||
|
|
d96e2bbeb7 | ||
|
|
a45d8ee8f4 | ||
|
|
9067452a38 | ||
|
|
1fa4d5b2ba | ||
|
|
bade36ea9d | ||
|
|
0c22041623 | ||
|
|
cccee05173 | ||
|
|
9f9c8fccf2 | ||
|
|
ad2e53a67a | ||
|
|
00f3853bd7 | ||
|
|
2880327e94 | ||
|
|
827b84f57e | ||
|
|
11aa8fe0c5 | ||
|
|
b9ac720d99 | ||
|
|
77b0ff7bbf | ||
|
|
e7af2c0ebd | ||
|
|
6a1bedb90f | ||
|
|
a2f142174b | ||
|
|
f4ce304a04 | ||
|
|
bb521f3e7e | ||
|
|
baaa470234 | ||
|
|
4871520dbb | ||
|
|
dad49ec96f | ||
|
|
ce4e37c75b | ||
|
|
c317ec39cb | ||
|
|
a4e9c6e890 | ||
|
|
72fb85f616 | ||
|
|
1e7a6f2071 | ||
|
|
5ffd664570 | ||
|
|
947100c041 | ||
|
|
5410a56638 | ||
|
|
8127dc4536 | ||
|
|
ee42a393aa | ||
|
|
2f37235aea | ||
|
|
290267bca4 | ||
|
|
8eace173b9 | ||
|
|
c9a9ed8164 | ||
|
|
30428053e8 | ||
|
|
1c0dbbcfd6 | ||
|
|
178f4fbdf7 | ||
|
|
2c07a4b2e3 | ||
|
|
75a797097b | ||
|
|
2879816e41 | ||
|
|
3501996b9e | ||
|
|
47556a6486 | ||
|
|
e554adc376 | ||
|
|
1804b935f6 | ||
|
|
985c9102da | ||
|
|
2e03cf3d48 | ||
|
|
33532d3cf7 | ||
|
|
a6999b1cf2 | ||
|
|
f5d18d6f9b | ||
|
|
e3ff7ef3e3 | ||
|
|
b84bc9b7c6 | ||
|
|
5e6e5ba9d8 | ||
|
|
de201d0b0a | ||
|
|
6866e2b63a | ||
|
|
3e4a1b92eb | ||
|
|
b9ca6ea9db | ||
|
|
f1d4543d5e | ||
|
|
d8c7c1eaf4 | ||
|
|
4330d7bd99 | ||
|
|
6e67864204 | ||
|
|
2102840bb9 | ||
|
|
1203d0589b | ||
|
|
30f061e774 | ||
|
|
c00aa6acbf | ||
|
|
8e9ab98a7a | ||
|
|
ce82e2322b | ||
|
|
ec7df05990 | ||
|
|
75a4e8e8ef | ||
|
|
b4319c7ea2 | ||
|
|
e9787b753d | ||
|
|
b419294b09 | ||
|
|
922b4d58f1 | ||
|
|
dc8ff78ee5 | ||
|
|
735c9952d8 | ||
|
|
21821295e3 | ||
|
|
a8467e80e8 | ||
|
|
95e14b4199 | ||
|
|
076262e479 | ||
|
|
653e5fa3a0 | ||
|
|
66931fe24f | ||
|
|
7feb4061f8 | ||
|
|
c4f4db3ebc | ||
|
|
4882bd25ad | ||
|
|
7a8f2e53d5 | ||
|
|
50182a8048 | ||
|
|
35d35028f6 | ||
|
|
a5a4a1a818 | ||
|
|
c106d13ab5 | ||
|
|
808001d8de | ||
|
|
ce24eadbb4 | ||
|
|
b87f8cc5d8 | ||
|
|
f650200771 | ||
|
|
f961dc6e7a | ||
|
|
4be25da185 | ||
|
|
675c1d7a7d | ||
|
|
28cc361c47 | ||
|
|
cedec5239f | ||
|
|
2f4cbbd3ac | ||
|
|
38b20450dc | ||
|
|
49f43ab3fb | ||
|
|
2eae756cec | ||
|
|
70c261d021 | ||
|
|
9ae2ebff46 | ||
|
|
8ce880d108 | ||
|
|
34304526b1 | ||
|
|
a16c4c1294 | ||
|
|
d1c4ac20e3 | ||
|
|
0195119a86 | ||
|
|
48a577e792 | ||
|
|
bf7a75dd9f | ||
|
|
d316aa4401 | ||
|
|
f1b2cc35b3 | ||
|
|
d2fabc998d | ||
|
|
7185047eb7 | ||
|
|
c75cfa2d69 | ||
|
|
7121fbe50a | ||
|
|
36cf3a69fc | ||
|
|
c34a01a173 | ||
|
|
9ac147a140 | ||
|
|
20f79ac655 | ||
|
|
1c5b92729a | ||
|
|
86feda1679 | ||
|
|
f95b29a450 | ||
|
|
a1cf5520a9 | ||
|
|
cbbf7f3a6d | ||
|
|
ebf5f486bc | ||
|
|
b1b1dbc1ce | ||
|
|
355d46948b | ||
|
|
938b0b4ed3 | ||
|
|
ebbbd39065 | ||
|
|
1f3936fcad | ||
|
|
e4d9fd37b9 | ||
|
|
0df6cc5395 | ||
|
|
2b4604dc0c | ||
|
|
1da9ef8e69 | ||
|
|
e049352f6d | ||
|
|
1cb1b5083f | ||
|
|
affd17d788 | ||
|
|
68f6d4a558 | ||
|
|
e575e50979 | ||
|
|
efedec70d6 | ||
|
|
8d11fb4ee8 | ||
|
|
b7f7027280 | ||
|
|
5d078f1d9f | ||
|
|
ac27aa1bba | ||
|
|
6a79ce8ff1 | ||
|
|
bf226f1af1 | ||
|
|
6b117551ae | ||
|
|
8c1153370c | ||
|
|
21fa21e9c0 | ||
|
|
970905198b | ||
|
|
a0c87358eb | ||
|
|
91a385c302 | ||
|
|
9627af9cda | ||
|
|
90bd276ad4 | ||
|
|
84d311802f | ||
|
|
8ee38a1463 | ||
|
|
e726bf31f6 | ||
|
|
f4248760a8 | ||
|
|
b715e21236 | ||
|
|
71d3a43fd7 | ||
|
|
02f0b0b1a4 | ||
|
|
2dffdffaf3 | ||
|
|
096235f8a1 | ||
|
|
c3b79c115d | ||
|
|
1fb8445165 | ||
|
|
53a11b81d6 | ||
|
|
307916a49a | ||
|
|
293160eb55 | ||
|
|
95999df13e | ||
|
|
803577a403 | ||
|
|
4b1f359cb6 |
1
.github/workflows/pr-quality.yml
vendored
1
.github/workflows/pr-quality.yml
vendored
@@ -16,7 +16,6 @@ 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,7 +99,14 @@ pnpm run dokploy:build
|
|||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
To build the docker image
|
To build the docker image first run commands to copy .env files
|
||||||
|
|
||||||
|
```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
|
||||||
|
|||||||
@@ -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, and Redis.
|
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, libsql, 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,4 +1,4 @@
|
|||||||
import { logger } from "./logger";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
const baseUrl = process.env.INNGEST_BASE_URL ?? "";
|
const baseUrl = process.env.INNGEST_BASE_URL ?? "";
|
||||||
const signingKey = process.env.INNGEST_SIGNING_KEY ?? "";
|
const signingKey = process.env.INNGEST_SIGNING_KEY ?? "";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
updateCompose,
|
updateCompose,
|
||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import type { DeployJob } from "./schema";
|
import type { DeployJob } from "./schema.js";
|
||||||
|
|
||||||
export const deploy = async (job: DeployJob) => {
|
export const deploy = async (job: DeployJob) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ describe("Host rule format regression tests", () => {
|
|||||||
previewDeploymentId: "",
|
previewDeploymentId: "",
|
||||||
internalPath: "/",
|
internalPath: "/",
|
||||||
stripPath: false,
|
stripPath: false,
|
||||||
|
customEntrypoint: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Host rule format validation", () => {
|
describe("Host rule format validation", () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ describe("createDomainLabels", () => {
|
|||||||
const baseDomain: Domain = {
|
const baseDomain: Domain = {
|
||||||
host: "example.com",
|
host: "example.com",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
|
customEntrypoint: null,
|
||||||
https: false,
|
https: false,
|
||||||
uniqueConfigKey: 1,
|
uniqueConfigKey: 1,
|
||||||
customCertResolver: null,
|
customCertResolver: null,
|
||||||
@@ -240,4 +241,134 @@ describe("createDomainLabels", () => {
|
|||||||
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create basic labels for custom entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{ ...baseDomain, customEntrypoint: "custom" },
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
expect(labels).toEqual([
|
||||||
|
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
|
||||||
|
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
|
||||||
|
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
|
||||||
|
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create https labels for custom entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
https: true,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
certificateType: "letsencrypt",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
expect(labels).toEqual([
|
||||||
|
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
|
||||||
|
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
|
||||||
|
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
|
||||||
|
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
|
||||||
|
"traefik.http.routers.test-app-1-custom.tls.certresolver=letsencrypt",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add stripPath middleware for custom entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
path: "/api",
|
||||||
|
stripPath: true,
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||||
|
);
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add internalPath middleware for custom entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
internalPath: "/hello",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-custom.middlewares=addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add path prefix in rule for custom entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
path: "/api",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`) && PathPrefix(`/api`)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should combine all middlewares for custom entrypoint", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
path: "/api",
|
||||||
|
stripPath: true,
|
||||||
|
internalPath: "/hello",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
|
||||||
|
);
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
|
||||||
|
);
|
||||||
|
expect(labels).toContain(
|
||||||
|
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not add redirect-to-https for custom entrypoint even with https", async () => {
|
||||||
|
const labels = await createDomainLabels(
|
||||||
|
appName,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
https: true,
|
||||||
|
certificateType: "letsencrypt",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
const middlewareLabel = labels.find((l) => l.includes(".middlewares="));
|
||||||
|
// Should not contain redirect-to-https since there's only one router
|
||||||
|
expect(middlewareLabel).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ networks:
|
|||||||
dokploy-network:
|
dokploy-network:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network", () => {
|
test("It shouldn't add suffix to dokploy-network", () => {
|
||||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ services:
|
|||||||
- dokploy-network
|
- dokploy-network
|
||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network in services", () => {
|
test("It shouldn't add suffix to dokploy-network in services", () => {
|
||||||
const composeData = parse(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
@@ -241,10 +241,10 @@ services:
|
|||||||
dokploy-network:
|
dokploy-network:
|
||||||
aliases:
|
aliases:
|
||||||
- apid
|
- apid
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
test("It shouldn't add suffix to dokploy-network in services multiples cases", () => {
|
||||||
const composeData = parse(composeFile8) as ComposeSpecification;
|
const composeData = parse(composeFile8) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getEnviromentVariablesObject } from "@dokploy/server/index";
|
import { getEnvironmentVariablesObject } from "@dokploy/server/index";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const projectEnv = `
|
const projectEnv = `
|
||||||
@@ -15,7 +15,7 @@ DATABASE_NAME=dev_database
|
|||||||
SECRET_KEY=env-secret-123
|
SECRET_KEY=env-secret-123
|
||||||
`;
|
`;
|
||||||
|
|
||||||
describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => {
|
describe("getEnvironmentVariablesObject with environment variables (Stack compose)", () => {
|
||||||
it("resolves environment variables correctly for Stack compose", () => {
|
it("resolves environment variables correctly for Stack compose", () => {
|
||||||
const serviceEnv = `
|
const serviceEnv = `
|
||||||
FOO=\${{environment.NODE_ENV}}
|
FOO=\${{environment.NODE_ENV}}
|
||||||
@@ -23,7 +23,7 @@ BAR=\${{environment.API_URL}}
|
|||||||
BAZ=test
|
BAZ=test
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnviromentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceEnv,
|
serviceEnv,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
@@ -45,7 +45,7 @@ DATABASE_URL=\${{project.DATABASE_URL}}
|
|||||||
SERVICE_PORT=4000
|
SERVICE_PORT=4000
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnviromentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceEnv,
|
serviceEnv,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
@@ -72,7 +72,7 @@ PASSWORD=secret123
|
|||||||
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv);
|
const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
|
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
|
||||||
@@ -85,7 +85,7 @@ UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv),
|
||||||
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ NODE_ENV=production
|
|||||||
API_URL=\${{environment.API_URL}}
|
API_URL=\${{environment.API_URL}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnviromentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceOverrideEnv,
|
serviceOverrideEnv,
|
||||||
"",
|
"",
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
@@ -115,7 +115,7 @@ SERVICE_NAME=my-service
|
|||||||
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnviromentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
complexServiceEnv,
|
complexServiceEnv,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
environmentEnv,
|
environmentEnv,
|
||||||
@@ -150,7 +150,7 @@ ENV_VAR=\${{environment.API_URL}}
|
|||||||
DB_NAME=\${{environment.DATABASE_NAME}}
|
DB_NAME=\${{environment.DATABASE_NAME}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnviromentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceWithConflicts,
|
serviceWithConflicts,
|
||||||
conflictingProjectEnv,
|
conflictingProjectEnv,
|
||||||
conflictingEnvironmentEnv,
|
conflictingEnvironmentEnv,
|
||||||
@@ -170,7 +170,7 @@ SERVICE_VAR=test
|
|||||||
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
PROJECT_VAR=\${{project.ENVIRONMENT}}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getEnviromentVariablesObject(
|
const result = getEnvironmentVariablesObject(
|
||||||
serviceWithEmpty,
|
serviceWithEmpty,
|
||||||
projectEnv,
|
projectEnv,
|
||||||
"",
|
"",
|
||||||
|
|||||||
144
apps/dokploy/__test__/permissions/check-permission.test.ts
Normal file
144
apps/dokploy/__test__/permissions/check-permission.test.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
enterpriseOnlyResources,
|
||||||
|
statements,
|
||||||
|
} from "@dokploy/server/lib/access-control";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
161
apps/dokploy/__test__/permissions/resolve-permissions.test.ts
Normal file
161
apps/dokploy/__test__/permissions/resolve-permissions.test.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
132
apps/dokploy/__test__/permissions/service-access.test.ts
Normal file
132
apps/dokploy/__test__/permissions/service-access.test.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
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,6 +48,20 @@ 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,
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ const baseDomain: Domain = {
|
|||||||
https: false,
|
https: false,
|
||||||
path: null,
|
path: null,
|
||||||
port: null,
|
port: null,
|
||||||
|
customEntrypoint: null,
|
||||||
serviceName: "",
|
serviceName: "",
|
||||||
composeId: "",
|
composeId: "",
|
||||||
customCertResolver: null,
|
customCertResolver: null,
|
||||||
@@ -276,6 +277,110 @@ test("CertificateType on websecure entrypoint", async () => {
|
|||||||
expect(router.tls?.certResolver).toBe("letsencrypt");
|
expect(router.tls?.certResolver).toBe("letsencrypt");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Custom entrypoint on http domain", async () => {
|
||||||
|
const router = await createRouterConfig(
|
||||||
|
baseApp,
|
||||||
|
{ ...baseDomain, https: false, customEntrypoint: "custom" },
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
|
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||||
|
expect(router.tls).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Custom entrypoint on https domain", async () => {
|
||||||
|
const router = await createRouterConfig(
|
||||||
|
baseApp,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
https: true,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
certificateType: "letsencrypt",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
|
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||||
|
expect(router.tls?.certResolver).toBe("letsencrypt");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Custom entrypoint with path includes PathPrefix in rule", async () => {
|
||||||
|
const router = await createRouterConfig(
|
||||||
|
baseApp,
|
||||||
|
{ ...baseDomain, customEntrypoint: "custom", path: "/api" },
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(router.rule).toContain("PathPrefix(`/api`)");
|
||||||
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Custom entrypoint with stripPath adds stripprefix middleware", async () => {
|
||||||
|
const router = await createRouterConfig(
|
||||||
|
baseApp,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
path: "/api",
|
||||||
|
stripPath: true,
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(router.middlewares).toContain("stripprefix--1");
|
||||||
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Custom entrypoint with internalPath adds addprefix middleware", async () => {
|
||||||
|
const router = await createRouterConfig(
|
||||||
|
baseApp,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
internalPath: "/hello",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(router.middlewares).toContain("addprefix--1");
|
||||||
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Custom entrypoint with https and custom cert resolver", async () => {
|
||||||
|
const router = await createRouterConfig(
|
||||||
|
baseApp,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
https: true,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
certificateType: "custom",
|
||||||
|
customCertResolver: "myresolver",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
|
expect(router.tls?.certResolver).toBe("myresolver");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Custom entrypoint without https should not have tls", async () => {
|
||||||
|
const router = await createRouterConfig(
|
||||||
|
baseApp,
|
||||||
|
{
|
||||||
|
...baseDomain,
|
||||||
|
https: false,
|
||||||
|
customEntrypoint: "custom",
|
||||||
|
certificateType: "letsencrypt",
|
||||||
|
},
|
||||||
|
"custom",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(router.entryPoints).toEqual(["custom"]);
|
||||||
|
expect(router.tls).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
/** IDN/Punycode */
|
/** IDN/Punycode */
|
||||||
|
|
||||||
test("Internationalized domain name is converted to punycode", async () => {
|
test("Internationalized domain name is converted to punycode", async () => {
|
||||||
|
|||||||
@@ -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: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "application"
|
||||||
|
| "libsql"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "postgres"
|
||||||
|
| "redis";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddSwarmSettings = ({ id, type }: Props) => {
|
export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||||
|
|||||||
@@ -37,27 +37,27 @@ import { AddSwarmSettings } from "./modify-swarm-settings";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectSchema = z.object({
|
||||||
replicas: z.number().min(1, "Replicas must be at least 1"),
|
replicas: z.number().min(1, "Replicas must be at least 1"),
|
||||||
registryId: z.string().optional(),
|
registryId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddCommand = z.infer<typeof AddRedirectchema>;
|
type AddCommand = z.infer<typeof AddRedirectSchema>;
|
||||||
|
|
||||||
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,12 +65,13 @@ 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]
|
||||||
@@ -86,7 +87,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
|
|||||||
: {}),
|
: {}),
|
||||||
replicas: data?.replicas || 1,
|
replicas: data?.replicas || 1,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddRedirectchema),
|
resolver: zodResolver(AddRedirectSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -105,11 +106,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 || "",
|
||||||
postgresId: id || "",
|
|
||||||
redisId: id || "",
|
|
||||||
mysqlId: id || "",
|
|
||||||
mariadbId: id || "",
|
mariadbId: id || "",
|
||||||
mongoId: id || "",
|
mongoId: id || "",
|
||||||
|
mysqlId: id || "",
|
||||||
|
postgresId: id || "",
|
||||||
|
redisId: id || "",
|
||||||
...(type === "application"
|
...(type === "application"
|
||||||
? {
|
? {
|
||||||
registryId:
|
registryId:
|
||||||
|
|||||||
@@ -28,7 +28,14 @@ export const endpointSpecFormSchema = z.object({
|
|||||||
|
|
||||||
interface EndpointSpecFormProps {
|
interface EndpointSpecFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||||
@@ -44,6 +51,7 @@ 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]()
|
||||||
@@ -56,6 +64,7 @@ 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]
|
||||||
@@ -94,6 +103,7 @@ 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,7 +26,14 @@ export const healthCheckFormSchema = z.object({
|
|||||||
|
|
||||||
interface HealthCheckFormProps {
|
interface HealthCheckFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||||
@@ -42,6 +49,7 @@ 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]()
|
||||||
@@ -54,6 +62,7 @@ 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]
|
||||||
@@ -104,6 +113,7 @@ 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,7 +29,14 @@ export const labelsFormSchema = z.object({
|
|||||||
|
|
||||||
interface LabelsFormProps {
|
interface LabelsFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||||
@@ -45,6 +52,7 @@ 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]()
|
||||||
@@ -57,6 +65,7 @@ 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]
|
||||||
@@ -112,6 +121,7 @@ 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,7 +23,14 @@ import { api } from "@/utils/api";
|
|||||||
|
|
||||||
interface ModeFormProps {
|
interface ModeFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModeForm = ({ id, type }: ModeFormProps) => {
|
export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||||
@@ -39,6 +46,7 @@ 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]()
|
||||||
@@ -51,6 +59,7 @@ 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]
|
||||||
@@ -95,6 +104,7 @@ 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");
|
||||||
@@ -122,6 +132,7 @@ 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,7 +35,14 @@ export const networkFormSchema = z.object({
|
|||||||
|
|
||||||
interface NetworkFormProps {
|
interface NetworkFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||||
@@ -51,6 +58,7 @@ 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]()
|
||||||
@@ -63,6 +71,7 @@ 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]
|
||||||
@@ -132,6 +141,7 @@ 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,7 +34,14 @@ export const placementFormSchema = z.object({
|
|||||||
|
|
||||||
interface PlacementFormProps {
|
interface PlacementFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||||
@@ -50,6 +57,7 @@ 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]()
|
||||||
@@ -62,6 +70,7 @@ 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]
|
||||||
@@ -114,6 +123,7 @@ 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,7 +32,14 @@ export const restartPolicyFormSchema = z.object({
|
|||||||
|
|
||||||
interface RestartPolicyFormProps {
|
interface RestartPolicyFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||||
@@ -48,6 +55,7 @@ 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]()
|
||||||
@@ -60,6 +68,7 @@ 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]
|
||||||
@@ -104,6 +113,7 @@ 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,7 +34,14 @@ export const rollbackConfigFormSchema = z.object({
|
|||||||
|
|
||||||
interface RollbackConfigFormProps {
|
interface RollbackConfigFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||||
@@ -50,6 +57,7 @@ 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]()
|
||||||
@@ -62,6 +70,7 @@ 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]
|
||||||
@@ -103,6 +112,7 @@ 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,7 +23,14 @@ const hasStopGracePeriodSwarm = (
|
|||||||
|
|
||||||
interface StopGracePeriodFormProps {
|
interface StopGracePeriodFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||||
@@ -39,6 +46,7 @@ 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]()
|
||||||
@@ -51,6 +59,7 @@ 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]
|
||||||
@@ -88,6 +97,7 @@ 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,7 +34,14 @@ export const updateConfigFormSchema = z.object({
|
|||||||
|
|
||||||
interface UpdateConfigFormProps {
|
interface UpdateConfigFormProps {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type:
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "redis"
|
||||||
|
| "application"
|
||||||
|
| "libsql";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||||
@@ -50,6 +57,7 @@ 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]()
|
||||||
@@ -62,6 +70,7 @@ 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]
|
||||||
@@ -109,6 +118,7 @@ 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectSchema = z.object({
|
||||||
regex: z.string().min(1, "Regex required"),
|
regex: z.string().min(1, "Regex required"),
|
||||||
permanent: z.boolean().default(false),
|
permanent: z.boolean().default(false),
|
||||||
replacement: z.string().min(1, "Replacement required"),
|
replacement: z.string().min(1, "Replacement required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddRedirect = z.infer<typeof AddRedirectchema>;
|
type AddRedirect = z.infer<typeof AddRedirectSchema>;
|
||||||
|
|
||||||
// Default presets
|
// Default presets
|
||||||
const redirectPresets = [
|
const redirectPresets = [
|
||||||
@@ -110,7 +110,7 @@ export const HandleRedirect = ({
|
|||||||
regex: "",
|
regex: "",
|
||||||
replacement: "",
|
replacement: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(AddRedirectchema),
|
resolver: zodResolver(AddRedirectSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -149,7 +149,7 @@ export const HandleRedirect = ({
|
|||||||
|
|
||||||
const onDialogToggle = (open: boolean) => {
|
const onDialogToggle = (open: boolean) => {
|
||||||
setIsOpen(open);
|
setIsOpen(open);
|
||||||
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
|
// commented for the moment because not resetting the form if accidentally closed the dialog can be considered as a feature instead of a bug
|
||||||
// setPresetSelected("");
|
// setPresetSelected("");
|
||||||
// form.reset();
|
// form.reset();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,12 +89,13 @@ const ULIMIT_PRESETS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export type ServiceType =
|
export type ServiceType =
|
||||||
| "postgres"
|
| "application"
|
||||||
| "mongo"
|
| "libsql"
|
||||||
| "redis"
|
|
||||||
| "mysql"
|
|
||||||
| "mariadb"
|
| "mariadb"
|
||||||
| "application";
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "postgres"
|
||||||
|
| "redis";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -105,27 +106,29 @@ 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]
|
||||||
@@ -155,19 +158,20 @@ 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?.ulimitsSwarm || [],
|
ulimitsSwarm: (data as any)?.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,13 +15,17 @@ 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 },
|
{ enabled: !!applicationId && canRead },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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,6 +60,8 @@ 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(
|
||||||
@@ -125,9 +127,11 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
{canWrite && (
|
||||||
<Button isLoading={isPending}>Modify</Button>
|
<DialogTrigger asChild>
|
||||||
</DialogTrigger>
|
<Button isLoading={isPending}>Modify</Button>
|
||||||
|
</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"
|
||||||
| "postgres"
|
| "compose"
|
||||||
| "redis"
|
| "libsql"
|
||||||
| "mongo"
|
|
||||||
| "redis"
|
|
||||||
| "mysql"
|
|
||||||
| "mariadb"
|
| "mariadb"
|
||||||
| "compose";
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "postgres"
|
||||||
|
| "redis";
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,24 +21,33 @@ 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">
|
||||||
@@ -50,7 +59,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
{canCreate && 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>
|
||||||
@@ -63,9 +72,11 @@ 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>
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
{canCreate && (
|
||||||
Add Volume
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
</AddVolumes>
|
Add Volume
|
||||||
|
</AddVolumes>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
@@ -130,38 +141,42 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateVolume
|
{canCreate && (
|
||||||
mountId={mount.mountId}
|
<UpdateVolume
|
||||||
type={mount.type}
|
mountId={mount.mountId}
|
||||||
refetch={refetch}
|
type={mount.type}
|
||||||
serviceType={type}
|
refetch={refetch}
|
||||||
/>
|
serviceType={type}
|
||||||
<DialogAction
|
/>
|
||||||
title="Delete Volume"
|
)}
|
||||||
description="Are you sure you want to delete this volume?"
|
{canDelete && (
|
||||||
type="destructive"
|
<DialogAction
|
||||||
onClick={async () => {
|
title="Delete Volume"
|
||||||
await deleteVolume({
|
description="Are you sure you want to delete this volume?"
|
||||||
mountId: mount.mountId,
|
type="destructive"
|
||||||
})
|
onClick={async () => {
|
||||||
.then(() => {
|
await deleteVolume({
|
||||||
refetch();
|
mountId: mount.mountId,
|
||||||
toast.success("Volume deleted successfully");
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting volume");
|
refetch();
|
||||||
});
|
toast.success("Volume deleted successfully");
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error deleting volume");
|
||||||
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
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,13 +67,13 @@ interface Props {
|
|||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
serviceType:
|
serviceType:
|
||||||
| "application"
|
| "application"
|
||||||
| "postgres"
|
| "compose"
|
||||||
| "redis"
|
| "libsql"
|
||||||
| "mongo"
|
|
||||||
| "redis"
|
|
||||||
| "mysql"
|
|
||||||
| "mariadb"
|
| "mariadb"
|
||||||
| "compose";
|
| "mongo"
|
||||||
|
| "mysql"
|
||||||
|
| "postgres"
|
||||||
|
| "redis";
|
||||||
}
|
}
|
||||||
|
|
||||||
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="max-w-full max-w-[45rem]">
|
<FormItem className="w-full max-w-[45rem]">
|
||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import copy from "copy-to-clipboard";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Clock,
|
Clock,
|
||||||
|
Copy,
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
@@ -97,6 +99,12 @@ 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 => {
|
||||||
@@ -224,11 +232,27 @@ 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">
|
||||||
<span className="break-all text-muted-foreground">
|
<Badge
|
||||||
{`${url}/api/deploy${
|
role="button"
|
||||||
type === "compose" ? "/compose" : ""
|
tabIndex={0}
|
||||||
}/${refreshToken}`}
|
aria-label="Copy webhook URL to clipboard"
|
||||||
</span>
|
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"
|
||||||
|
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} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ export const domain = z
|
|||||||
.min(1, { message: "Port must be at least 1" })
|
.min(1, { message: "Port must be at least 1" })
|
||||||
.max(65535, { message: "Port must be 65535 or below" })
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
.optional(),
|
.optional(),
|
||||||
|
useCustomEntrypoint: z.boolean(),
|
||||||
|
customEntrypoint: z.string().optional(),
|
||||||
https: z.boolean().optional(),
|
https: z.boolean().optional(),
|
||||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
customCertResolver: z.string().optional(),
|
customCertResolver: z.string().optional(),
|
||||||
@@ -114,6 +116,14 @@ export const domain = z
|
|||||||
message: "Internal path must start with '/'",
|
message: "Internal path must start with '/'",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.useCustomEntrypoint && !input.customEntrypoint) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["customEntrypoint"],
|
||||||
|
message: "Custom entry point must be specified",
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
@@ -196,6 +206,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
internalPath: undefined,
|
internalPath: undefined,
|
||||||
stripPath: false,
|
stripPath: false,
|
||||||
port: undefined,
|
port: undefined,
|
||||||
|
useCustomEntrypoint: false,
|
||||||
|
customEntrypoint: undefined,
|
||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
customCertResolver: undefined,
|
customCertResolver: undefined,
|
||||||
@@ -206,6 +218,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const certificateType = form.watch("certificateType");
|
const certificateType = form.watch("certificateType");
|
||||||
|
const useCustomEntrypoint = form.watch("useCustomEntrypoint");
|
||||||
const https = form.watch("https");
|
const https = form.watch("https");
|
||||||
const domainType = form.watch("domainType");
|
const domainType = form.watch("domainType");
|
||||||
const host = form.watch("host");
|
const host = form.watch("host");
|
||||||
@@ -220,6 +233,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
internalPath: data?.internalPath || undefined,
|
internalPath: data?.internalPath || undefined,
|
||||||
stripPath: data?.stripPath || false,
|
stripPath: data?.stripPath || false,
|
||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
|
useCustomEntrypoint: !!data.customEntrypoint,
|
||||||
|
customEntrypoint: data.customEntrypoint || undefined,
|
||||||
certificateType: data?.certificateType || undefined,
|
certificateType: data?.certificateType || undefined,
|
||||||
customCertResolver: data?.customCertResolver || undefined,
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
serviceName: data?.serviceName || undefined,
|
serviceName: data?.serviceName || undefined,
|
||||||
@@ -234,6 +249,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
internalPath: undefined,
|
internalPath: undefined,
|
||||||
stripPath: false,
|
stripPath: false,
|
||||||
port: undefined,
|
port: undefined,
|
||||||
|
useCustomEntrypoint: false,
|
||||||
|
customEntrypoint: undefined,
|
||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
customCertResolver: undefined,
|
customCertResolver: undefined,
|
||||||
@@ -635,6 +652,50 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="useCustomEntrypoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Custom Entrypoint</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Use custom entrypoint for domina
|
||||||
|
<br />
|
||||||
|
"web" and/or "websecure" is used by default.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{useCustomEntrypoint && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customEntrypoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Entrypoint Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter entrypoint name manually"
|
||||||
|
{...field}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="https"
|
name="https"
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ 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(
|
||||||
@@ -149,7 +152,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">
|
||||||
{data && data?.length > 0 && (
|
{canCreateDomain && 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
|
||||||
@@ -173,13 +176,15 @@ 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>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
{canCreateDomain && (
|
||||||
<AddDomain id={id} type={type}>
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<Button>
|
<AddDomain id={id} type={type}>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<Button>
|
||||||
</Button>
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</AddDomain>
|
</Button>
|
||||||
</div>
|
</AddDomain>
|
||||||
|
</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] ">
|
||||||
@@ -214,47 +219,51 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddDomain
|
{canCreateDomain && (
|
||||||
id={id}
|
<AddDomain
|
||||||
type={type}
|
id={id}
|
||||||
domainId={item.domainId}
|
type={type}
|
||||||
>
|
domainId={item.domainId}
|
||||||
<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
|
||||||
</Button>
|
variant="ghost"
|
||||||
</AddDomain>
|
size="icon"
|
||||||
<DialogAction
|
className="group hover:bg-blue-500/10"
|
||||||
title="Delete Domain"
|
>
|
||||||
description="Are you sure you want to delete this domain?"
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
type="destructive"
|
</Button>
|
||||||
onClick={async () => {
|
</AddDomain>
|
||||||
await deleteDomain({
|
)}
|
||||||
domainId: item.domainId,
|
{canDeleteDomain && (
|
||||||
})
|
<DialogAction
|
||||||
.then((_data) => {
|
title="Delete Domain"
|
||||||
refetch();
|
description="Are you sure you want to delete this domain?"
|
||||||
toast.success(
|
type="destructive"
|
||||||
"Domain deleted successfully",
|
onClick={async () => {
|
||||||
);
|
await deleteDomain({
|
||||||
|
domainId: item.domainId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then((_data) => {
|
||||||
toast.error("Error deleting domain");
|
refetch();
|
||||||
});
|
toast.success(
|
||||||
}}
|
"Domain deleted successfully",
|
||||||
>
|
);
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error("Error deleting domain");
|
||||||
className="group hover:bg-red-500/10"
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
||||||
<div className="w-full break-all">
|
<div className="w-full break-all">
|
||||||
|
|||||||
@@ -36,16 +36,19 @@ 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 = {
|
||||||
postgres: () =>
|
compose: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
libsql: () => api.libsql.one.useQuery({ libsqlId: 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: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||||
compose: () =>
|
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
postgres: () =>
|
||||||
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
|
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data, refetch } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
@@ -53,12 +56,13 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||||
|
|
||||||
const mutationMap = {
|
const mutationMap = {
|
||||||
postgres: () => api.postgres.update.useMutation(),
|
compose: () => api.compose.update.useMutation(),
|
||||||
redis: () => api.redis.update.useMutation(),
|
libsql: () => api.libsql.update.useMutation(),
|
||||||
mysql: () => api.mysql.update.useMutation(),
|
|
||||||
mariadb: () => api.mariadb.update.useMutation(),
|
mariadb: () => api.mariadb.update.useMutation(),
|
||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
compose: () => api.compose.update.useMutation(),
|
mysql: () => api.mysql.update.useMutation(),
|
||||||
|
postgres: () => api.postgres.update.useMutation(),
|
||||||
|
redis: () => api.redis.update.useMutation(),
|
||||||
};
|
};
|
||||||
const { mutateAsync, isPending } = mutationMap[type]
|
const { mutateAsync, isPending } = mutationMap[type]
|
||||||
? mutationMap[type]()
|
? mutationMap[type]()
|
||||||
@@ -85,12 +89,13 @@ 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 () => {
|
||||||
@@ -185,25 +190,27 @@ PORT=3000
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{canWrite && (
|
||||||
{hasChanges && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
|
{hasChanges && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
isLoading={isPending}
|
||||||
variant="outline"
|
className="w-fit"
|
||||||
onClick={handleCancel}
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
>
|
>
|
||||||
Cancel
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
<Button
|
)}
|
||||||
isLoading={isPending}
|
|
||||||
className="w-fit"
|
|
||||||
type="submit"
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -31,6 +31,8 @@ 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();
|
||||||
|
|
||||||
@@ -201,27 +203,30 @@ 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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{canWrite && (
|
||||||
{hasChanges && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
{hasChanges && (
|
||||||
Cancel
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
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, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, 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,10 +416,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</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 { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { HelpCircle, 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,10 +228,8 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ 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,
|
||||||
@@ -57,128 +60,135 @@ 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}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Application"
|
<DialogAction
|
||||||
description="Are you sure you want to deploy this application?"
|
title="Deploy Application"
|
||||||
type="default"
|
description="Are you sure you want to deploy this application?"
|
||||||
onClick={async () => {
|
type="default"
|
||||||
await deploy({
|
onClick={async () => {
|
||||||
applicationId: applicationId,
|
await deploy({
|
||||||
})
|
applicationId: applicationId,
|
||||||
.then(() => {
|
|
||||||
toast.success("Application deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deploying application");
|
toast.success("Application deployed successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
router.push(
|
||||||
>
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||||
<Button
|
);
|
||||||
variant="default"
|
})
|
||||||
isLoading={data?.applicationStatus === "running"}
|
.catch(() => {
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
toast.error("Error deploying application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="default"
|
||||||
<div className="flex items-center">
|
isLoading={data?.applicationStatus === "running"}
|
||||||
<Rocket className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Deploy
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<Rocket className="size-4 mr-1" />
|
||||||
<p>
|
Deploy
|
||||||
Downloads the source code and performs a complete build
|
</div>
|
||||||
</p>
|
</TooltipTrigger>
|
||||||
</TooltipContent>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</Tooltip>
|
<p>
|
||||||
</Button>
|
Downloads the source code and performs a complete
|
||||||
</DialogAction>
|
build
|
||||||
<DialogAction
|
</p>
|
||||||
title="Reload Application"
|
</TooltipContent>
|
||||||
description="Are you sure you want to reload this application?"
|
</TooltipPrimitive.Portal>
|
||||||
type="default"
|
</Tooltip>
|
||||||
onClick={async () => {
|
</Button>
|
||||||
await reload({
|
</DialogAction>
|
||||||
applicationId: applicationId,
|
)}
|
||||||
appName: data?.appName || "",
|
{canDeploy && (
|
||||||
})
|
<DialogAction
|
||||||
.then(() => {
|
title="Reload Application"
|
||||||
toast.success("Application reloaded successfully");
|
description="Are you sure you want to reload this application?"
|
||||||
refetch();
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
applicationId: applicationId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error reloading application");
|
toast.success("Application reloaded successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error reloading application");
|
||||||
variant="secondary"
|
});
|
||||||
isLoading={isReloading}
|
}}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="secondary"
|
||||||
<div className="flex items-center">
|
isLoading={isReloading}
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Reload
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<p>Reload the application without rebuilding it</p>
|
Reload
|
||||||
</TooltipContent>
|
</div>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipPrimitive.Portal>
|
||||||
</Button>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</DialogAction>
|
<p>Reload the application without rebuilding it</p>
|
||||||
<DialogAction
|
</TooltipContent>
|
||||||
title="Rebuild Application"
|
</TooltipPrimitive.Portal>
|
||||||
description="Are you sure you want to rebuild this application?"
|
</Tooltip>
|
||||||
type="default"
|
</Button>
|
||||||
onClick={async () => {
|
</DialogAction>
|
||||||
await redeploy({
|
)}
|
||||||
applicationId: applicationId,
|
{canDeploy && (
|
||||||
})
|
<DialogAction
|
||||||
.then(() => {
|
title="Rebuild Application"
|
||||||
toast.success("Application rebuilt successfully");
|
description="Are you sure you want to rebuild this application?"
|
||||||
refetch();
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
applicationId: applicationId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error rebuilding application");
|
toast.success("Application rebuilt successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error rebuilding application");
|
||||||
variant="secondary"
|
});
|
||||||
isLoading={data?.applicationStatus === "running"}
|
}}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="secondary"
|
||||||
<div className="flex items-center">
|
isLoading={data?.applicationStatus === "running"}
|
||||||
<Hammer className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Rebuild
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<Hammer className="size-4 mr-1" />
|
||||||
<p>
|
Rebuild
|
||||||
Only rebuilds the application without downloading new
|
</div>
|
||||||
code
|
</TooltipTrigger>
|
||||||
</p>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipPrimitive.Portal>
|
<p>
|
||||||
</Tooltip>
|
Only rebuilds the application without downloading new
|
||||||
</Button>
|
code
|
||||||
</DialogAction>
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
{canDeploy && 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?"
|
||||||
@@ -219,7 +229,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?"
|
||||||
@@ -256,7 +266,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
) : null}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -270,49 +280,53 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
aria-label="Toggle autodeploy"
|
<Switch
|
||||||
checked={data?.autoDeploy || false}
|
aria-label="Toggle autodeploy"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.autoDeploy || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
applicationId,
|
await update({
|
||||||
autoDeploy: enabled,
|
applicationId,
|
||||||
})
|
autoDeploy: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Auto Deploy Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.success("Auto Deploy Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Auto Deploy");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Clean Cache</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Clean Cache</span>
|
||||||
aria-label="Toggle clean cache"
|
<Switch
|
||||||
checked={data?.cleanCache || false}
|
aria-label="Toggle clean cache"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.cleanCache || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
applicationId,
|
await update({
|
||||||
cleanCache: enabled,
|
applicationId,
|
||||||
})
|
cleanCache: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Clean Cache Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Clean Cache");
|
toast.success("Clean Cache Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
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} />
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
}, [option, services, containers]);
|
}, [option, services, containers]);
|
||||||
|
|
||||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||||
const containersLenght =
|
const containersLength =
|
||||||
option === "native" ? containers?.length : services?.length;
|
option === "native" ? containers?.length : services?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,7 +167,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
<SelectLabel>Containers ({containersLength})</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./show-patches";
|
|
||||||
export * from "./patch-editor";
|
export * from "./patch-editor";
|
||||||
|
export * from "./show-patches";
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ 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"),
|
||||||
@@ -482,7 +483,7 @@ export const HandleVolumeBackups = ({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Choose the volume to backup, if you dont see the
|
Choose the volume to backup. If you do not see the
|
||||||
volume here, you can type the volume name manually
|
volume here, you can type the volume name manually
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -517,7 +518,7 @@ export const HandleVolumeBackups = ({
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Choose the volume to backup, if you dont see the volume
|
Choose the volume to backup. If you do not see the volume
|
||||||
here, you can type the volume name manually
|
here, you can type the volume name manually
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ 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 = {
|
||||||
@@ -55,6 +57,7 @@ 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 }),
|
||||||
@@ -70,6 +73,7 @@ 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(),
|
||||||
@@ -96,6 +100,7 @@ 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,
|
||||||
@@ -123,6 +128,8 @@ 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,6 +19,9 @@ 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,
|
||||||
@@ -35,162 +38,169 @@ 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}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
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="Start Compose"
|
title="Deploy Compose"
|
||||||
description="Are you sure you want to start this compose?"
|
description="Are you sure you want to deploy this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await deploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose started successfully");
|
toast.success("Compose deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting compose");
|
toast.error("Error deploying compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.composeStatus === "running"}
|
||||||
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">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the compose (requires a previous successful build)
|
Downloads the source code and performs a complete build
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
)}
|
||||||
|
{canDeploy && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Compose"
|
title="Reload Compose"
|
||||||
description="Are you sure you want to stop this compose?"
|
description="Are you sure you want to reload this compose?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose stopped successfully");
|
toast.success("Compose reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping compose");
|
toast.error("Error reloading compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="secondary"
|
||||||
isLoading={isStopping}
|
isLoading={data?.composeStatus === "running"}
|
||||||
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">
|
||||||
<Ban className="size-4 mr-1" />
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
Stop
|
Reload
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running compose</p>
|
<p>Reload the compose without rebuilding it</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 || ""}
|
||||||
@@ -205,27 +215,29 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
aria-label="Toggle autodeploy"
|
<Switch
|
||||||
checked={data?.autoDeploy || false}
|
aria-label="Toggle autodeploy"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.autoDeploy || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
composeId,
|
await update({
|
||||||
autoDeploy: enabled,
|
composeId,
|
||||||
})
|
autoDeploy: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Auto Deploy Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.success("Auto Deploy Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Auto Deploy");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ 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(
|
||||||
{
|
{
|
||||||
@@ -164,14 +166,16 @@ 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" />
|
||||||
<Button
|
{canUpdate && (
|
||||||
type="submit"
|
<Button
|
||||||
form="hook-form-save-compose-file"
|
type="submit"
|
||||||
isLoading={isPending}
|
form="hook-form-save-compose-file"
|
||||||
className="lg:w-fit w-full"
|
isLoading={isPending}
|
||||||
>
|
className="lg:w-fit w-full"
|
||||||
Save
|
>
|
||||||
</Button>
|
Save
|
||||||
|
</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 { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { HelpCircle, 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,10 +230,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
|||||||
}, [option, services, containers]);
|
}, [option, services, containers]);
|
||||||
|
|
||||||
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||||
const containersLenght =
|
const containersLength =
|
||||||
option === "native" ? containers?.length : services?.length;
|
option === "native" ? containers?.length : services?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -152,7 +152,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
<SelectLabel>Containers ({containersLength})</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -65,7 +65,13 @@ import { ScheduleFormField } from "../../application/schedules/handle-schedules"
|
|||||||
|
|
||||||
type CacheType = "cache" | "fetch";
|
type CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
type DatabaseType =
|
||||||
|
| "postgres"
|
||||||
|
| "mariadb"
|
||||||
|
| "mysql"
|
||||||
|
| "mongo"
|
||||||
|
| "web-server"
|
||||||
|
| "libsql";
|
||||||
|
|
||||||
const Schema = z
|
const Schema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -77,7 +83,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"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]),
|
backupType: z.enum(["database", "compose"]),
|
||||||
metadata: z
|
metadata: z
|
||||||
@@ -209,7 +215,12 @@ export const HandleBackup = ({
|
|||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
database: databaseType === "web-server" ? "dokploy" : "",
|
database:
|
||||||
|
databaseType === "web-server"
|
||||||
|
? "dokploy"
|
||||||
|
: databaseType === "libsql"
|
||||||
|
? "iku.db"
|
||||||
|
: "",
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
prefix: "/",
|
prefix: "/",
|
||||||
@@ -246,7 +257,9 @@ 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 ?? "/",
|
||||||
@@ -281,11 +294,15 @@ export const HandleBackup = ({
|
|||||||
? {
|
? {
|
||||||
mongoId: id,
|
mongoId: id,
|
||||||
}
|
}
|
||||||
: databaseType === "web-server"
|
: databaseType === "libsql"
|
||||||
? {
|
? {
|
||||||
userId: id,
|
libsqlId: id,
|
||||||
}
|
}
|
||||||
: undefined;
|
: databaseType === "web-server"
|
||||||
|
? {
|
||||||
|
userId: id,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
await createBackup({
|
await createBackup({
|
||||||
destinationId: data.destinationId,
|
destinationId: data.destinationId,
|
||||||
@@ -568,7 +585,10 @@ export const HandleBackup = ({
|
|||||||
<FormLabel>Database</FormLabel>
|
<FormLabel>Database</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
disabled={databaseType === "web-server"}
|
disabled={
|
||||||
|
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"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]).default("database"),
|
backupType: z.enum(["database", "compose"]).default("database"),
|
||||||
metadata: z
|
metadata: z
|
||||||
@@ -211,7 +211,12 @@ export const RestoreBackup = ({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
destinationId: "",
|
destinationId: "",
|
||||||
backupFile: "",
|
backupFile: "",
|
||||||
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
databaseName:
|
||||||
|
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,
|
||||||
@@ -220,7 +225,7 @@ export const RestoreBackup = ({
|
|||||||
resolver: zodResolver(RestoreBackupSchema),
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const destionationId = form.watch("destinationId");
|
const destinationId = form.watch("destinationId");
|
||||||
const currentDatabaseType = form.watch("databaseType");
|
const currentDatabaseType = form.watch("databaseType");
|
||||||
const metadata = form.watch("metadata");
|
const metadata = form.watch("metadata");
|
||||||
|
|
||||||
@@ -235,12 +240,12 @@ export const RestoreBackup = ({
|
|||||||
|
|
||||||
const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
|
const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
|
||||||
{
|
{
|
||||||
destinationId: destionationId,
|
destinationId: destinationId,
|
||||||
search: debouncedSearchTerm,
|
search: debouncedSearchTerm,
|
||||||
serverId: serverId ?? "",
|
serverId: serverId ?? "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isOpen && !!destionationId,
|
enabled: isOpen && !!destinationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -523,7 +528,10 @@ export const RestoreBackup = ({
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Enter database name"
|
placeholder="Enter database name"
|
||||||
{...field}
|
{...field}
|
||||||
disabled={databaseType === "web-server"}
|
disabled={
|
||||||
|
databaseType === "web-server" ||
|
||||||
|
databaseType === "libsql"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -53,14 +53,16 @@ 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(),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -77,10 +79,11 @@ 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,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { inferRouterOutputs } from "@trpc/server";
|
import type { inferRouterOutputs } from "@trpc/server";
|
||||||
import Link from "next/link";
|
|
||||||
import { ArrowRight, ListTodo, Loader2, XCircle } from "lucide-react";
|
import { ArrowRight, ListTodo, Loader2, XCircle } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
|||||||
>
|
>
|
||||||
{" "}
|
{" "}
|
||||||
<div className="flex items-start gap-x-2">
|
<div className="flex items-start gap-x-2">
|
||||||
{/* Icon to expand the log item maybe implement a colapsible later */}
|
{/* Icon to expand the log item maybe implement a collapsible later */}
|
||||||
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
||||||
{tooltip(color, rawTimestamp)}
|
{tooltip(color, rawTimestamp)}
|
||||||
{!noTimestamp && (
|
{!noTimestamp && (
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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,6 +10,7 @@ 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";
|
||||||
|
|
||||||
@@ -127,6 +128,10 @@ export const columns: ColumnDef<Container>[] = [
|
|||||||
>
|
>
|
||||||
Terminal
|
Terminal
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
|
<RemoveContainerDialog
|
||||||
|
containerId={container.containerId}
|
||||||
|
serverId={container.serverId ?? undefined}
|
||||||
|
/>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { api, type RouterOutputs } from "@/utils/api";
|
import { api, type RouterOutputs } from "@/utils/api";
|
||||||
import { columns } from "./colums";
|
import { columns } from "./columns";
|
||||||
export type Container = NonNullable<
|
export type Container = NonNullable<
|
||||||
RouterOutputs["docker"]["getContainers"]
|
RouterOutputs["docker"]["getContainers"]
|
||||||
>[0];
|
>[0];
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ 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);
|
||||||
@@ -180,7 +182,10 @@ 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 className="w-10 h-10" />
|
<Logo
|
||||||
|
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}>
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
163
apps/dokploy/components/dashboard/libsql/update-libsql.tsx
Normal file
163
apps/dokploy/components/dashboard/libsql/update-libsql.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
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,6 +21,8 @@ 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,
|
||||||
@@ -72,154 +74,33 @@ 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">
|
||||||
<TooltipProvider delayDuration={0}>
|
{canDeploy && (
|
||||||
<DialogAction
|
|
||||||
title="Deploy Mariadb"
|
|
||||||
description="Are you sure you want to deploy this mariadb?"
|
|
||||||
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 MariaDB database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mariadb"
|
|
||||||
description="Are you sure you want to reload this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
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="Start Mariadb"
|
title="Deploy Mariadb"
|
||||||
description="Are you sure you want to start this mariadb?"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mariadbId: mariadbId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
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">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MariaDB database</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>
|
||||||
@@ -227,6 +108,132 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mariadb"
|
||||||
|
description="Are you sure you want to reload this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{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,6 +21,8 @@ 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,
|
||||||
@@ -73,153 +75,158 @@ 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}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Mongo"
|
|
||||||
description="Are you sure you want to deploy this mongo?"
|
|
||||||
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 MongoDB database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mongo"
|
|
||||||
description="Are you sure you want to reload this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
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="Start Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to start this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mongoId: mongoId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mongo");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
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">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MongoDB database</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 && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mongo"
|
||||||
|
description="Are you sure you want to reload this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{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 || ""}
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["block"];
|
accumulativeData: DockerStatsJSON["block"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerBlockChart = ({ acummulativeData }: Props) => {
|
export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
||||||
const transformedData = acummulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => {
|
||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["cpu"];
|
accumulativeData: DockerStatsJSON["cpu"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerCpuChart = ({ acummulativeData }: Props) => {
|
export const DockerCpuChart = ({ accumulativeData }: Props) => {
|
||||||
const transformedData = acummulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => {
|
||||||
return {
|
return {
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
time: item.time,
|
time: item.time,
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import {
|
|||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["disk"];
|
accumulativeData: DockerStatsJSON["disk"];
|
||||||
diskTotal: number;
|
diskTotal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerDiskChart = ({ acummulativeData, diskTotal }: Props) => {
|
export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
|
||||||
const transformedData = acummulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => {
|
||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
|||||||
import { convertMemoryToBytes } from "./show-free-container-monitoring";
|
import { convertMemoryToBytes } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["memory"];
|
accumulativeData: DockerStatsJSON["memory"];
|
||||||
memoryLimitGB: number;
|
memoryLimitGB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerMemoryChart = ({
|
export const DockerMemoryChart = ({
|
||||||
acummulativeData,
|
accumulativeData,
|
||||||
memoryLimitGB,
|
memoryLimitGB,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const transformedData = acummulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => {
|
||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
acummulativeData: DockerStatsJSON["network"];
|
accumulativeData: DockerStatsJSON["network"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerNetworkChart = ({ acummulativeData }: Props) => {
|
export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
||||||
const transformedData = acummulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => {
|
||||||
return {
|
return {
|
||||||
time: item.time,
|
time: item.time,
|
||||||
name: `Point ${index + 1}`,
|
name: `Point ${index + 1}`,
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
|
const [accumulativeData, setAccumulativeData] = useState<DockerStatsJSON>({
|
||||||
cpu: [],
|
cpu: [],
|
||||||
memory: [],
|
memory: [],
|
||||||
block: [],
|
block: [],
|
||||||
@@ -136,7 +136,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentData(defaultData);
|
setCurrentData(defaultData);
|
||||||
|
|
||||||
setAcummulativeData({
|
setAccumulativeData({
|
||||||
cpu: [],
|
cpu: [],
|
||||||
memory: [],
|
memory: [],
|
||||||
block: [],
|
block: [],
|
||||||
@@ -155,7 +155,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
network: data.network[data.network.length - 1] ?? currentData.network,
|
network: data.network[data.network.length - 1] ?? currentData.network,
|
||||||
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
|
||||||
});
|
});
|
||||||
setAcummulativeData({
|
setAccumulativeData({
|
||||||
block: data?.block || [],
|
block: data?.block || [],
|
||||||
cpu: data?.cpu || [],
|
cpu: data?.cpu || [],
|
||||||
disk: data?.disk || [],
|
disk: data?.disk || [],
|
||||||
@@ -184,7 +184,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
setCurrentData(data);
|
setCurrentData(data);
|
||||||
|
|
||||||
const MAX_DATA_POINTS = 300;
|
const MAX_DATA_POINTS = 300;
|
||||||
setAcummulativeData((prevData) => ({
|
setAccumulativeData((prevData) => ({
|
||||||
cpu: [...prevData.cpu, data.cpu].slice(-MAX_DATA_POINTS),
|
cpu: [...prevData.cpu, data.cpu].slice(-MAX_DATA_POINTS),
|
||||||
memory: [...prevData.memory, data.memory].slice(-MAX_DATA_POINTS),
|
memory: [...prevData.memory, data.memory].slice(-MAX_DATA_POINTS),
|
||||||
block: [...prevData.block, data.block].slice(-MAX_DATA_POINTS),
|
block: [...prevData.block, data.block].slice(-MAX_DATA_POINTS),
|
||||||
@@ -228,7 +228,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
)}
|
)}
|
||||||
className="w-[100%]"
|
className="w-[100%]"
|
||||||
/>
|
/>
|
||||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
<DockerCpuChart accumulativeData={accumulativeData.cpu} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -252,7 +252,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
className="w-[100%]"
|
className="w-[100%]"
|
||||||
/>
|
/>
|
||||||
<DockerMemoryChart
|
<DockerMemoryChart
|
||||||
acummulativeData={acummulativeData.memory}
|
accumulativeData={accumulativeData.memory}
|
||||||
memoryLimitGB={
|
memoryLimitGB={
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
convertMemoryToBytes(currentData.memory.value.total) /
|
convertMemoryToBytes(currentData.memory.value.total) /
|
||||||
@@ -277,7 +277,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
className="w-[100%]"
|
className="w-[100%]"
|
||||||
/>
|
/>
|
||||||
<DockerDiskChart
|
<DockerDiskChart
|
||||||
acummulativeData={acummulativeData.disk}
|
accumulativeData={accumulativeData.disk}
|
||||||
diskTotal={currentData.disk.value.diskTotal}
|
diskTotal={currentData.disk.value.diskTotal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -294,7 +294,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
|
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
|
||||||
</span>
|
</span>
|
||||||
<DockerBlockChart acummulativeData={acummulativeData.block} />
|
<DockerBlockChart accumulativeData={accumulativeData.block} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -307,7 +307,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
|
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
|
||||||
</span>
|
</span>
|
||||||
<DockerNetworkChart acummulativeData={acummulativeData.network} />
|
<DockerNetworkChart accumulativeData={accumulativeData.network} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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,
|
||||||
@@ -71,153 +73,158 @@ 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}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy MySQL"
|
|
||||||
description="Are you sure you want to deploy this mysql?"
|
|
||||||
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 MySQL database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload MySQL"
|
|
||||||
description="Are you sure you want to reload this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
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="Start MySQL"
|
title="Deploy MySQL"
|
||||||
description="Are you sure you want to start this mysql?"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mysqlId: mysqlId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("MySQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting MySQL");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
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">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MySQL database</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 && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload MySQL"
|
||||||
|
description="Are you sure you want to reload this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{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,7 +24,6 @@ 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({
|
||||||
@@ -55,8 +54,6 @@ 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),
|
||||||
@@ -89,7 +86,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 });
|
||||||
refetchActiveOrganization();
|
utils.organization.active.invalidate();
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ 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: () =>
|
||||||
@@ -56,6 +57,7 @@ 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(),
|
||||||
@@ -84,7 +86,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.args?.map((arg) => ({ value: arg })) || [],
|
args: (data as any).args?.map((arg: string) => ({ value: arg })) || [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
@@ -95,6 +97,7 @@ 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,
|
||||||
@@ -144,7 +147,14 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Command</FormLabel>
|
<FormLabel>Command</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="/bin/sh" {...field} />
|
<Input
|
||||||
|
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,6 +21,8 @@ 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,
|
||||||
@@ -73,153 +75,162 @@ 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}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy PostgreSQL"
|
|
||||||
description="Are you sure you want to deploy this postgres?"
|
|
||||||
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 PostgreSQL database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload PostgreSQL"
|
|
||||||
description="Are you sure you want to reload this postgres?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
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="Start PostgreSQL"
|
title="Deploy PostgreSQL"
|
||||||
description="Are you sure you want to start this postgres?"
|
description="Are you sure you want to deploy this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
postgresId: postgresId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("PostgreSQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting PostgreSQL");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
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">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the PostgreSQL database</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>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload PostgreSQL"
|
||||||
|
description="Are you sure you want to reload this postgres?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{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,6 +117,8 @@ 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,6 +5,7 @@ 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,
|
||||||
@@ -55,6 +56,7 @@ 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",
|
||||||
@@ -66,8 +68,9 @@ const databasesUserDefaultPlaceholder: Record<
|
|||||||
Exclude<DbType, "redis">,
|
Exclude<DbType, "redis">,
|
||||||
string
|
string
|
||||||
> = {
|
> = {
|
||||||
mongo: "mongo",
|
libsql: "libsql",
|
||||||
mariadb: "mariadb",
|
mariadb: "mariadb",
|
||||||
|
mongo: "mongo",
|
||||||
mysql: "mysql",
|
mysql: "mysql",
|
||||||
postgres: "postgres",
|
postgres: "postgres",
|
||||||
};
|
};
|
||||||
@@ -94,56 +97,88 @@ const baseDatabaseSchema = z.object({
|
|||||||
serverId: z.string().nullable(),
|
serverId: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("type", [
|
const mySchema = z
|
||||||
z
|
.discriminatedUnion("type", [
|
||||||
.object({
|
z
|
||||||
type: z.literal("postgres"),
|
.object({
|
||||||
databaseName: z.string().default("postgres"),
|
type: z.literal("libsql"),
|
||||||
databaseUser: z.string().default("postgres"),
|
dockerImage: z
|
||||||
})
|
.string()
|
||||||
.merge(baseDatabaseSchema),
|
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||||
z
|
databaseUser: z.string().default("libsql"),
|
||||||
.object({
|
sqldNode: z.enum(["primary", "replica"]).default("primary"),
|
||||||
type: z.literal("mongo"),
|
sqldPrimaryUrl: z.string().optional(),
|
||||||
databaseUser: z.string().default("mongo"),
|
enableNamespaces: z.boolean().default(false),
|
||||||
replicaSets: z.boolean().default(false),
|
})
|
||||||
})
|
.merge(baseDatabaseSchema),
|
||||||
.merge(baseDatabaseSchema),
|
z
|
||||||
z
|
.object({
|
||||||
.object({
|
type: z.literal("mariadb"),
|
||||||
type: z.literal("redis"),
|
dockerImage: z.string().default("mariadb:4"),
|
||||||
})
|
databaseRootPassword: z
|
||||||
.merge(baseDatabaseSchema),
|
.string()
|
||||||
z
|
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||||
.object({
|
message:
|
||||||
type: z.literal("mysql"),
|
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||||
databaseRootPassword: z
|
})
|
||||||
.string()
|
.optional(),
|
||||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
databaseUser: z.string().default("mariadb"),
|
||||||
|
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:
|
||||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
|
||||||
})
|
});
|
||||||
.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: {
|
||||||
@@ -166,6 +201,10 @@ 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>;
|
||||||
@@ -181,11 +220,12 @@ 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 postgresMutation = api.postgres.create.useMutation();
|
const libsqlMutation = api.libsql.create.useMutation();
|
||||||
const mongoMutation = api.mongo.create.useMutation();
|
|
||||||
const redisMutation = api.redis.create.useMutation();
|
|
||||||
const mariadbMutation = api.mariadb.create.useMutation();
|
const mariadbMutation = api.mariadb.create.useMutation();
|
||||||
|
const mongoMutation = api.mongo.create.useMutation();
|
||||||
const mysqlMutation = api.mysql.create.useMutation();
|
const mysqlMutation = api.mysql.create.useMutation();
|
||||||
|
const postgresMutation = api.postgres.create.useMutation();
|
||||||
|
const redisMutation = api.redis.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 });
|
||||||
@@ -210,13 +250,15 @@ 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 = {
|
||||||
postgres: postgresMutation,
|
libsql: libsqlMutation,
|
||||||
mongo: mongoMutation,
|
|
||||||
redis: redisMutation,
|
|
||||||
mariadb: mariadbMutation,
|
mariadb: mariadbMutation,
|
||||||
|
mongo: mongoMutation,
|
||||||
mysql: mysqlMutation,
|
mysql: mysqlMutation,
|
||||||
|
postgres: postgresMutation,
|
||||||
|
redis: redisMutation,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: AddDatabase) => {
|
const onSubmit = async (data: AddDatabase) => {
|
||||||
@@ -233,12 +275,23 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data.type === "postgres") {
|
if (data.type === "libsql") {
|
||||||
promise = postgresMutation.mutateAsync({
|
promise = libsqlMutation.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,
|
||||||
databaseName: data.databaseName || "postgres",
|
databaseRootPassword: data.databaseRootPassword || "",
|
||||||
|
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,
|
||||||
@@ -252,22 +305,6 @@ 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,
|
||||||
@@ -278,6 +315,21 @@ 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) {
|
||||||
@@ -305,6 +357,7 @@ 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">
|
||||||
@@ -506,8 +559,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{(type === "mysql" ||
|
{(type === "mariadb" ||
|
||||||
type === "mariadb" ||
|
type === "mysql" ||
|
||||||
type === "postgres") && (
|
type === "postgres") && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -524,10 +577,101 @@ 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 === "postgres" ||
|
type === "mongo" ||
|
||||||
type === "mongo") && (
|
type === "mysql" ||
|
||||||
|
type === "postgres") && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="databaseUser"
|
name="databaseUser"
|
||||||
@@ -568,7 +712,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{(type === "mysql" || type === "mariadb") && (
|
{(type === "mariadb" || type === "mysql") && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="databaseRootPassword"
|
name="databaseRootPassword"
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ 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,19 +57,13 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
// Get current user's permissions
|
// Get current user's permissions
|
||||||
const { data: currentUser } = api.user.get.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
// Check if user can create environments
|
// Check if user can create environments
|
||||||
const canCreateEnvironments =
|
const canCreateEnvironments = !!permissions?.environment.create;
|
||||||
currentUser?.role === "owner" ||
|
|
||||||
currentUser?.role === "admin" ||
|
|
||||||
currentUser?.canCreateEnvironments === true;
|
|
||||||
|
|
||||||
// Check if user can delete environments
|
// Check if user can delete environments
|
||||||
const canDeleteEnvironments =
|
const canDeleteEnvironments = !!permissions?.environment.delete;
|
||||||
currentUser?.role === "owner" ||
|
|
||||||
currentUser?.role === "admin" ||
|
|
||||||
currentUser?.canDeleteEnvironments === true;
|
|
||||||
|
|
||||||
const haveServices =
|
const haveServices =
|
||||||
selectedEnvironment &&
|
selectedEnvironment &&
|
||||||
@@ -98,6 +92,8 @@ 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,13 +28,14 @@ export type Services = {
|
|||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
type:
|
type:
|
||||||
| "mariadb"
|
|
||||||
| "application"
|
| "application"
|
||||||
| "postgres"
|
| "compose"
|
||||||
| "mysql"
|
| "libsql"
|
||||||
|
| "mariadb"
|
||||||
| "mongo"
|
| "mongo"
|
||||||
| "redis"
|
| "mysql"
|
||||||
| "compose";
|
| "postgres"
|
||||||
|
| "redis";
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ 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 } =
|
||||||
@@ -97,6 +100,10 @@ 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>
|
||||||
@@ -141,6 +148,7 @@ 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
|
||||||
@@ -157,11 +165,13 @@ API_KEY=your-api-key-here
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
{canWrite && (
|
||||||
<Button isLoading={isPending} type="submit">
|
<DialogFooter>
|
||||||
Update
|
<Button isLoading={isPending} type="submit">
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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,
|
||||||
@@ -62,6 +63,7 @@ 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()
|
||||||
@@ -75,6 +77,10 @@ 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: {
|
||||||
@@ -89,6 +95,13 @@ 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) => {
|
||||||
@@ -98,12 +111,26 @@ 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
|
||||||
@@ -190,6 +217,20 @@ 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,6 +39,9 @@ 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 } =
|
||||||
@@ -96,6 +99,10 @@ 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>
|
||||||
@@ -139,6 +146,7 @@ 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
|
||||||
@@ -154,11 +162,13 @@ PORT=3000
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
{canWrite && (
|
||||||
<Button isLoading={isPending} type="submit">
|
<DialogFooter>
|
||||||
Update
|
<Button isLoading={isPending} type="submit">
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ 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,
|
||||||
@@ -49,7 +51,6 @@ 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";
|
||||||
@@ -61,7 +62,9 @@ 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 : "",
|
||||||
@@ -75,10 +78,31 @@ 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 : "";
|
||||||
@@ -106,7 +130,7 @@ export const ShowProjects = () => {
|
|||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
const filtered = data.filter(
|
let filtered = data.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.name
|
project.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -116,6 +140,15 @@ 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) => {
|
||||||
@@ -161,18 +194,13 @@ export const ShowProjects = () => {
|
|||||||
}
|
}
|
||||||
return direction === "asc" ? comparison : -comparison;
|
return direction === "asc" ? comparison : -comparison;
|
||||||
});
|
});
|
||||||
}, [data, debouncedSearchQuery, sortBy]);
|
}, [data, debouncedSearchQuery, sortBy, selectedTagIds]);
|
||||||
|
|
||||||
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 ">
|
||||||
@@ -186,9 +214,7 @@ export const ShowProjects = () => {
|
|||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.project.create && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canCreateProjects) && (
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -214,29 +240,44 @@ 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 min-w-48 max-sm:w-full">
|
<div className="flex items-center gap-2">
|
||||||
<ArrowUpDown className="size-4 text-muted-foreground" />
|
<TagFilter
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
tags={
|
||||||
<SelectTrigger className="w-full">
|
availableTags?.map((tag) => ({
|
||||||
<SelectValue placeholder="Sort by..." />
|
id: tag.tagId,
|
||||||
</SelectTrigger>
|
name: tag.name,
|
||||||
<SelectContent>
|
color: tag.color || undefined,
|
||||||
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
})) || []
|
||||||
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
}
|
||||||
<SelectItem value="createdAt-desc">
|
selectedTags={selectedTagIds}
|
||||||
Newest first
|
onTagsChange={setSelectedTagIds}
|
||||||
</SelectItem>
|
/>
|
||||||
<SelectItem value="createdAt-asc">
|
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
||||||
Oldest first
|
<ArrowUpDown className="size-4 text-muted-foreground" />
|
||||||
</SelectItem>
|
<Select value={sortBy} onValueChange={setSortBy}>
|
||||||
<SelectItem value="services-desc">
|
<SelectTrigger className="w-full">
|
||||||
Most services
|
<SelectValue placeholder="Sort by..." />
|
||||||
</SelectItem>
|
</SelectTrigger>
|
||||||
<SelectItem value="services-asc">
|
<SelectContent>
|
||||||
Least services
|
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||||
</SelectItem>
|
<SelectItem value="name-desc">
|
||||||
</SelectContent>
|
Name (Z-A)
|
||||||
</Select>
|
</SelectItem>
|
||||||
|
<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 && (
|
||||||
@@ -253,26 +294,27 @@ 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);
|
||||||
|
|
||||||
@@ -315,6 +357,19 @@ 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" />
|
||||||
@@ -361,8 +416,7 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.project.delete && (
|
||||||
auth?.canDeleteProjects) && (
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -436,7 +490,7 @@ export const ShowProjects = () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="pt-4">
|
<CardFooter className="pt-4">
|
||||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||||
<DateTooltip date={project.createdAt}>
|
<DateTooltip date={project.createdAt}>
|
||||||
Created
|
Created
|
||||||
</DateTooltip>
|
</DateTooltip>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ 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,
|
||||||
@@ -72,153 +74,158 @@ 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}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Redis"
|
|
||||||
description="Are you sure you want to deploy this redis?"
|
|
||||||
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 Redis database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Redis"
|
|
||||||
description="Are you sure you want to reload this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
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="Start Redis"
|
title="Deploy Redis"
|
||||||
description="Are you sure you want to start this redis?"
|
description="Are you sure you want to deploy this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
redisId: redisId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Redis");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
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">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the Redis database</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 && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Redis"
|
||||||
|
description="Are you sure you want to reload this redis?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{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,7 +23,6 @@ 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";
|
||||||
|
|
||||||
@@ -56,7 +55,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 } = authClient.useSession();
|
const { data: session } = api.user.session.useQuery();
|
||||||
const { data } = api.project.all.useQuery(undefined, {
|
const { data } = api.project.all.useQuery(undefined, {
|
||||||
enabled: !!session,
|
enabled: !!session,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ export const ShowBilling = () => {
|
|||||||
api.stripe.upgradeSubscription.useMutation();
|
api.stripe.upgradeSubscription.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const [serverQuantity, setServerQuantity] = useState(3);
|
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
||||||
|
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,
|
||||||
@@ -111,6 +114,12 @@ 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,
|
||||||
@@ -679,7 +688,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceHobby(
|
{calculatePriceHobby(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -692,7 +701,8 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceHobby(serverQuantity, true) / 12
|
calculatePriceHobby(hobbyServerQuantity, true) /
|
||||||
|
12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -724,19 +734,19 @@ export const ShowBilling = () => {
|
|||||||
Servers:
|
Servers:
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) => Math.max(1, q - 1))
|
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
Number(
|
Number(
|
||||||
@@ -750,7 +760,7 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -775,7 +785,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCheckout("hobby", data!.hobbyProductId!)
|
handleCheckout("hobby", data!.hobbyProductId!)
|
||||||
}
|
}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
@@ -806,7 +816,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceStartup(
|
{calculatePriceStartup(
|
||||||
serverQuantity,
|
startupServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -819,7 +829,10 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceStartup(serverQuantity, true) / 12
|
calculatePriceStartup(
|
||||||
|
startupServerQuantity,
|
||||||
|
true,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -856,13 +869,14 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
startupServerQuantity <=
|
||||||
|
STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) =>
|
setStartupServerQuantity((q) =>
|
||||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -870,9 +884,9 @@ export const ShowBilling = () => {
|
|||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={startupServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setStartupServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
STARTUP_SERVERS_INCLUDED,
|
STARTUP_SERVERS_INCLUDED,
|
||||||
Number(
|
Number(
|
||||||
@@ -887,7 +901,9 @@ export const ShowBilling = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() =>
|
||||||
|
setStartupServerQuantity((q) => q + 1)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -917,7 +933,7 @@ export const ShowBilling = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity < STARTUP_SERVERS_INCLUDED
|
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
@@ -1009,7 +1025,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(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
@@ -1018,7 +1034,10 @@ 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(serverQuantity, isAnnual) / 12
|
calculatePrice(
|
||||||
|
hobbyServerQuantity,
|
||||||
|
isAnnual,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
/ Month USD
|
/ Month USD
|
||||||
</p>
|
</p>
|
||||||
@@ -1026,9 +1045,10 @@ export const ShowBilling = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
{calculatePrice(
|
||||||
2,
|
hobbyServerQuantity,
|
||||||
)}{" "}
|
isAnnual,
|
||||||
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1071,26 +1091,28 @@ 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">
|
||||||
{serverQuantity} Servers
|
{hobbyServerQuantity} Servers
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverQuantity <= 1) return;
|
if (hobbyServerQuantity <= 1) return;
|
||||||
|
|
||||||
setServerQuantity(serverQuantity - 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity - 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
e.target.value as unknown as number,
|
e.target.value as unknown as number,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -1099,7 +1121,9 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setServerQuantity(serverQuantity + 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity + 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@@ -1125,7 +1149,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handleCheckout("legacy", product.id);
|
handleCheckout("legacy", product.id);
|
||||||
}}
|
}}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Subscribe
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { AlertCircle, Link, Loader2, ShieldCheck, Trash2 } from "lucide-react";
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Link,
|
||||||
|
Loader2,
|
||||||
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -12,12 +21,19 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AddCertificate } from "./add-certificate";
|
import { AddCertificate } from "./add-certificate";
|
||||||
import { getCertificateChainInfo, getExpirationStatus } from "./utils";
|
import {
|
||||||
|
extractLeafCommonName,
|
||||||
|
getCertificateChainExpirationDetails,
|
||||||
|
getCertificateChainInfo,
|
||||||
|
getExpirationStatus,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export const ShowCertificates = () => {
|
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();
|
||||||
|
const [expandedChains, setExpandedChains] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -53,7 +69,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>
|
||||||
<AddCertificate />
|
{permissions?.certificate.create && <AddCertificate />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -65,6 +81,30 @@ export const ShowCertificates = () => {
|
|||||||
const chainInfo = getCertificateChainInfo(
|
const chainInfo = getCertificateChainInfo(
|
||||||
certificate.certificateData,
|
certificate.certificateData,
|
||||||
);
|
);
|
||||||
|
const commonName = extractLeafCommonName(
|
||||||
|
certificate.certificateData,
|
||||||
|
);
|
||||||
|
const chainDetails = chainInfo.isChain
|
||||||
|
? getCertificateChainExpirationDetails(
|
||||||
|
certificate.certificateData,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const isExpanded = expandedChains.has(
|
||||||
|
certificate.certificateId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleChain = () => {
|
||||||
|
setExpandedChains((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(certificate.certificateId)) {
|
||||||
|
next.delete(certificate.certificateId);
|
||||||
|
} else {
|
||||||
|
next.add(certificate.certificateId);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={certificate.certificateId}
|
key={certificate.certificateId}
|
||||||
@@ -76,12 +116,52 @@ export const ShowCertificates = () => {
|
|||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{index + 1}. {certificate.name}
|
{index + 1}. {certificate.name}
|
||||||
</span>
|
</span>
|
||||||
|
{commonName && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
CN: {commonName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{chainInfo.isChain && (
|
{chainInfo.isChain && (
|
||||||
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted/50">
|
<div className="flex flex-col gap-1.5 mt-1">
|
||||||
<Link className="size-3 text-muted-foreground" />
|
<button
|
||||||
<span className="text-xs text-muted-foreground">
|
type="button"
|
||||||
Chain ({chainInfo.count})
|
onClick={toggleChain}
|
||||||
</span>
|
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted/50 w-fit hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="size-3 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="size-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<Link className="size-3 text-muted-foreground" />
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Chain ({chainInfo.count} certificates)
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="flex flex-col gap-3 pl-2 border-l-2 border-muted">
|
||||||
|
{chainDetails?.map((cert) => (
|
||||||
|
<div
|
||||||
|
key={cert.index}
|
||||||
|
className="flex flex-col gap-1 p-2 rounded-md bg-muted/30"
|
||||||
|
>
|
||||||
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
|
{cert.label}
|
||||||
|
</span>
|
||||||
|
{cert.commonName && (
|
||||||
|
<span className="text-xs text-muted-foreground/80">
|
||||||
|
CN: {cert.commonName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`text-xs ${cert.className}`}
|
||||||
|
>
|
||||||
|
{cert.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
@@ -101,47 +181,52 @@ export const ShowCertificates = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1">
|
{permissions?.certificate.delete && (
|
||||||
<DialogAction
|
<div className="flex flex-row gap-1">
|
||||||
title="Delete Certificate"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this certificate?"
|
title="Delete Certificate"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this certificate?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
certificateId: certificate.certificateId,
|
await mutateAsync({
|
||||||
})
|
certificateId:
|
||||||
.then(() => {
|
certificate.certificateId,
|
||||||
toast.success(
|
|
||||||
"Certificate deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting certificate",
|
"Certificate deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting certificate",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
</div>
|
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 className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.certificate.create && (
|
||||||
<AddCertificate />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<AddCertificate />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
|
// Split certificate chain into individual certificates
|
||||||
|
export const splitCertificateChain = (certData: string): string[] => {
|
||||||
|
const certRegex =
|
||||||
|
/(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)/g;
|
||||||
|
const matches = certData.match(certRegex);
|
||||||
|
return matches || [];
|
||||||
|
};
|
||||||
|
|
||||||
export const extractExpirationDate = (certData: string): Date | null => {
|
export const extractExpirationDate = (certData: string): Date | null => {
|
||||||
try {
|
try {
|
||||||
// Decode PEM base64 to DER binary
|
// Decode PEM base64 to DER binary
|
||||||
@@ -14,13 +22,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: <explanation>
|
// biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation
|
||||||
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: <explanation>
|
// biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation
|
||||||
len = (len << 8) + der[pos++];
|
len = (len << 8) + der[pos++];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,8 +102,156 @@ export const extractExpirationDate = (certData: string): Date | null => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const extractCommonName = (certData: string): string | null => {
|
||||||
|
try {
|
||||||
|
// Decode PEM base64 to DER binary
|
||||||
|
const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
||||||
|
const binStr = atob(b64);
|
||||||
|
const der = new Uint8Array(binStr.length);
|
||||||
|
for (let i = 0; i < binStr.length; i++) {
|
||||||
|
der[i] = binStr.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
// Helper: read ASN.1 length field
|
||||||
|
function readLength(pos: number): { length: number; offset: number } {
|
||||||
|
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||||
|
let len = der[pos++];
|
||||||
|
if (len & 0x80) {
|
||||||
|
const bytes = len & 0x7f;
|
||||||
|
len = 0;
|
||||||
|
for (let i = 0; i < bytes; i++) {
|
||||||
|
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||||
|
len = (len << 8) + der[pos++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { length: len, offset: pos };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: skip a field
|
||||||
|
function skipField(pos: number): number {
|
||||||
|
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||||
|
pos++;
|
||||||
|
const fieldLen = readLength(pos);
|
||||||
|
return fieldLen.offset + fieldLen.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the outer certificate sequence
|
||||||
|
if (der[offset++] !== 0x30) throw new Error("Expected sequence");
|
||||||
|
({ offset } = readLength(offset));
|
||||||
|
|
||||||
|
// Skip tbsCertificate sequence
|
||||||
|
if (der[offset++] !== 0x30) throw new Error("Expected tbsCertificate");
|
||||||
|
({ offset } = readLength(offset));
|
||||||
|
|
||||||
|
// Check for optional version field (context-specific tag [0])
|
||||||
|
if (der[offset] === 0xa0) {
|
||||||
|
offset++;
|
||||||
|
const versionLen = readLength(offset);
|
||||||
|
offset = versionLen.offset + versionLen.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip serialNumber
|
||||||
|
offset = skipField(offset);
|
||||||
|
|
||||||
|
// Skip signature
|
||||||
|
offset = skipField(offset);
|
||||||
|
|
||||||
|
// Skip issuer
|
||||||
|
offset = skipField(offset);
|
||||||
|
|
||||||
|
// Skip validity
|
||||||
|
offset = skipField(offset);
|
||||||
|
|
||||||
|
// Subject sequence - where we find the CN
|
||||||
|
if (der[offset++] !== 0x30) throw new Error("Expected subject sequence");
|
||||||
|
const subjectLen = readLength(offset);
|
||||||
|
const subjectEnd = subjectLen.offset + subjectLen.length;
|
||||||
|
offset = subjectLen.offset;
|
||||||
|
|
||||||
|
// Parse subject RDNs looking for CN (OID 2.5.4.3)
|
||||||
|
while (offset < subjectEnd) {
|
||||||
|
if (der[offset++] !== 0x31) continue; // SET
|
||||||
|
const setLen = readLength(offset);
|
||||||
|
offset = setLen.offset;
|
||||||
|
|
||||||
|
if (der[offset++] !== 0x30) continue; // SEQUENCE
|
||||||
|
const seqLen = readLength(offset);
|
||||||
|
offset = seqLen.offset;
|
||||||
|
|
||||||
|
if (der[offset++] !== 0x06) continue; // OID
|
||||||
|
const oidLen = readLength(offset);
|
||||||
|
offset = oidLen.offset;
|
||||||
|
|
||||||
|
// Check if OID is 2.5.4.3 (commonName)
|
||||||
|
const oid = Array.from(der.slice(offset, offset + oidLen.length));
|
||||||
|
offset += oidLen.length;
|
||||||
|
|
||||||
|
// OID 2.5.4.3 in DER: [0x55, 0x04, 0x03]
|
||||||
|
if (
|
||||||
|
oid.length === 3 &&
|
||||||
|
oid[0] === 0x55 &&
|
||||||
|
oid[1] === 0x04 &&
|
||||||
|
oid[2] === 0x03
|
||||||
|
) {
|
||||||
|
// Next should be the string value
|
||||||
|
const strType = der[offset++];
|
||||||
|
const strLen = readLength(offset);
|
||||||
|
const cnBytes = der.slice(strLen.offset, strLen.offset + strLen.length);
|
||||||
|
return new TextDecoder().decode(cnBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing certificate CN:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract the Common Name from the first (leaf) certificate in a chain
|
||||||
|
export const extractLeafCommonName = (certData: string): string | null => {
|
||||||
|
const certs = splitCertificateChain(certData);
|
||||||
|
if (certs.length === 0) return null;
|
||||||
|
return extractCommonName(certs[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract expiration dates from all certificates in a chain
|
||||||
|
export const extractAllExpirationDates = (
|
||||||
|
certData: string,
|
||||||
|
): Array<{
|
||||||
|
cert: string;
|
||||||
|
index: number;
|
||||||
|
expirationDate: Date | null;
|
||||||
|
commonName: string | null;
|
||||||
|
}> => {
|
||||||
|
const certs = splitCertificateChain(certData);
|
||||||
|
return certs.map((cert, index) => ({
|
||||||
|
cert,
|
||||||
|
index,
|
||||||
|
expirationDate: extractExpirationDate(cert),
|
||||||
|
commonName: extractCommonName(cert),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the earliest expiration date from a certificate chain
|
||||||
|
export const getEarliestExpirationDate = (certData: string): Date | null => {
|
||||||
|
const expirationDates = extractAllExpirationDates(certData);
|
||||||
|
const validDates = expirationDates
|
||||||
|
.filter((item) => item.expirationDate !== null)
|
||||||
|
.map((item) => item.expirationDate as Date);
|
||||||
|
|
||||||
|
if (validDates.length === 0) return null;
|
||||||
|
|
||||||
|
return new Date(Math.min(...validDates.map((date) => date.getTime())));
|
||||||
|
};
|
||||||
|
|
||||||
export const getExpirationStatus = (certData: string) => {
|
export const getExpirationStatus = (certData: string) => {
|
||||||
const expirationDate = extractExpirationDate(certData);
|
const chainInfo = getCertificateChainInfo(certData);
|
||||||
|
const expirationDate = chainInfo.isChain
|
||||||
|
? getEarliestExpirationDate(certData)
|
||||||
|
: extractExpirationDate(certData);
|
||||||
|
|
||||||
if (!expirationDate)
|
if (!expirationDate)
|
||||||
return {
|
return {
|
||||||
@@ -153,3 +309,67 @@ export const getCertificateChainInfo = (certData: string) => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get detailed expiration information for all certificates in a chain
|
||||||
|
export const getCertificateChainExpirationDetails = (certData: string) => {
|
||||||
|
const allExpirations = extractAllExpirationDates(certData);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return allExpirations.map(({ index, expirationDate, commonName }) => {
|
||||||
|
if (!expirationDate) {
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
label: `Certificate ${index + 1}`,
|
||||||
|
commonName,
|
||||||
|
status: "unknown" as const,
|
||||||
|
className: "text-muted-foreground",
|
||||||
|
message: "Could not determine expiration",
|
||||||
|
expirationDate: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysUntilExpiration = Math.ceil(
|
||||||
|
(expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||||
|
);
|
||||||
|
|
||||||
|
let status: "expired" | "warning" | "valid";
|
||||||
|
let className: string;
|
||||||
|
let message: string;
|
||||||
|
|
||||||
|
if (daysUntilExpiration < 0) {
|
||||||
|
status = "expired";
|
||||||
|
className = "text-red-500";
|
||||||
|
message = `Expired on ${expirationDate.toLocaleDateString([], {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}`;
|
||||||
|
} else if (daysUntilExpiration <= 30) {
|
||||||
|
status = "warning";
|
||||||
|
className = "text-yellow-500";
|
||||||
|
message = `Expires in ${daysUntilExpiration} days`;
|
||||||
|
} else {
|
||||||
|
status = "valid";
|
||||||
|
className = "text-muted-foreground";
|
||||||
|
message = `Expires ${expirationDate.toLocaleDateString([], {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
label:
|
||||||
|
index === 0
|
||||||
|
? `Certificate ${index + 1} (Leaf)`
|
||||||
|
: `Certificate ${index + 1}`,
|
||||||
|
commonName,
|
||||||
|
status,
|
||||||
|
className,
|
||||||
|
message,
|
||||||
|
expirationDate,
|
||||||
|
daysUntilExpiration,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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">
|
||||||
@@ -44,7 +45,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>
|
||||||
<HandleRegistry />
|
{permissions?.registry.create && <HandleRegistry />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -73,45 +74,49 @@ export const ShowRegistry = () => {
|
|||||||
registryId={registry.registryId}
|
registryId={registry.registryId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.registry.delete && (
|
||||||
title="Delete Registry"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this registry configuration?"
|
title="Delete Registry"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this registry configuration?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
registryId: registry.registryId,
|
await mutateAsync({
|
||||||
})
|
registryId: registry.registryId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"Registry configuration deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting registry configuration",
|
"Registry configuration deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting registry configuration",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.registry.create && (
|
||||||
<HandleRegistry />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleRegistry />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import {
|
||||||
|
ADDITIONAL_FLAG_ERROR,
|
||||||
|
ADDITIONAL_FLAG_REGEX,
|
||||||
|
} from "@dokploy/server/db/validations/destination";
|
||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useFieldArray, 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";
|
||||||
@@ -46,6 +50,16 @@ const addDestination = z.object({
|
|||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string().min(1, "Endpoint is required"),
|
endpoint: z.string().min(1, "Endpoint is required"),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
|
additionalFlags: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
value: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Flag cannot be empty")
|
||||||
|
.regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddDestination = z.infer<typeof addDestination>;
|
type AddDestination = z.infer<typeof addDestination>;
|
||||||
@@ -89,9 +103,16 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
region: "",
|
region: "",
|
||||||
secretAccessKey: "",
|
secretAccessKey: "",
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
|
additionalFlags: [],
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addDestination),
|
resolver: zodResolver(addDestination),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "additionalFlags",
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (destination) {
|
if (destination) {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -102,6 +123,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
bucket: destination.bucket,
|
bucket: destination.bucket,
|
||||||
region: destination.region,
|
region: destination.region,
|
||||||
endpoint: destination.endpoint,
|
endpoint: destination.endpoint,
|
||||||
|
additionalFlags:
|
||||||
|
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.reset();
|
form.reset();
|
||||||
@@ -118,6 +141,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
region: data.region,
|
region: data.region,
|
||||||
secretAccessKey: data.secretAccessKey,
|
secretAccessKey: data.secretAccessKey,
|
||||||
destinationId: destinationId || "",
|
destinationId: destinationId || "",
|
||||||
|
additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [],
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
|
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
|
||||||
@@ -127,9 +151,12 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
`Error ${destinationId ? "Updating" : "Creating"} the Destination`,
|
`Error ${destinationId ? "Updating" : "Creating"} the Destination`,
|
||||||
|
{
|
||||||
|
description: e.message,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -141,6 +168,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
"secretAccessKey",
|
"secretAccessKey",
|
||||||
"bucket",
|
"bucket",
|
||||||
"endpoint",
|
"endpoint",
|
||||||
|
"additionalFlags",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@@ -179,6 +207,8 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
region,
|
region,
|
||||||
secretAccessKey: secretKey,
|
secretAccessKey: secretKey,
|
||||||
serverId,
|
serverId,
|
||||||
|
additionalFlags:
|
||||||
|
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Connection Success");
|
toast.success("Connection Success");
|
||||||
@@ -358,6 +388,48 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Additional Flags (Optional)</FormLabel>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => append({ value: "" })}
|
||||||
|
>
|
||||||
|
<PlusIcon className="size-4" />
|
||||||
|
Add Flag
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<FormField
|
||||||
|
key={field.id}
|
||||||
|
control={form.control}
|
||||||
|
name={`additionalFlags.${index}.value`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="--s3-sign-accept-encoding=false"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter
|
<DialogFooter
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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">
|
||||||
@@ -45,7 +46,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>
|
||||||
<HandleDestinations />
|
{permissions?.destination.create && <HandleDestinations />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -71,43 +72,49 @@ export const ShowDestinations = () => {
|
|||||||
<HandleDestinations
|
<HandleDestinations
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
/>
|
/>
|
||||||
<DialogAction
|
{permissions?.destination.delete && (
|
||||||
title="Delete Destination"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this destination?"
|
title="Delete Destination"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this destination?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
destinationId: destination.destinationId,
|
await mutateAsync({
|
||||||
})
|
destinationId: destination.destinationId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"Destination deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting destination");
|
toast.success(
|
||||||
});
|
"Destination deleted successfully",
|
||||||
}}
|
);
|
||||||
>
|
refetch();
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error(
|
||||||
className="group hover:bg-red-500/10 "
|
"Error deleting destination",
|
||||||
isLoading={isRemoving}
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.destination.create && (
|
||||||
<HandleDestinations />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleDestinations />
|
||||||
|
</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 } = authClient.useActiveOrganization();
|
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||||
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);
|
||||||
}, [data?.id, activeOrganization?.id, session?.user?.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}`
|
? `https://github.com/organizations/${organizationName}/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
||||||
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}`
|
: `https://github.com/settings/apps/new?state=gh_init:${activeOrganization?.id}:${session?.user?.id ?? ""}`
|
||||||
}
|
}
|
||||||
method="post"
|
method="post"
|
||||||
>
|
>
|
||||||
@@ -131,11 +131,7 @@ export const AddGithubProvider = () => {
|
|||||||
Unsure if you already have an app?
|
Unsure if you already have an app?
|
||||||
</a>
|
</a>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={isOrganization && organizationName.length < 1}
|
||||||
(isOrganization && organizationName.length < 1) ||
|
|
||||||
!activeOrganization?.id ||
|
|
||||||
!session?.user?.id
|
|
||||||
}
|
|
||||||
type="submit"
|
type="submit"
|
||||||
className="self-end"
|
className="self-end"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ export const AddGitlabProvider = () => {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
placeholder="For organization/group access use the slug name of the group eg: my-org"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="For organization/group access use the slugish name of the group eg: my-org"
|
placeholder="For organization/group access use the slug name of the group eg: my-org"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ImportIcon,
|
ImportIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -24,6 +25,13 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
import { useUrl } from "@/utils/hooks/use-url";
|
||||||
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
|
||||||
@@ -39,6 +47,8 @@ export const ShowGitProviders = () => {
|
|||||||
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
|
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.gitProvider.remove.useMutation();
|
api.gitProvider.remove.useMutation();
|
||||||
|
const { mutateAsync: toggleShare, isPending: isToggling } =
|
||||||
|
api.gitProvider.toggleShare.useMutation();
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
const getGitlabUrl = (
|
const getGitlabUrl = (
|
||||||
@@ -154,10 +164,62 @@ export const ShowGitProviders = () => {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{!gitProvider.isOwner && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Users className="size-3 mr-1" />
|
||||||
|
Shared
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1 items-center">
|
<div className="flex flex-row gap-1 items-center">
|
||||||
|
{gitProvider.isOwner && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center gap-1.5 mr-2">
|
||||||
|
<Users className="size-4 text-muted-foreground" />
|
||||||
|
<Switch
|
||||||
|
disabled={isToggling}
|
||||||
|
checked={
|
||||||
|
gitProvider.sharedWithOrganization
|
||||||
|
}
|
||||||
|
onCheckedChange={async (
|
||||||
|
checked,
|
||||||
|
) => {
|
||||||
|
await toggleShare({
|
||||||
|
gitProviderId:
|
||||||
|
gitProvider.gitProviderId,
|
||||||
|
sharedWithOrganization: checked,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
checked
|
||||||
|
? "Provider shared with organization"
|
||||||
|
: "Provider unshared",
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error updating sharing",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Share with entire organization
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
{isBitbucket &&
|
{isBitbucket &&
|
||||||
gitProvider.bitbucket?.appPassword &&
|
gitProvider.bitbucket?.appPassword &&
|
||||||
!gitProvider.bitbucket?.apiToken ? (
|
!gitProvider.bitbucket?.apiToken ? (
|
||||||
@@ -222,62 +284,71 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGithub && haveGithubRequirements && (
|
{gitProvider.isOwner && (
|
||||||
<EditGithubProvider
|
<>
|
||||||
githubId={gitProvider.github?.githubId}
|
{isGithub && haveGithubRequirements && (
|
||||||
/>
|
<EditGithubProvider
|
||||||
)}
|
githubId={gitProvider.github?.githubId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isGitlab && (
|
{isGitlab && (
|
||||||
<EditGitlabProvider
|
<EditGitlabProvider
|
||||||
gitlabId={gitProvider.gitlab?.gitlabId}
|
gitlabId={gitProvider.gitlab?.gitlabId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isBitbucket && (
|
{isBitbucket && (
|
||||||
<EditBitbucketProvider
|
<EditBitbucketProvider
|
||||||
bitbucketId={
|
bitbucketId={
|
||||||
gitProvider.bitbucket?.bitbucketId
|
gitProvider.bitbucket?.bitbucketId
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isGitea && (
|
{isGitea && (
|
||||||
<EditGiteaProvider
|
<EditGiteaProvider
|
||||||
giteaId={gitProvider.gitea?.giteaId}
|
giteaId={gitProvider.gitea?.giteaId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Git Provider"
|
title="Delete Git Provider"
|
||||||
description="Are you sure you want to delete this Git Provider?"
|
description={
|
||||||
type="destructive"
|
gitProvider.sharedWithOrganization
|
||||||
onClick={async () => {
|
? "This provider is shared with the organization. Deleting it will remove access for all members. Are you sure?"
|
||||||
await mutateAsync({
|
: "Are you sure you want to delete this Git Provider?"
|
||||||
gitProviderId: gitProvider.gitProviderId,
|
}
|
||||||
})
|
type="destructive"
|
||||||
.then(() => {
|
onClick={async () => {
|
||||||
toast.success(
|
await mutateAsync({
|
||||||
"Git Provider deleted successfully",
|
gitProviderId:
|
||||||
);
|
gitProvider.gitProviderId,
|
||||||
refetch();
|
})
|
||||||
})
|
.then(() => {
|
||||||
.catch(() => {
|
toast.success(
|
||||||
toast.error(
|
"Git Provider deleted successfully",
|
||||||
"Error deleting Git Provider",
|
);
|
||||||
);
|
refetch();
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error(
|
||||||
<Button
|
"Error deleting Git Provider",
|
||||||
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>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
GotifyIcon,
|
GotifyIcon,
|
||||||
LarkIcon,
|
LarkIcon,
|
||||||
|
MattermostIcon,
|
||||||
NtfyIcon,
|
NtfyIcon,
|
||||||
PushoverIcon,
|
PushoverIcon,
|
||||||
ResendIcon,
|
ResendIcon,
|
||||||
@@ -53,6 +54,7 @@ const notificationBaseSchema = z.object({
|
|||||||
appDeploy: z.boolean().default(false),
|
appDeploy: z.boolean().default(false),
|
||||||
appBuildError: z.boolean().default(false),
|
appBuildError: z.boolean().default(false),
|
||||||
databaseBackup: z.boolean().default(false),
|
databaseBackup: z.boolean().default(false),
|
||||||
|
dokployBackup: z.boolean().default(false),
|
||||||
volumeBackup: z.boolean().default(false),
|
volumeBackup: z.boolean().default(false),
|
||||||
dokployRestart: z.boolean().default(false),
|
dokployRestart: z.boolean().default(false),
|
||||||
dockerCleanup: z.boolean().default(false),
|
dockerCleanup: z.boolean().default(false),
|
||||||
@@ -134,6 +136,14 @@ 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"),
|
||||||
@@ -210,6 +220,10 @@ 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",
|
||||||
@@ -253,14 +267,16 @@ 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();
|
||||||
|
|
||||||
@@ -288,6 +304,9 @@ 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();
|
||||||
@@ -337,6 +356,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
dockerCleanup: notification.dockerCleanup,
|
dockerCleanup: notification.dockerCleanup,
|
||||||
webhookUrl: notification.slack?.webhookUrl,
|
webhookUrl: notification.slack?.webhookUrl,
|
||||||
@@ -351,6 +371,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
botToken: notification.telegram?.botToken,
|
botToken: notification.telegram?.botToken,
|
||||||
messageThreadId: notification.telegram?.messageThreadId || "",
|
messageThreadId: notification.telegram?.messageThreadId || "",
|
||||||
@@ -366,6 +387,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
webhookUrl: notification.discord?.webhookUrl,
|
webhookUrl: notification.discord?.webhookUrl,
|
||||||
@@ -380,6 +402,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
smtpServer: notification.email?.smtpServer,
|
smtpServer: notification.email?.smtpServer,
|
||||||
@@ -398,6 +421,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
apiKey: notification.resend?.apiKey,
|
apiKey: notification.resend?.apiKey,
|
||||||
@@ -413,6 +437,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
appToken: notification.gotify?.appToken,
|
appToken: notification.gotify?.appToken,
|
||||||
@@ -428,6 +453,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
accessToken: notification.ntfy?.accessToken || "",
|
accessToken: notification.ntfy?.accessToken || "",
|
||||||
@@ -438,12 +464,29 @@ 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,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
|
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,
|
||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
webhookUrl: notification.lark?.webhookUrl,
|
webhookUrl: notification.lark?.webhookUrl,
|
||||||
name: notification.name,
|
name: notification.name,
|
||||||
@@ -457,6 +500,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
webhookUrl: notification.teams?.webhookUrl,
|
webhookUrl: notification.teams?.webhookUrl,
|
||||||
@@ -470,6 +514,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
endpoint: notification.custom?.endpoint || "",
|
endpoint: notification.custom?.endpoint || "",
|
||||||
headers: notification.custom?.headers
|
headers: notification.custom?.headers
|
||||||
@@ -491,6 +536,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: notification.appDeploy,
|
appDeploy: notification.appDeploy,
|
||||||
dokployRestart: notification.dokployRestart,
|
dokployRestart: notification.dokployRestart,
|
||||||
databaseBackup: notification.databaseBackup,
|
databaseBackup: notification.databaseBackup,
|
||||||
|
dokployBackup: notification.dokployBackup,
|
||||||
volumeBackup: notification.volumeBackup,
|
volumeBackup: notification.volumeBackup,
|
||||||
type: notification.notificationType,
|
type: notification.notificationType,
|
||||||
userKey: notification.pushover?.userKey,
|
userKey: notification.pushover?.userKey,
|
||||||
@@ -516,6 +562,7 @@ 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,
|
||||||
@@ -528,6 +575,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy,
|
appDeploy,
|
||||||
dokployRestart,
|
dokployRestart,
|
||||||
databaseBackup,
|
databaseBackup,
|
||||||
|
dokployBackup,
|
||||||
volumeBackup,
|
volumeBackup,
|
||||||
dockerCleanup,
|
dockerCleanup,
|
||||||
serverThreshold,
|
serverThreshold,
|
||||||
@@ -539,6 +587,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
channel: data.channel,
|
channel: data.channel,
|
||||||
@@ -554,6 +603,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
botToken: data.botToken,
|
botToken: data.botToken,
|
||||||
messageThreadId: data.messageThreadId || "",
|
messageThreadId: data.messageThreadId || "",
|
||||||
@@ -570,6 +620,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
decoration: data.decoration,
|
decoration: data.decoration,
|
||||||
@@ -585,6 +636,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
smtpServer: data.smtpServer,
|
smtpServer: data.smtpServer,
|
||||||
smtpPort: data.smtpPort,
|
smtpPort: data.smtpPort,
|
||||||
@@ -604,6 +656,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
apiKey: data.apiKey,
|
apiKey: data.apiKey,
|
||||||
fromAddress: data.fromAddress,
|
fromAddress: data.fromAddress,
|
||||||
@@ -620,6 +673,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
serverUrl: data.serverUrl,
|
serverUrl: data.serverUrl,
|
||||||
appToken: data.appToken,
|
appToken: data.appToken,
|
||||||
@@ -636,6 +690,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
serverUrl: data.serverUrl,
|
serverUrl: data.serverUrl,
|
||||||
accessToken: data.accessToken || "",
|
accessToken: data.accessToken || "",
|
||||||
@@ -646,12 +701,30 @@ 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,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
|
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,
|
||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -666,6 +739,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -692,6 +766,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
endpoint: data.endpoint,
|
endpoint: data.endpoint,
|
||||||
headers: headersRecord,
|
headers: headersRecord,
|
||||||
@@ -711,6 +786,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
appDeploy: appDeploy,
|
appDeploy: appDeploy,
|
||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
|
dokployBackup: dokployBackup,
|
||||||
volumeBackup: volumeBackup,
|
volumeBackup: volumeBackup,
|
||||||
userKey: data.userKey,
|
userKey: data.userKey,
|
||||||
apiToken: data.apiToken,
|
apiToken: data.apiToken,
|
||||||
@@ -737,6 +813,9 @@ 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(
|
||||||
@@ -1403,6 +1482,62 @@ 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
|
||||||
@@ -1489,6 +1624,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "lark" && (
|
{type === "lark" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -1746,6 +1882,27 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="dokployBackup"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Dokploy Backup</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Trigger the action when a dokploy backup is created.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="volumeBackup"
|
name="volumeBackup"
|
||||||
@@ -1849,6 +2006,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
isLoadingResend ||
|
isLoadingResend ||
|
||||||
isLoadingGotify ||
|
isLoadingGotify ||
|
||||||
isLoadingNtfy ||
|
isLoadingNtfy ||
|
||||||
|
isLoadingMattermost ||
|
||||||
isLoadingLark ||
|
isLoadingLark ||
|
||||||
isLoadingTeams ||
|
isLoadingTeams ||
|
||||||
isLoadingCustom ||
|
isLoadingCustom ||
|
||||||
@@ -1908,6 +2066,12 @@ 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,6 +4,7 @@ import {
|
|||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
GotifyIcon,
|
GotifyIcon,
|
||||||
LarkIcon,
|
LarkIcon,
|
||||||
|
MattermostIcon,
|
||||||
NtfyIcon,
|
NtfyIcon,
|
||||||
ResendIcon,
|
ResendIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
@@ -26,6 +27,7 @@ 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">
|
||||||
@@ -56,7 +58,9 @@ 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>
|
||||||
<HandleNotifications />
|
{permissions?.notification.create && (
|
||||||
|
<HandleNotifications />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -118,6 +122,12 @@ 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>
|
||||||
@@ -126,45 +136,50 @@ export const ShowNotifications = () => {
|
|||||||
notificationId={notification.notificationId}
|
notificationId={notification.notificationId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.notification.delete && (
|
||||||
title="Delete Notification"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this notification?"
|
title="Delete Notification"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this notification?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
notificationId: notification.notificationId,
|
await mutateAsync({
|
||||||
})
|
notificationId:
|
||||||
.then(() => {
|
notification.notificationId,
|
||||||
toast.success(
|
|
||||||
"Notification deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting notification",
|
"Notification deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting notification",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.notification.create && (
|
||||||
<HandleNotifications />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleNotifications />
|
||||||
|
</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