mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +00:00
Merge branch 'Dokploy:canary' into 4053-fix-slack-notifications-content
This commit is contained in:
@@ -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.
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -35,6 +35,7 @@ const ENTERPRISE_RESOURCES = [
|
||||
"domain",
|
||||
"destination",
|
||||
"notification",
|
||||
"tag",
|
||||
"logs",
|
||||
"monitoring",
|
||||
"auditLog",
|
||||
|
||||
+8
-8
@@ -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 {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "application"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
}
|
||||
|
||||
export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
|
||||
+15
-14
@@ -37,7 +37,7 @@ import { AddSwarmSettings } from "./modify-swarm-settings";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||
}
|
||||
|
||||
const AddRedirectchema = z.object({
|
||||
@@ -49,15 +49,15 @@ type AddCommand = z.infer<typeof AddRedirectchema>;
|
||||
|
||||
export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||
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: () =>
|
||||
api.postgres.one.useQuery({ postgresId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -65,12 +65,13 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||
const { data: registries } = api.registry.all.useQuery();
|
||||
|
||||
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(),
|
||||
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]
|
||||
@@ -105,11 +106,11 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
applicationId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
mysqlId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
...(type === "application"
|
||||
? {
|
||||
registryId:
|
||||
|
||||
+11
-1
@@ -28,7 +28,14 @@ export const endpointSpecFormSchema = z.object({
|
||||
|
||||
interface EndpointSpecFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||
@@ -44,6 +51,7 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -56,6 +64,7 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -94,6 +103,7 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
endpointSpecSwarm: hasAnyValue ? formData : null,
|
||||
});
|
||||
|
||||
|
||||
+11
-1
@@ -26,7 +26,14 @@ export const healthCheckFormSchema = z.object({
|
||||
|
||||
interface HealthCheckFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
@@ -42,6 +49,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -54,6 +62,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -104,6 +113,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
healthCheckSwarm: hasAnyValue ? formData : null,
|
||||
});
|
||||
|
||||
|
||||
+11
-1
@@ -29,7 +29,14 @@ export const labelsFormSchema = z.object({
|
||||
|
||||
interface LabelsFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||
@@ -45,6 +52,7 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -57,6 +65,7 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -112,6 +121,7 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
labelsSwarm: labelsToSend,
|
||||
});
|
||||
|
||||
|
||||
+12
-1
@@ -23,7 +23,14 @@ import { api } from "@/utils/api";
|
||||
|
||||
interface ModeFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
@@ -39,6 +46,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -51,6 +59,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -95,6 +104,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
modeSwarm: null,
|
||||
});
|
||||
toast.success("Mode updated successfully");
|
||||
@@ -122,6 +132,7 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
modeSwarm: modeData,
|
||||
});
|
||||
|
||||
|
||||
+11
-1
@@ -35,7 +35,14 @@ export const networkFormSchema = z.object({
|
||||
|
||||
interface NetworkFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||
@@ -51,6 +58,7 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -63,6 +71,7 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -132,6 +141,7 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
networkSwarm: networksToSend,
|
||||
});
|
||||
|
||||
|
||||
+11
-1
@@ -34,7 +34,14 @@ export const placementFormSchema = z.object({
|
||||
|
||||
interface PlacementFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||
@@ -50,6 +57,7 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -62,6 +70,7 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -114,6 +123,7 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
placementSwarm: hasAnyValue
|
||||
? {
|
||||
...formData,
|
||||
|
||||
+11
-1
@@ -32,7 +32,14 @@ export const restartPolicyFormSchema = z.object({
|
||||
|
||||
interface RestartPolicyFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||
@@ -48,6 +55,7 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -60,6 +68,7 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -104,6 +113,7 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
restartPolicySwarm: hasAnyValue ? formData : null,
|
||||
});
|
||||
|
||||
|
||||
+11
-1
@@ -34,7 +34,14 @@ export const rollbackConfigFormSchema = z.object({
|
||||
|
||||
interface RollbackConfigFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||
@@ -50,6 +57,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -62,6 +70,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -103,6 +112,7 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
rollbackConfigSwarm: (hasAnyValue ? formData : null) as any,
|
||||
});
|
||||
|
||||
|
||||
+11
-1
@@ -23,7 +23,14 @@ const hasStopGracePeriodSwarm = (
|
||||
|
||||
interface StopGracePeriodFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||
@@ -39,6 +46,7 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -51,6 +59,7 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -88,6 +97,7 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
stopGracePeriodSwarm: formData.value,
|
||||
});
|
||||
|
||||
|
||||
+11
-1
@@ -34,7 +34,14 @@ export const updateConfigFormSchema = z.object({
|
||||
|
||||
interface UpdateConfigFormProps {
|
||||
id: string;
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
}
|
||||
|
||||
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||
@@ -50,6 +57,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -62,6 +70,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
};
|
||||
|
||||
const { mutateAsync } = mutationMap[type]
|
||||
@@ -109,6 +118,7 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
updateConfigSwarm: (hasAnyValue ? formData : null) as any,
|
||||
});
|
||||
|
||||
|
||||
@@ -89,12 +89,13 @@ const ULIMIT_PRESETS = [
|
||||
];
|
||||
|
||||
export type ServiceType =
|
||||
| "postgres"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "application"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "application";
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -105,27 +106,29 @@ type AddResources = z.infer<typeof addResourcesSchema>;
|
||||
|
||||
export const ShowResources = ({ id, type }: Props) => {
|
||||
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: () =>
|
||||
api.postgres.one.useQuery({ postgresId: 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]
|
||||
? queryMap[type]()
|
||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||
|
||||
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(),
|
||||
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]
|
||||
@@ -155,19 +158,20 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
ulimitsSwarm: data?.ulimitsSwarm || [],
|
||||
ulimitsSwarm: (data as any)?.ulimitsSwarm || [],
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: AddResources) => {
|
||||
await mutateAsync({
|
||||
applicationId: id || "",
|
||||
libsqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
mysqlId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
applicationId: id || "",
|
||||
cpuLimit: formData.cpuLimit || null,
|
||||
cpuReservation: formData.cpuReservation || null,
|
||||
memoryLimit: formData.memoryLimit || null,
|
||||
|
||||
@@ -34,13 +34,13 @@ interface Props {
|
||||
serviceId: string;
|
||||
serviceType:
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "redis"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "compose"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "compose";
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
refetch: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -29,23 +29,25 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
||||
if (!canRead) return null;
|
||||
|
||||
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: () =>
|
||||
api.postgres.one.useQuery({ postgresId: 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]
|
||||
? queryMap[type]()
|
||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||
const { mutateAsync: deleteVolume, isPending: isRemoving } =
|
||||
api.mounts.remove.useMutation();
|
||||
|
||||
return (
|
||||
<Card className="bg-background">
|
||||
<CardHeader className="flex flex-row justify-between flex-wrap gap-4">
|
||||
|
||||
@@ -67,13 +67,13 @@ interface Props {
|
||||
refetch: () => void;
|
||||
serviceType:
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "redis"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "compose"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "compose";
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
}
|
||||
|
||||
export const UpdateVolume = ({
|
||||
@@ -253,7 +253,7 @@ export const UpdateVolume = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="max-w-full max-w-[45rem]">
|
||||
<FormItem className="w-full max-w-[45rem]">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -39,15 +39,16 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canWrite = permissions?.envVars.write ?? false;
|
||||
const queryMap = {
|
||||
postgres: () =>
|
||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||
mysql: () => api.mysql.one.useQuery({ mysqlId: 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 }),
|
||||
compose: () =>
|
||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||
mysql: () => api.mysql.one.useQuery({ mysqlId: 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]
|
||||
? queryMap[type]()
|
||||
@@ -55,12 +56,13 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
|
||||
const mutationMap = {
|
||||
postgres: () => api.postgres.update.useMutation(),
|
||||
redis: () => api.redis.update.useMutation(),
|
||||
mysql: () => api.mysql.update.useMutation(),
|
||||
compose: () => api.compose.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
mariadb: () => api.mariadb.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]
|
||||
? mutationMap[type]()
|
||||
@@ -87,12 +89,13 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
|
||||
const onSubmit = async (formData: EnvironmentSchema) => {
|
||||
mutateAsync({
|
||||
composeId: id || "",
|
||||
libsqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
mysqlId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
composeId: id || "",
|
||||
env: formData.environment,
|
||||
})
|
||||
.then(async () => {
|
||||
|
||||
@@ -71,6 +71,7 @@ const formSchema = z
|
||||
"mongo",
|
||||
"mysql",
|
||||
"redis",
|
||||
"libsql",
|
||||
]),
|
||||
serviceName: z.string(),
|
||||
destinationId: z.string().min(1, "Destination required"),
|
||||
|
||||
@@ -57,6 +57,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||
mariadb: () =>
|
||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
||||
application: () =>
|
||||
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
|
||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||
@@ -72,6 +73,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
redis: () => api.redis.remove.useMutation(),
|
||||
mysql: () => api.mysql.remove.useMutation(),
|
||||
mariadb: () => api.mariadb.remove.useMutation(),
|
||||
libsql: () => api.libsql.remove.useMutation(),
|
||||
application: () => api.application.delete.useMutation(),
|
||||
mongo: () => api.mongo.remove.useMutation(),
|
||||
compose: () => api.compose.delete.useMutation(),
|
||||
@@ -98,6 +100,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
libsqlId: id || "",
|
||||
applicationId: id || "",
|
||||
composeId: id || "",
|
||||
deleteVolumes,
|
||||
|
||||
@@ -65,7 +65,13 @@ import { ScheduleFormField } from "../../application/schedules/handle-schedules"
|
||||
|
||||
type CacheType = "cache" | "fetch";
|
||||
|
||||
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||
type DatabaseType =
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "web-server"
|
||||
| "libsql";
|
||||
|
||||
const Schema = z
|
||||
.object({
|
||||
@@ -77,7 +83,7 @@ const Schema = z
|
||||
keepLatestCount: z.coerce.number().optional(),
|
||||
serviceName: z.string().nullable(),
|
||||
databaseType: z
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||
.optional(),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
metadata: z
|
||||
@@ -209,7 +215,12 @@ export const HandleBackup = ({
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
database: databaseType === "web-server" ? "dokploy" : "",
|
||||
database:
|
||||
databaseType === "web-server"
|
||||
? "dokploy"
|
||||
: databaseType === "libsql"
|
||||
? "iku.db"
|
||||
: "",
|
||||
destinationId: "",
|
||||
enabled: true,
|
||||
prefix: "/",
|
||||
@@ -246,7 +257,9 @@ export const HandleBackup = ({
|
||||
? backup?.database
|
||||
: databaseType === "web-server"
|
||||
? "dokploy"
|
||||
: "",
|
||||
: databaseType === "libsql"
|
||||
? "iku.db"
|
||||
: "",
|
||||
destinationId: backup?.destinationId ?? "",
|
||||
enabled: backup?.enabled ?? true,
|
||||
prefix: backup?.prefix ?? "/",
|
||||
@@ -281,11 +294,15 @@ export const HandleBackup = ({
|
||||
? {
|
||||
mongoId: id,
|
||||
}
|
||||
: databaseType === "web-server"
|
||||
: databaseType === "libsql"
|
||||
? {
|
||||
userId: id,
|
||||
libsqlId: id,
|
||||
}
|
||||
: undefined;
|
||||
: databaseType === "web-server"
|
||||
? {
|
||||
userId: id,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await createBackup({
|
||||
destinationId: data.destinationId,
|
||||
@@ -568,7 +585,10 @@ export const HandleBackup = ({
|
||||
<FormLabel>Database</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={databaseType === "web-server"}
|
||||
disabled={
|
||||
databaseType === "web-server" ||
|
||||
databaseType === "libsql"
|
||||
}
|
||||
placeholder={"dokploy"}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@@ -88,7 +88,7 @@ const RestoreBackupSchema = z
|
||||
message: "Database name is required",
|
||||
}),
|
||||
databaseType: z
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||
.optional(),
|
||||
backupType: z.enum(["database", "compose"]).default("database"),
|
||||
metadata: z
|
||||
@@ -211,7 +211,12 @@ export const RestoreBackup = ({
|
||||
defaultValues: {
|
||||
destinationId: "",
|
||||
backupFile: "",
|
||||
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||
databaseName:
|
||||
databaseType === "web-server"
|
||||
? "dokploy"
|
||||
: databaseType === "libsql"
|
||||
? "iku.db"
|
||||
: "",
|
||||
databaseType:
|
||||
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
||||
backupType: backupType,
|
||||
@@ -523,7 +528,10 @@ export const RestoreBackup = ({
|
||||
<Input
|
||||
placeholder="Enter database name"
|
||||
{...field}
|
||||
disabled={databaseType === "web-server"}
|
||||
disabled={
|
||||
databaseType === "web-server" ||
|
||||
databaseType === "libsql"
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -53,14 +53,16 @@ export const ShowBackups = ({
|
||||
const queryMap =
|
||||
backupType === "database"
|
||||
? {
|
||||
postgres: () =>
|
||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||
mysql: () =>
|
||||
api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||
mariadb: () =>
|
||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||
mongo: () =>
|
||||
api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||
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(),
|
||||
}
|
||||
: {
|
||||
@@ -77,10 +79,11 @@ export const ShowBackups = ({
|
||||
const mutationMap =
|
||||
backupType === "database"
|
||||
? {
|
||||
postgres: api.backup.manualBackupPostgres.useMutation(),
|
||||
mysql: api.backup.manualBackupMySql.useMutation(),
|
||||
mariadb: api.backup.manualBackupMariadb.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(),
|
||||
}
|
||||
: {
|
||||
|
||||
+251
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+121
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -42,6 +42,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
|
||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
||||
mariadb: () =>
|
||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||
application: () =>
|
||||
@@ -56,6 +57,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
postgres: () => api.postgres.update.useMutation(),
|
||||
redis: () => api.redis.update.useMutation(),
|
||||
mysql: () => api.mysql.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
application: () => api.application.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
@@ -84,7 +86,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
form.reset({
|
||||
dockerImage: data.dockerImage,
|
||||
command: data.command || "",
|
||||
args: data.args?.map((arg) => ({ value: arg })) || [],
|
||||
args: (data as any).args?.map((arg: string) => ({ value: arg })) || [],
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
@@ -95,6 +97,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
libsqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
dockerImage: formData?.dockerImage,
|
||||
command: formData?.command,
|
||||
@@ -144,7 +147,14 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Command</FormLabel>
|
||||
<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>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
LibsqlIcon,
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
MysqlIcon,
|
||||
@@ -55,6 +56,7 @@ import { api } from "@/utils/api";
|
||||
type DbType = z.infer<typeof mySchema>["type"];
|
||||
|
||||
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||
libsql: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
||||
mongo: "mongo:7",
|
||||
mariadb: "mariadb:11",
|
||||
mysql: "mysql:8",
|
||||
@@ -66,8 +68,9 @@ const databasesUserDefaultPlaceholder: Record<
|
||||
Exclude<DbType, "redis">,
|
||||
string
|
||||
> = {
|
||||
mongo: "mongo",
|
||||
libsql: "libsql",
|
||||
mariadb: "mariadb",
|
||||
mongo: "mongo",
|
||||
mysql: "mysql",
|
||||
postgres: "postgres",
|
||||
};
|
||||
@@ -94,56 +97,88 @@ const baseDatabaseSchema = z.object({
|
||||
serverId: z.string().nullable(),
|
||||
});
|
||||
|
||||
const mySchema = z.discriminatedUnion("type", [
|
||||
z
|
||||
.object({
|
||||
type: z.literal("postgres"),
|
||||
databaseName: z.string().default("postgres"),
|
||||
databaseUser: z.string().default("postgres"),
|
||||
})
|
||||
.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("redis"),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("mysql"),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
const mySchema = z
|
||||
.discriminatedUnion("type", [
|
||||
z
|
||||
.object({
|
||||
type: z.literal("libsql"),
|
||||
dockerImage: z
|
||||
.string()
|
||||
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||
databaseUser: z.string().default("libsql"),
|
||||
sqldNode: z.enum(["primary", "replica"]).default("primary"),
|
||||
sqldPrimaryUrl: z.string().optional(),
|
||||
enableNamespaces: z.boolean().default(false),
|
||||
})
|
||||
.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),
|
||||
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:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databaseUser: z.string().default("mysql"),
|
||||
databaseName: z.string().default("mysql"),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("mariadb"),
|
||||
dockerImage: z.string().default("mariadb:4"),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databaseUser: z.string().default("mariadb"),
|
||||
databaseName: z.string().default("mariadb"),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
]);
|
||||
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const databasesMap = {
|
||||
postgres: {
|
||||
@@ -166,6 +201,10 @@ const databasesMap = {
|
||||
icon: <RedisIcon />,
|
||||
label: "Redis",
|
||||
},
|
||||
libsql: {
|
||||
icon: <LibsqlIcon className="size-10" />,
|
||||
label: "libSQL",
|
||||
},
|
||||
};
|
||||
|
||||
type AddDatabase = z.infer<typeof mySchema>;
|
||||
@@ -181,11 +220,12 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
const slug = slugify(projectName);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const postgresMutation = api.postgres.create.useMutation();
|
||||
const mongoMutation = api.mongo.create.useMutation();
|
||||
const redisMutation = api.redis.create.useMutation();
|
||||
const libsqlMutation = api.libsql.create.useMutation();
|
||||
const mariadbMutation = api.mariadb.create.useMutation();
|
||||
const mongoMutation = api.mongo.create.useMutation();
|
||||
const mysqlMutation = api.mysql.create.useMutation();
|
||||
const postgresMutation = api.postgres.create.useMutation();
|
||||
const redisMutation = api.redis.create.useMutation();
|
||||
|
||||
// Get environment data to extract projectId
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
@@ -210,13 +250,15 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
},
|
||||
resolver: zodResolver(mySchema),
|
||||
});
|
||||
const sqldNode = form.watch("sqldNode");
|
||||
const type = form.watch("type");
|
||||
const activeMutation = {
|
||||
postgres: postgresMutation,
|
||||
mongo: mongoMutation,
|
||||
redis: redisMutation,
|
||||
libsql: libsqlMutation,
|
||||
mariadb: mariadbMutation,
|
||||
mongo: mongoMutation,
|
||||
mysql: mysqlMutation,
|
||||
postgres: postgresMutation,
|
||||
redis: redisMutation,
|
||||
};
|
||||
|
||||
const onSubmit = async (data: AddDatabase) => {
|
||||
@@ -233,12 +275,23 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
if (data.type === "postgres") {
|
||||
promise = postgresMutation.mutateAsync({
|
||||
if (data.type === "libsql") {
|
||||
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,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseName: data.databaseName || "postgres",
|
||||
|
||||
databaseRootPassword: data.databaseRootPassword || "",
|
||||
databaseName: data.databaseName || "mariadb",
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
@@ -252,22 +305,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
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") {
|
||||
promise = mysqlMutation.mutateAsync({
|
||||
...commonParams,
|
||||
@@ -278,6 +315,21 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
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) {
|
||||
@@ -305,6 +357,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
@@ -506,8 +559,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{(type === "mysql" ||
|
||||
type === "mariadb" ||
|
||||
{(type === "mariadb" ||
|
||||
type === "mysql" ||
|
||||
type === "postgres") && (
|
||||
<FormField
|
||||
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 === "postgres" ||
|
||||
type === "mongo") && (
|
||||
type === "mongo" ||
|
||||
type === "mysql" ||
|
||||
type === "postgres") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databaseUser"
|
||||
@@ -568,7 +712,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{(type === "mysql" || type === "mariadb") && (
|
||||
{(type === "mariadb" || type === "mysql") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databaseRootPassword"
|
||||
|
||||
@@ -28,13 +28,14 @@ export type Services = {
|
||||
serverId?: string | null;
|
||||
name: string;
|
||||
type:
|
||||
| "mariadb"
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "compose"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
|
||||
@@ -51,7 +51,6 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TimeBadge } from "@/components/ui/time-badge";
|
||||
import { api } from "@/utils/api";
|
||||
import { useDebounce } from "@/utils/hooks/use-debounce";
|
||||
import { HandleProject } from "./handle-project";
|
||||
@@ -199,11 +198,6 @@ export const ShowProjects = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isCloud && (
|
||||
<div className="absolute top-4 right-4">
|
||||
<TimeBadge />
|
||||
</div>
|
||||
)}
|
||||
<BreadcrumbSidebar
|
||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||
/>
|
||||
@@ -300,26 +294,27 @@ export const ShowProjects = () => {
|
||||
.map(
|
||||
(env) =>
|
||||
env.applications.length === 0 &&
|
||||
env.compose.length === 0 &&
|
||||
env.libsql.length === 0 &&
|
||||
env.mariadb.length === 0 &&
|
||||
env.mongo.length === 0 &&
|
||||
env.mysql.length === 0 &&
|
||||
env.postgres.length === 0 &&
|
||||
env.redis.length === 0 &&
|
||||
env.applications.length === 0 &&
|
||||
env.compose.length === 0,
|
||||
env.redis.length === 0,
|
||||
)
|
||||
.every(Boolean);
|
||||
|
||||
const totalServices = project?.environments
|
||||
.map(
|
||||
(env) =>
|
||||
env.applications.length +
|
||||
env.compose.length +
|
||||
env.libsql.length +
|
||||
env.mariadb.length +
|
||||
env.mongo.length +
|
||||
env.mysql.length +
|
||||
env.postgres.length +
|
||||
env.redis.length +
|
||||
env.applications.length +
|
||||
env.compose.length,
|
||||
env.redis.length,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr, 0);
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ export type Services = {
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "compose"
|
||||
| "libsql";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -136,6 +137,18 @@ export const extractServices = (data: Environment | undefined) => {
|
||||
serverId: item.serverId,
|
||||
})) ?? []) as Services[];
|
||||
|
||||
const libsql: Services[] =
|
||||
data?.libsql?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "libsql" as const,
|
||||
id: item.libsqlId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
})) || [];
|
||||
|
||||
applications.push(
|
||||
...mysql,
|
||||
...redis,
|
||||
@@ -143,6 +156,7 @@ export const extractServices = (data: Environment | undefined) => {
|
||||
...postgres,
|
||||
...mariadb,
|
||||
...compose,
|
||||
...libsql,
|
||||
);
|
||||
|
||||
applications.sort((a, b) => {
|
||||
|
||||
@@ -17,17 +17,18 @@ import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||
}
|
||||
|
||||
export const RebuildDatabase = ({ id, type }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const mutationMap = {
|
||||
postgres: () => api.postgres.rebuild.useMutation(),
|
||||
mysql: () => api.mysql.rebuild.useMutation(),
|
||||
libsql: () => api.libsql.rebuild.useMutation(),
|
||||
mariadb: () => api.mariadb.rebuild.useMutation(),
|
||||
mongo: () => api.mongo.rebuild.useMutation(),
|
||||
mysql: () => api.mysql.rebuild.useMutation(),
|
||||
postgres: () => api.postgres.rebuild.useMutation(),
|
||||
redis: () => api.redis.rebuild.useMutation(),
|
||||
};
|
||||
|
||||
@@ -36,10 +37,11 @@ export const RebuildDatabase = ({ id, type }: Props) => {
|
||||
const handleRebuild = async () => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
postgresId: type === "postgres" ? id : "",
|
||||
mysqlId: type === "mysql" ? id : "",
|
||||
libsqlId: type === "libsql" ? id : "",
|
||||
mariadbId: type === "mariadb" ? id : "",
|
||||
mongoId: type === "mongo" ? id : "",
|
||||
mysqlId: type === "mysql" ? id : "",
|
||||
postgresId: type === "postgres" ? id : "",
|
||||
redisId: type === "redis" ? id : "",
|
||||
});
|
||||
toast.success("Database rebuilt successfully");
|
||||
|
||||
@@ -6,14 +6,20 @@ import { RebuildDatabase } from "./rebuild-database";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||
}
|
||||
|
||||
export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<ShowCustomCommand id={id} type={type} />
|
||||
<ShowClusterSettings id={id} type={type} />
|
||||
{type === "mariadb" ||
|
||||
type === "mongo" ||
|
||||
type === "mysql" ||
|
||||
type === "postgres" ||
|
||||
type === "redis" ? (
|
||||
<ShowClusterSettings id={id} type={type} />
|
||||
) : null}
|
||||
<ShowVolumes id={id} type={type} />
|
||||
<ShowResources id={id} type={type} />
|
||||
<RebuildDatabase id={id} type={type} />
|
||||
|
||||
@@ -156,6 +156,61 @@ export const RedisIcon = ({ className }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const LibsqlIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
aria-label="libsql"
|
||||
height="1em"
|
||||
width="1em"
|
||||
viewBox="0 0 217.2 217.2"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
|
||||
d="M118,87.2c.7,0,1.3,0,1.9.3.4.1.8.3,1.2.5.2.1.4.2.6.3.4.3.7.5,1.1.9l5.2,5.2c2.7,2.7,2.7,7,0,9.7l-95.1,95.1c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l85.1-85.1,10-10c.3-.3.7-.6,1.1-.9.2-.1.4-.2.6-.3.4-.2.8-.4,1.2-.5.6-.2,1.3-.3,1.9-.3M118,71.2c-2.2,0-4.4.3-6.5.9-1.4.4-2.8,1-4.1,1.7-.7.4-1.3.7-2,1.2-1.3.8-2.5,1.8-3.6,2.9l-10,10L6.7,173c-8.9,8.9-8.9,23.4,0,32.3l5.2,5.2c4.3,4.3,10.1,6.7,16.2,6.7s11.8-2.4,16.2-6.7l95.1-95.1c4.3-4.3,6.7-10.1,6.7-16.2s-2.4-11.8-6.7-16.2l-5.2-5.2c-1.1-1.1-2.3-2.1-3.6-2.9-.6-.4-1.3-.8-1.9-1.2-1.3-.7-2.7-1.3-4.1-1.7-2.1-.6-4.3-.9-6.5-.9h0Z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
|
||||
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,0c-8.4,0-16.3,3.3-22.3,9.2L17.4,88.9c-8.5,8.5-11.4,20.8-7.6,32.1l16.5,49.9c3.1,9.4,10.6,16.9,20,20l49.9,16.5c3.2,1.1,6.5,1.6,9.9,1.6,8.4,0,16.3-3.3,22.3-9.2l79.7-79.7c6.3-6.3,9.7-15.1,9.2-24.1l-3.8-66.5c-.8-13.9-11.9-24.9-25.7-25.7L121.2,0c-.6,0-1.2,0-1.8,0h0Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#a8f7d9", strokeWidth: "0px" }}
|
||||
d="M24.9,116.1l16.5,49.9c1.5,4.7,5.2,8.3,9.8,9.8l49.9,16.5c5.6,1.8,11.7.4,15.8-3.8l79.7-79.7c3.1-3.1,4.8-7.4,4.5-11.8l-3.8-66.5c-.3-5.8-4.9-10.3-10.7-10.7l-66.5-3.8c-4.4-.3-8.7,1.4-11.8,4.5L28.7,100.3c-4.1,4.1-5.6,10.3-3.8,15.8Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,6c-6.8,0-13.2,2.7-18,7.5L21.6,93.2c-6.8,6.8-9.2,16.8-6.2,26l16.5,49.9c2.5,7.6,8.6,13.7,16.2,16.2l49.9,16.5c2.6.9,5.3,1.3,8,1.3,6.8,0,13.2-2.7,18-7.5l79.7-79.7c5.1-5.1,7.8-12.2,7.4-19.5l-3.8-66.5c-.6-10.8-9.3-19.5-20.1-20.1l-66.5-3.8c-.5,0-1,0-1.5,0h0Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M136.7,173.7l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M166.5,143.9l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M43.5,80.5l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M73.3,50.7l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
style={{ fill: "#79ac91", strokeWidth: "0px" }}
|
||||
d="M130.6,101.5l-97.7,97.7c-2.7,2.7-7,2.7-9.7,0l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c2.8-.3,5.1,2.1,4.9,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M129.5,83.3c2.6,0,4.7,2.2,4.4,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5l-97.7,97.7c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c.1,0,.3,0,.4,0M129.5,73.3h0c-.5,0-.9,0-1.4,0l-7.9.8c-4.4.4-8.5,2.4-11.5,5.5L10.9,177.3c-6.6,6.6-6.6,17.3,0,23.8l5.2,5.2c3.2,3.2,7.4,4.9,11.9,4.9s8.7-1.8,11.9-4.9l97.7-97.7c3.1-3.1,5-7.2,5.5-11.5l.8-7.9c.4-4-.9-8.1-3.7-11.1-2.7-3-6.6-4.7-10.7-4.7h0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GitlabIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { useRouter } from "next/router";
|
||||
import { type ComponentType, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
LibsqlIcon,
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
MysqlIcon,
|
||||
@@ -58,6 +59,7 @@ type EnvironmentServiceCollections = {
|
||||
mariadb: (NamedService & { mariadbId: string })[];
|
||||
redis: (NamedService & { redisId: string })[];
|
||||
mongo: (NamedService & { mongoId: string })[];
|
||||
libsql: (NamedService & { libsqlId: string })[];
|
||||
};
|
||||
|
||||
type ServiceCollections = Pick<
|
||||
@@ -69,6 +71,7 @@ type ServiceCollections = Pick<
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "mongo"
|
||||
| "libsql"
|
||||
>;
|
||||
|
||||
const SERVICE_COLLECTION_KEYS = [
|
||||
@@ -79,6 +82,7 @@ const SERVICE_COLLECTION_KEYS = [
|
||||
"mariadb",
|
||||
"redis",
|
||||
"mongo",
|
||||
"libsql",
|
||||
] as const satisfies ReadonlyArray<keyof ServiceCollections>;
|
||||
|
||||
const SERVICE_QUERY_KEYS = [
|
||||
@@ -89,6 +93,7 @@ const SERVICE_QUERY_KEYS = [
|
||||
"mariadbId",
|
||||
"redisId",
|
||||
"mongoId",
|
||||
"libsqlId",
|
||||
] as const;
|
||||
|
||||
const SERVICE_ICONS: Record<
|
||||
@@ -102,6 +107,7 @@ const SERVICE_ICONS: Record<
|
||||
mariadb: MariadbIcon,
|
||||
redis: RedisIcon,
|
||||
mongo: MongodbIcon,
|
||||
libsql: LibsqlIcon,
|
||||
};
|
||||
|
||||
const getStringQueryParam = (value: string | string[] | undefined) =>
|
||||
@@ -156,6 +162,7 @@ const extractServicesFromEnvironment = (
|
||||
...mapServices(servicesByType.mariadb, (item) => item.mariadbId, "mariadb"),
|
||||
...mapServices(servicesByType.redis, (item) => item.redisId, "redis"),
|
||||
...mapServices(servicesByType.mongo, (item) => item.mongoId, "mongo"),
|
||||
...mapServices(servicesByType.libsql, (item) => item.libsqlId, "libsql"),
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const CommandInput = React.forwardRef<
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-inset",
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
CREATE TYPE "public"."sqldNode" AS ENUM('primary', 'replica');--> statement-breakpoint
|
||||
ALTER TYPE "public"."databaseType" ADD VALUE 'libsql';--> statement-breakpoint
|
||||
ALTER TYPE "public"."serviceType" ADD VALUE 'libsql';--> statement-breakpoint
|
||||
CREATE TABLE "libsql" (
|
||||
"libsqlId" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"appName" text NOT NULL,
|
||||
"description" text,
|
||||
"databaseUser" text NOT NULL,
|
||||
"databasePassword" text NOT NULL,
|
||||
"sqldNode" "sqldNode" DEFAULT 'primary' NOT NULL,
|
||||
"sqldPrimaryUrl" text,
|
||||
"enableNamespaces" boolean DEFAULT false NOT NULL,
|
||||
"dockerImage" text NOT NULL,
|
||||
"command" text,
|
||||
"env" text,
|
||||
"memoryReservation" text,
|
||||
"memoryLimit" text,
|
||||
"cpuReservation" text,
|
||||
"cpuLimit" text,
|
||||
"externalPort" integer,
|
||||
"externalGRPCPort" integer,
|
||||
"externalAdminPort" integer,
|
||||
"applicationStatus" "applicationStatus" DEFAULT 'idle' NOT NULL,
|
||||
"healthCheckSwarm" json,
|
||||
"restartPolicySwarm" json,
|
||||
"placementSwarm" json,
|
||||
"updateConfigSwarm" json,
|
||||
"rollbackConfigSwarm" json,
|
||||
"modeSwarm" json,
|
||||
"labelsSwarm" json,
|
||||
"networkSwarm" json,
|
||||
"stopGracePeriodSwarm" bigint,
|
||||
"endpointSpecSwarm" json,
|
||||
"replicas" integer DEFAULT 1 NOT NULL,
|
||||
"createdAt" text NOT NULL,
|
||||
"environmentId" text NOT NULL,
|
||||
"serverId" text,
|
||||
CONSTRAINT "libsql_appName_unique" UNIQUE("appName")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "backup" ADD COLUMN "libsqlId" text;--> statement-breakpoint
|
||||
ALTER TABLE "mount" ADD COLUMN "libsqlId" text;--> statement-breakpoint
|
||||
ALTER TABLE "volume_backup" ADD COLUMN "libsqlId" text;--> statement-breakpoint
|
||||
ALTER TABLE "libsql" ADD CONSTRAINT "libsql_environmentId_environment_environmentId_fk" FOREIGN KEY ("environmentId") REFERENCES "public"."environment"("environmentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "libsql" ADD CONSTRAINT "libsql_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "backup" ADD CONSTRAINT "backup_libsqlId_libsql_libsqlId_fk" FOREIGN KEY ("libsqlId") REFERENCES "public"."libsql"("libsqlId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "mount" ADD CONSTRAINT "mount_libsqlId_libsql_libsqlId_fk" FOREIGN KEY ("libsqlId") REFERENCES "public"."libsql"("libsqlId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "volume_backup" ADD CONSTRAINT "volume_backup_libsqlId_libsql_libsqlId_fk" FOREIGN KEY ("libsqlId") REFERENCES "public"."libsql"("libsqlId") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1072,6 +1072,13 @@
|
||||
"when": 1773903778014,
|
||||
"tag": "0152_odd_firelord",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 153,
|
||||
"version": "7",
|
||||
"when": 1774322599182,
|
||||
"tag": "0153_motionless_mastermind",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,11 +6,12 @@ import { useCallback, useEffect, useState } from "react";
|
||||
const PAGES = [
|
||||
"compose",
|
||||
"application",
|
||||
"postgres",
|
||||
"redis",
|
||||
"mysql",
|
||||
"libsql",
|
||||
"mariadb",
|
||||
"mongodb",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"redis",
|
||||
] as const;
|
||||
type Page = (typeof PAGES)[number];
|
||||
|
||||
@@ -63,11 +64,12 @@ const REDIS_SHORTCUTS: Shortcuts = {
|
||||
const SHORTCUTS: ShortcutsDictionary = {
|
||||
application: APPLICATION_SHORTCUTS,
|
||||
compose: COMPOSE_SHORTCUTS,
|
||||
postgres: POSTGRES_SHORTCUTS,
|
||||
redis: REDIS_SHORTCUTS,
|
||||
mysql: POSTGRES_SHORTCUTS,
|
||||
libsql: POSTGRES_SHORTCUTS,
|
||||
mariadb: POSTGRES_SHORTCUTS,
|
||||
mongodb: POSTGRES_SHORTCUTS,
|
||||
mysql: POSTGRES_SHORTCUTS,
|
||||
postgres: POSTGRES_SHORTCUTS,
|
||||
redis: REDIS_SHORTCUTS,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,7 @@ import { DuplicateProject } from "@/components/dashboard/project/duplicate-proje
|
||||
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
|
||||
import { ProjectEnvironment } from "@/components/dashboard/projects/project-environment";
|
||||
import {
|
||||
LibsqlIcon,
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
MysqlIcon,
|
||||
@@ -111,7 +112,8 @@ export type Services = {
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "compose"
|
||||
| "libsql";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -248,14 +250,27 @@ export const extractServicesFromEnvironment = (
|
||||
};
|
||||
}) || [];
|
||||
|
||||
const libsql: Services[] =
|
||||
environment.libsql?.map((item) => ({
|
||||
name: item.name,
|
||||
type: "libsql",
|
||||
id: item.libsqlId,
|
||||
createdAt: item.createdAt,
|
||||
status: item.applicationStatus,
|
||||
description: item.description,
|
||||
serverId: item.serverId,
|
||||
serverName: item?.server?.name || null,
|
||||
})) || [];
|
||||
|
||||
allServices.push(
|
||||
...applications,
|
||||
...compose,
|
||||
...libsql,
|
||||
...mysql,
|
||||
...redis,
|
||||
...mongo,
|
||||
...postgres,
|
||||
...mariadb,
|
||||
...compose,
|
||||
);
|
||||
|
||||
allServices.sort((a, b) => {
|
||||
@@ -383,7 +398,8 @@ const EnvironmentPage = (
|
||||
(currentEnvironment.postgres?.length || 0) === 0 &&
|
||||
(currentEnvironment.redis?.length || 0) === 0 &&
|
||||
(currentEnvironment.applications?.length || 0) === 0 &&
|
||||
(currentEnvironment.compose?.length || 0) === 0);
|
||||
(currentEnvironment.compose?.length || 0) === 0 &&
|
||||
(currentEnvironment.libsql?.length || 0) === 0);
|
||||
|
||||
const applications = extractServicesFromEnvironment(currentEnvironment);
|
||||
|
||||
@@ -396,6 +412,7 @@ const EnvironmentPage = (
|
||||
{ value: "mysql", label: "MySQL", icon: MysqlIcon },
|
||||
{ value: "redis", label: "Redis", icon: RedisIcon },
|
||||
{ value: "compose", label: "Compose", icon: CircuitBoard },
|
||||
{ value: "libsql", label: "Libsql", icon: LibsqlIcon },
|
||||
];
|
||||
|
||||
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
|
||||
@@ -1525,6 +1542,9 @@ const EnvironmentPage = (
|
||||
{service.type === "compose" && (
|
||||
<CircuitBoard className="h-6 w-6" />
|
||||
)}
|
||||
{service.type === "libsql" && (
|
||||
<LibsqlIcon className="h-6 w-6" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</CardTitle>
|
||||
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import { HelpCircle, ServerOff } from "lucide-react";
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
InferGetServerSidePropsType,
|
||||
} from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import superjson from "superjson";
|
||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||
import { ShowExternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-external-libsql-credentials";
|
||||
import { ShowGeneralLibsql } from "@/components/dashboard/libsql/general/show-general-libsql";
|
||||
import { ShowInternalLibsqlCredentials } from "@/components/dashboard/libsql/general/show-internal-libsql-credentials";
|
||||
import { UpdateLibsql } from "@/components/dashboard/libsql/update-libsql";
|
||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { LibsqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||
|
||||
const Libsql = (
|
||||
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
||||
) => {
|
||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||
|
||||
const { libsqlId, activeTab } = props;
|
||||
const router = useRouter();
|
||||
const { projectId, environmentId } = router.query;
|
||||
const [tab, setSab] = useState<TabState>(activeTab);
|
||||
const { data } = api.libsql.one.useQuery({ libsqlId });
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="libsql" />
|
||||
<AdvanceBreadcrumb />
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
||||
Dokploy
|
||||
</title>
|
||||
</Head>
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="flex flex-row justify-between items-center">
|
||||
<div className="flex flex-col">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<div className="relative flex flex-row gap-4">
|
||||
<div className="absolute -right-1 -top-2">
|
||||
<StatusTooltip status={data?.applicationStatus} />
|
||||
</div>
|
||||
|
||||
<LibsqlIcon className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
{data?.name}
|
||||
</CardTitle>
|
||||
{data?.description && (
|
||||
<CardDescription>{data?.description}</CardDescription>
|
||||
)}
|
||||
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{data?.appName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col h-fit w-fit gap-2">
|
||||
<div className="flex flex-row h-fit w-fit gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
!data?.serverId
|
||||
? "default"
|
||||
: data?.server?.serverStatus === "active"
|
||||
? "default"
|
||||
: "destructive"
|
||||
}
|
||||
>
|
||||
{data?.server?.name || "Dokploy Server"}
|
||||
</Badge>
|
||||
{data?.server?.serverStatus === "inactive" && (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<span>
|
||||
You cannot, deploy this application because the
|
||||
server is inactive, please upgrade your plan to add
|
||||
more servers.
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 justify-end">
|
||||
<UpdateLibsql libsqlId={libsqlId} />
|
||||
{(auth?.role === "owner" || auth?.canDeleteServices) && (
|
||||
<DeleteService id={libsqlId} type="libsql" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
{data?.server?.serverStatus === "inactive" ? (
|
||||
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
This service is hosted on the server {data.server.name},
|
||||
but this server has been disabled because your current
|
||||
plan doesn't include enough servers. Please purchase more
|
||||
servers to regain access to this application.
|
||||
</span>
|
||||
<span className="text-center text-base text-muted-foreground">
|
||||
Go to{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/billing"
|
||||
className="text-primary"
|
||||
>
|
||||
Billing
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs
|
||||
value={tab}
|
||||
defaultValue="general"
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
setSab(e as TabState);
|
||||
const newPath = `/dashboard/project/${projectId}/environment/${environmentId}/services/libsql/${libsqlId}?tab=${e}`;
|
||||
|
||||
router.push(newPath, undefined, { shallow: true });
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
|
||||
<TabsList
|
||||
className={cn(
|
||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||
isCloud && data?.serverId
|
||||
? "md:grid-cols-6"
|
||||
: data?.serverId
|
||||
? "md:grid-cols-5"
|
||||
: "md:grid-cols-6",
|
||||
)}
|
||||
>
|
||||
<TabsTrigger value="general">General</TabsTrigger>
|
||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
{((data?.serverId && isCloud) || !data?.server) && (
|
||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="general">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowGeneralLibsql libsqlId={libsqlId} />
|
||||
<ShowInternalLibsqlCredentials libsqlId={libsqlId} />
|
||||
<ShowExternalLibsqlCredentials libsqlId={libsqlId} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowEnvironment id={libsqlId} type="libsql" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="monitoring">
|
||||
<div className="pt-2.5">
|
||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||
{data?.serverId && isCloud ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||
token={
|
||||
data?.server?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* {monitoring?.enabledFeatures && (
|
||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||
<Label className="text-muted-foreground">
|
||||
Change Monitoring
|
||||
</Label>
|
||||
<Switch
|
||||
checked={toggleMonitoring}
|
||||
onCheckedChange={setToggleMonitoring}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toggleMonitoring ? (
|
||||
<ContainerPaidMonitoring
|
||||
appName={data?.appName || ""}
|
||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
||||
token={
|
||||
monitoring?.metricsConfig?.server?.token || ""
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div> */}
|
||||
<ContainerFreeMonitoring
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="logs">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDockerLogs
|
||||
serverId={data?.serverId || ""}
|
||||
appName={data?.appName || ""}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="backups">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowBackups
|
||||
id={libsqlId}
|
||||
databaseType="libsql"
|
||||
backupType="database"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<ShowDatabaseAdvancedSettings
|
||||
id={libsqlId}
|
||||
type="libsql"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Libsql;
|
||||
Libsql.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{
|
||||
libsqlId: string;
|
||||
activeTab: TabState;
|
||||
environmentId: string;
|
||||
}>,
|
||||
) {
|
||||
const { query, params, req, res } = ctx;
|
||||
const activeTab = query.tab;
|
||||
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
// Fetch data from external API
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session as any,
|
||||
user: user as any,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
if (typeof params?.libsqlId === "string") {
|
||||
try {
|
||||
await helpers.libsql.one.fetch({
|
||||
libsqlId: params?.libsqlId,
|
||||
});
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
libsqlId: params?.libsqlId,
|
||||
activeTab: (activeTab || "general") as TabState,
|
||||
environmentId: params?.environmentId,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/dashboard/projects",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
redirect: {
|
||||
permanent: false,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { gitProviderRouter } from "./routers/git-provider";
|
||||
import { giteaRouter } from "./routers/gitea";
|
||||
import { githubRouter } from "./routers/github";
|
||||
import { gitlabRouter } from "./routers/gitlab";
|
||||
import { libsqlRouter } from "./routers/libsql";
|
||||
import { mariadbRouter } from "./routers/mariadb";
|
||||
import { mongoRouter } from "./routers/mongo";
|
||||
import { mountRouter } from "./routers/mount";
|
||||
@@ -54,39 +55,40 @@ import { volumeBackupsRouter } from "./routers/volume-backups";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
admin: adminRouter,
|
||||
docker: dockerRouter,
|
||||
project: projectRouter,
|
||||
application: applicationRouter,
|
||||
mysql: mysqlRouter,
|
||||
postgres: postgresRouter,
|
||||
redis: redisRouter,
|
||||
mongo: mongoRouter,
|
||||
mariadb: mariadbRouter,
|
||||
compose: composeRouter,
|
||||
user: userRouter,
|
||||
domain: domainRouter,
|
||||
destination: destinationRouter,
|
||||
backup: backupRouter,
|
||||
deployment: deploymentRouter,
|
||||
previewDeployment: previewDeploymentRouter,
|
||||
mounts: mountRouter,
|
||||
certificates: certificateRouter,
|
||||
settings: settingsRouter,
|
||||
security: securityRouter,
|
||||
redirects: redirectsRouter,
|
||||
port: portRouter,
|
||||
registry: registryRouter,
|
||||
cluster: clusterRouter,
|
||||
notification: notificationRouter,
|
||||
sshKey: sshRouter,
|
||||
gitProvider: gitProviderRouter,
|
||||
gitea: giteaRouter,
|
||||
bitbucket: bitbucketRouter,
|
||||
gitlab: gitlabRouter,
|
||||
certificates: certificateRouter,
|
||||
cluster: clusterRouter,
|
||||
compose: composeRouter,
|
||||
deployment: deploymentRouter,
|
||||
destination: destinationRouter,
|
||||
docker: dockerRouter,
|
||||
domain: domainRouter,
|
||||
gitea: giteaRouter,
|
||||
gitProvider: gitProviderRouter,
|
||||
github: githubRouter,
|
||||
gitlab: gitlabRouter,
|
||||
libsql: libsqlRouter,
|
||||
mariadb: mariadbRouter,
|
||||
mongo: mongoRouter,
|
||||
mounts: mountRouter,
|
||||
mysql: mysqlRouter,
|
||||
notification: notificationRouter,
|
||||
port: portRouter,
|
||||
postgres: postgresRouter,
|
||||
previewDeployment: previewDeploymentRouter,
|
||||
project: projectRouter,
|
||||
redirects: redirectsRouter,
|
||||
redis: redisRouter,
|
||||
registry: registryRouter,
|
||||
security: securityRouter,
|
||||
server: serverRouter,
|
||||
settings: settingsRouter,
|
||||
sshKey: sshRouter,
|
||||
stripe: stripeRouter,
|
||||
swarm: swarmRouter,
|
||||
user: userRouter,
|
||||
ai: aiRouter,
|
||||
organization: organizationRouter,
|
||||
licenseKey: licenseKeyRouter,
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
findBackupById,
|
||||
findComposeByBackupId,
|
||||
findComposeById,
|
||||
findLibsqlByBackupId,
|
||||
findLibsqlById,
|
||||
findMariadbByBackupId,
|
||||
findMariadbById,
|
||||
findMongoByBackupId,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
keepLatestNBackups,
|
||||
removeBackupById,
|
||||
removeScheduleBackup,
|
||||
runLibsqlBackup,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
@@ -36,6 +39,7 @@ import {
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
restoreComposeBackup,
|
||||
restoreLibsqlBackup,
|
||||
restoreMariadbBackup,
|
||||
restoreMongoBackup,
|
||||
restoreMySqlBackup,
|
||||
@@ -82,6 +86,7 @@ export const backupRouter = createTRPCRouter({
|
||||
input.mysqlId ||
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.libsqlId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -103,6 +108,8 @@ export const backupRouter = createTRPCRouter({
|
||||
serverId = backup.mongo.serverId;
|
||||
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||
serverId = backup.mariadb.serverId;
|
||||
} else if (databaseType === "libsql" && backup.libsql?.serverId) {
|
||||
serverId = backup.libsql.serverId;
|
||||
} else if (
|
||||
backup.backupType === "compose" &&
|
||||
backup.compose?.serverId
|
||||
@@ -154,6 +161,7 @@ export const backupRouter = createTRPCRouter({
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.libsqlId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -173,6 +181,7 @@ export const backupRouter = createTRPCRouter({
|
||||
existing.mysqlId ||
|
||||
existing.mariadbId ||
|
||||
existing.mongoId ||
|
||||
existing.libsqlId ||
|
||||
existing.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -229,6 +238,7 @@ export const backupRouter = createTRPCRouter({
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.libsqlId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -400,6 +410,33 @@ export const backupRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackupLibsql: protectedProcedure
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const backup = await findBackupById(input.backupId);
|
||||
if (backup.libsqlId) {
|
||||
await checkServicePermissionAndAccess(ctx, backup.libsqlId, {
|
||||
backup: ["create"],
|
||||
});
|
||||
}
|
||||
const libsql = await findLibsqlByBackupId(backup.backupId);
|
||||
await runLibsqlBackup(libsql, backup);
|
||||
await keepLatestNBackups(backup, libsql?.serverId);
|
||||
await audit(ctx, {
|
||||
action: "run",
|
||||
resourceType: "backup",
|
||||
resourceId: backup.backupId,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error running manual Libsql backup ",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
manualBackupWebServer: withPermission("backup", "create")
|
||||
.input(apiFindOneBackup)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -536,6 +573,12 @@ export const backupRouter = createTRPCRouter({
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "libsql") {
|
||||
const libsql = await findLibsqlById(input.databaseId);
|
||||
restoreLibsqlBackup(libsql, destination, input, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
}
|
||||
if (input.databaseType === "web-server") {
|
||||
restoreWebServerBackup(destination, input.backupFile, (log) => {
|
||||
queue.push(log);
|
||||
|
||||
@@ -38,6 +38,12 @@ const filterEnvironmentServices = (
|
||||
applications: environment.applications.filter((app: any) =>
|
||||
accessedServices.includes(app.applicationId),
|
||||
),
|
||||
compose: environment.compose.filter((comp: any) =>
|
||||
accessedServices.includes(comp.composeId),
|
||||
),
|
||||
libsql: environment.libsql.filter((db: any) =>
|
||||
accessedServices.includes(db.libsqlId),
|
||||
),
|
||||
mariadb: environment.mariadb.filter((db: any) =>
|
||||
accessedServices.includes(db.mariadbId),
|
||||
),
|
||||
@@ -53,9 +59,6 @@ const filterEnvironmentServices = (
|
||||
redis: environment.redis.filter((db: any) =>
|
||||
accessedServices.includes(db.redisId),
|
||||
),
|
||||
compose: environment.compose.filter((comp: any) =>
|
||||
accessedServices.includes(comp.composeId),
|
||||
),
|
||||
});
|
||||
|
||||
export const environmentRouter = createTRPCRouter({
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
import {
|
||||
checkPortInUse,
|
||||
createLibsql,
|
||||
createMount,
|
||||
deployLibsql,
|
||||
findEnvironmentById,
|
||||
findLibsqlById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
removeLibsqlById,
|
||||
removeService,
|
||||
startService,
|
||||
startServiceRemote,
|
||||
stopService,
|
||||
stopServiceRemote,
|
||||
updateLibsqlById,
|
||||
} from "@dokploy/server";
|
||||
import {
|
||||
addNewService,
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiChangeLibsqlStatus,
|
||||
apiCreateLibsql,
|
||||
apiDeployLibsql,
|
||||
apiFindOneLibsql,
|
||||
apiRebuildLibsql,
|
||||
apiResetLibsql,
|
||||
apiSaveEnvironmentVariablesLibsql,
|
||||
apiSaveExternalPortsLibsql,
|
||||
apiUpdateLibsql,
|
||||
libsql as libsqlTable,
|
||||
} from "@/server/db/schema";
|
||||
export const libsqlRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const environment = await findEnvironmentById(input.environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
await checkServiceAccess(ctx, project.projectId, "create");
|
||||
|
||||
if (IS_CLOUD && !input.serverId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You need to use a server to create a Libsql",
|
||||
});
|
||||
}
|
||||
|
||||
if (project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this project",
|
||||
});
|
||||
}
|
||||
const newLibsql = await createLibsql({
|
||||
...input,
|
||||
});
|
||||
await addNewService(ctx, newLibsql.libsqlId);
|
||||
|
||||
await createMount({
|
||||
serviceId: newLibsql.libsqlId,
|
||||
serviceType: "libsql",
|
||||
volumeName: `${newLibsql.appName}-data`,
|
||||
mountPath: "/var/lib/sqld",
|
||||
type: "volume",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "create",
|
||||
resourceType: "service",
|
||||
resourceId: newLibsql.libsqlId,
|
||||
resourceName: newLibsql.appName,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure
|
||||
.input(apiFindOneLibsql)
|
||||
.query(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.libsqlId, "read");
|
||||
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
if (
|
||||
libsql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this Libsql",
|
||||
});
|
||||
}
|
||||
return libsql;
|
||||
}),
|
||||
|
||||
start: protectedProcedure
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
|
||||
if (libsql.serverId) {
|
||||
await startServiceRemote(libsql.serverId, libsql.appName);
|
||||
} else {
|
||||
await startService(libsql.appName);
|
||||
}
|
||||
await updateLibsqlById(input.libsqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "start",
|
||||
resourceType: "service",
|
||||
resourceId: libsql.libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
return libsql;
|
||||
}),
|
||||
stop: protectedProcedure
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
|
||||
if (libsql.serverId) {
|
||||
await stopServiceRemote(libsql.serverId, libsql.appName);
|
||||
} else {
|
||||
await stopService(libsql.appName);
|
||||
}
|
||||
await updateLibsqlById(input.libsqlId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
await audit(ctx, {
|
||||
action: "stop",
|
||||
resourceType: "service",
|
||||
resourceId: libsql.libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
return libsql;
|
||||
}),
|
||||
saveExternalPorts: protectedProcedure
|
||||
.input(apiSaveExternalPortsLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
|
||||
if (libsql.sqldNode === "replica" && input.externalGRPCPort !== null) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "externalGRPCPort cannot be set when sqldNode is 'replica'",
|
||||
});
|
||||
}
|
||||
|
||||
const portsToCheck = [
|
||||
{
|
||||
port: input.externalPort,
|
||||
name: "externalPort",
|
||||
current: libsql.externalPort,
|
||||
},
|
||||
{
|
||||
port: input.externalGRPCPort,
|
||||
name: "externalGRPCPort",
|
||||
current: libsql.externalGRPCPort,
|
||||
},
|
||||
{
|
||||
port: input.externalAdminPort,
|
||||
name: "externalAdminPort",
|
||||
current: libsql.externalAdminPort,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { port, name, current } of portsToCheck) {
|
||||
if (port && port !== current) {
|
||||
const portCheck = await checkPortInUse(
|
||||
port,
|
||||
libsql.serverId || undefined,
|
||||
);
|
||||
if (portCheck.isInUse) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: `Port ${port} (${name}) is already in use by ${portCheck.conflictingContainer}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await updateLibsqlById(input.libsqlId, {
|
||||
externalPort: input.externalPort,
|
||||
externalGRPCPort: input.externalGRPCPort,
|
||||
externalAdminPort: input.externalAdminPort,
|
||||
});
|
||||
await deployLibsql(input.libsqlId);
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: libsql.libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
return libsql;
|
||||
}),
|
||||
deploy: protectedProcedure
|
||||
.input(apiDeployLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
await audit(ctx, {
|
||||
action: "deploy",
|
||||
resourceType: "service",
|
||||
resourceId: libsql.libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
return deployLibsql(input.libsqlId);
|
||||
}),
|
||||
deployWithLogs: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
path: "/deploy/libsql-with-logs",
|
||||
method: "POST",
|
||||
override: true,
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
.input(apiDeployLibsql)
|
||||
.subscription(async function* ({ input, ctx, signal }) {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const queue: string[] = [];
|
||||
const done = false;
|
||||
|
||||
deployLibsql(input.libsqlId, (log) => {
|
||||
queue.push(log);
|
||||
});
|
||||
|
||||
while (!done || queue.length > 0) {
|
||||
if (queue.length > 0) {
|
||||
yield queue.shift()!;
|
||||
} else {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
changeStatus: protectedProcedure
|
||||
.input(apiChangeLibsqlStatus)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
await updateLibsqlById(input.libsqlId, {
|
||||
applicationStatus: input.applicationStatus,
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: libsql.libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
return libsql;
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiFindOneLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServiceAccess(ctx, input.libsqlId, "delete");
|
||||
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
|
||||
if (
|
||||
libsql.environment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this Libsql",
|
||||
});
|
||||
}
|
||||
await audit(ctx, {
|
||||
action: "delete",
|
||||
resourceType: "service",
|
||||
resourceId: libsql.libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
const cleanupOperations = [
|
||||
async () => await removeService(libsql?.appName, libsql.serverId),
|
||||
async () => await removeLibsqlById(input.libsqlId),
|
||||
];
|
||||
|
||||
for (const operation of cleanupOperations) {
|
||||
try {
|
||||
await operation();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return libsql;
|
||||
}),
|
||||
saveEnvironment: protectedProcedure
|
||||
.input(apiSaveEnvironmentVariablesLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
envVars: ["write"],
|
||||
});
|
||||
const service = await updateLibsqlById(input.libsqlId, {
|
||||
env: input.env,
|
||||
});
|
||||
|
||||
if (!service) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error adding environment variables",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: input.libsqlId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
reload: protectedProcedure
|
||||
.input(apiResetLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
const libsql = await findLibsqlById(input.libsqlId);
|
||||
if (libsql.serverId) {
|
||||
await stopServiceRemote(libsql.serverId, libsql.appName);
|
||||
} else {
|
||||
await stopService(libsql.appName);
|
||||
}
|
||||
await updateLibsqlById(input.libsqlId, {
|
||||
applicationStatus: "idle",
|
||||
});
|
||||
|
||||
if (libsql.serverId) {
|
||||
await startServiceRemote(libsql.serverId, libsql.appName);
|
||||
} else {
|
||||
await startService(libsql.appName);
|
||||
}
|
||||
await updateLibsqlById(input.libsqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
await audit(ctx, {
|
||||
action: "reload",
|
||||
resourceType: "service",
|
||||
resourceId: libsql.libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { libsqlId, ...rest } = input;
|
||||
await checkServicePermissionAndAccess(ctx, libsqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
const libsql = await updateLibsqlById(libsqlId, {
|
||||
...rest,
|
||||
});
|
||||
|
||||
if (!libsql) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error updating Libsql",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "update",
|
||||
resourceType: "service",
|
||||
resourceId: libsqlId,
|
||||
resourceName: libsql.appName,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
move: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
libsqlId: z.string(),
|
||||
targetEnvironmentId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
service: ["create"],
|
||||
});
|
||||
|
||||
const updatedLibsql = await db
|
||||
.update(libsqlTable)
|
||||
.set({
|
||||
environmentId: input.targetEnvironmentId,
|
||||
})
|
||||
.where(eq(libsqlTable.libsqlId, input.libsqlId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!updatedLibsql) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to move libsql",
|
||||
});
|
||||
}
|
||||
|
||||
await audit(ctx, {
|
||||
action: "move",
|
||||
resourceType: "service",
|
||||
resourceId: updatedLibsql.libsqlId,
|
||||
resourceName: updatedLibsql.appName,
|
||||
});
|
||||
return updatedLibsql;
|
||||
}),
|
||||
rebuild: protectedProcedure
|
||||
.input(apiRebuildLibsql)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkServicePermissionAndAccess(ctx, input.libsqlId, {
|
||||
deployment: ["create"],
|
||||
});
|
||||
|
||||
await rebuildDatabase(input.libsqlId, "libsql");
|
||||
await audit(ctx, {
|
||||
action: "rebuild",
|
||||
resourceType: "service",
|
||||
resourceId: input.libsqlId,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
deleteMount,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findLibsqlById,
|
||||
findMariadbById,
|
||||
findMongoById,
|
||||
findMountById,
|
||||
@@ -13,13 +14,14 @@ import {
|
||||
getServiceContainer,
|
||||
updateMount,
|
||||
} from "@dokploy/server";
|
||||
import type { ServiceType } from "@dokploy/server/db/schema/mount";
|
||||
import {
|
||||
checkServiceAccess,
|
||||
checkServicePermissionAndAccess,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import type { ServiceType } from "@dokploy/server/db/schema/mount";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateMount,
|
||||
apiFindMountByApplicationId,
|
||||
@@ -28,7 +30,6 @@ import {
|
||||
apiUpdateMount,
|
||||
} from "@/server/db/schema";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
|
||||
async function getServiceOrganizationId(
|
||||
serviceId: string,
|
||||
@@ -63,6 +64,10 @@ async function getServiceOrganizationId(
|
||||
const compose = await findComposeById(serviceId);
|
||||
return compose?.environment?.project?.organizationId ?? null;
|
||||
}
|
||||
case "libsql": {
|
||||
const libsql = await findLibsqlById(serviceId);
|
||||
return libsql?.environment?.project?.organizationId ?? null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -95,6 +100,7 @@ export const mountRouter = createTRPCRouter({
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.libsqlId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -120,6 +126,7 @@ export const mountRouter = createTRPCRouter({
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.libsqlId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -139,6 +146,7 @@ export const mountRouter = createTRPCRouter({
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.libsqlId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -169,7 +177,6 @@ export const mountRouter = createTRPCRouter({
|
||||
listByServiceId: protectedProcedure
|
||||
.input(apiFindMountByApplicationId)
|
||||
.query(async ({ input, ctx }) => {
|
||||
console.log("input", input);
|
||||
await checkServiceAccess(ctx, input.serviceId, "read");
|
||||
const organizationId = await getServiceOrganizationId(
|
||||
input.serviceId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createBackup,
|
||||
createCompose,
|
||||
createDomain,
|
||||
createLibsql,
|
||||
createMariadb,
|
||||
createMongo,
|
||||
createMount,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findEnvironmentById,
|
||||
findLibsqlById,
|
||||
findMariadbById,
|
||||
findMongoById,
|
||||
findMySqlById,
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
IS_CLOUD,
|
||||
updateProjectById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewEnvironment,
|
||||
addNewProject,
|
||||
@@ -35,17 +38,16 @@ import {
|
||||
checkProjectAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "@/server/api/trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import {
|
||||
apiCreateProject,
|
||||
apiFindOneProject,
|
||||
@@ -54,6 +56,7 @@ import {
|
||||
applications,
|
||||
compose,
|
||||
environments,
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -138,6 +141,9 @@ export const projectRouter = createTRPCRouter({
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
libsql: {
|
||||
where: buildServiceFilter(libsql.libsqlId, accessedServices),
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(
|
||||
mariadb.mariadbId,
|
||||
@@ -227,6 +233,14 @@ export const projectRouter = createTRPCRouter({
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
where: buildServiceFilter(libsql.libsqlId, accessedServices),
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
columns: {
|
||||
@@ -338,6 +352,11 @@ export const projectRouter = createTRPCRouter({
|
||||
composeStatus: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
@@ -450,6 +469,17 @@ export const projectRouter = createTRPCRouter({
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -622,12 +652,13 @@ export const projectRouter = createTRPCRouter({
|
||||
id: z.string(),
|
||||
type: z.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"compose",
|
||||
"libsql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"redis",
|
||||
"compose",
|
||||
]),
|
||||
}),
|
||||
)
|
||||
@@ -771,21 +802,27 @@ export const projectRouter = createTRPCRouter({
|
||||
|
||||
break;
|
||||
}
|
||||
case "postgres": {
|
||||
const { postgresId, mounts, backups, appName, ...postgres } =
|
||||
await findPostgresById(id);
|
||||
case "compose": {
|
||||
const {
|
||||
composeId,
|
||||
mounts,
|
||||
domains,
|
||||
appName,
|
||||
refreshToken,
|
||||
...compose
|
||||
} = await findComposeById(id);
|
||||
|
||||
const newAppName = appName.substring(
|
||||
0,
|
||||
appName.lastIndexOf("-"),
|
||||
);
|
||||
|
||||
const newPostgres = await createPostgres({
|
||||
...postgres,
|
||||
const newCompose = await createCompose({
|
||||
...compose,
|
||||
appName: newAppName,
|
||||
name: input.duplicateInSameProject
|
||||
? `${postgres.name} (copy)`
|
||||
: postgres.name,
|
||||
? `${compose.name} (copy)`
|
||||
: compose.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
@@ -793,18 +830,49 @@ export const projectRouter = createTRPCRouter({
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newPostgres.postgresId,
|
||||
serviceType: "postgres",
|
||||
serviceId: newCompose.composeId,
|
||||
serviceType: "compose",
|
||||
});
|
||||
}
|
||||
|
||||
for (const backup of backups) {
|
||||
const { backupId, appName: _appName, ...rest } = backup;
|
||||
await createBackup({
|
||||
for (const domain of domains) {
|
||||
const { domainId, ...rest } = domain;
|
||||
await createDomain({
|
||||
...rest,
|
||||
postgresId: newPostgres.postgresId,
|
||||
composeId: newCompose.composeId,
|
||||
domainType: "compose",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "libsql": {
|
||||
const { libsqlId, mounts, appName, ...libsql } =
|
||||
await findLibsqlById(id);
|
||||
|
||||
const newAppName = appName.substring(
|
||||
0,
|
||||
appName.lastIndexOf("-"),
|
||||
);
|
||||
|
||||
const newLibsql = await createLibsql({
|
||||
...libsql,
|
||||
appName: newAppName,
|
||||
name: input.duplicateInSameProject
|
||||
? `${libsql.name} (copy)`
|
||||
: libsql.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newLibsql.libsqlId,
|
||||
serviceType: "libsql",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "mariadb": {
|
||||
@@ -915,6 +983,42 @@ export const projectRouter = createTRPCRouter({
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "postgres": {
|
||||
const { postgresId, mounts, backups, appName, ...postgres } =
|
||||
await findPostgresById(id);
|
||||
|
||||
const newAppName = appName.substring(
|
||||
0,
|
||||
appName.lastIndexOf("-"),
|
||||
);
|
||||
|
||||
const newPostgres = await createPostgres({
|
||||
...postgres,
|
||||
appName: newAppName,
|
||||
name: input.duplicateInSameProject
|
||||
? `${postgres.name} (copy)`
|
||||
: postgres.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newPostgres.postgresId,
|
||||
serviceType: "postgres",
|
||||
});
|
||||
}
|
||||
|
||||
for (const backup of backups) {
|
||||
const { backupId, ...rest } = backup;
|
||||
await createBackup({
|
||||
...rest,
|
||||
postgresId: newPostgres.postgresId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "redis": {
|
||||
const { redisId, mounts, appName, ...redis } =
|
||||
await findRedisById(id);
|
||||
@@ -942,50 +1046,6 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "compose": {
|
||||
const {
|
||||
composeId,
|
||||
mounts,
|
||||
domains,
|
||||
appName,
|
||||
refreshToken,
|
||||
...compose
|
||||
} = await findComposeById(id);
|
||||
|
||||
const newAppName = appName.substring(
|
||||
0,
|
||||
appName.lastIndexOf("-"),
|
||||
);
|
||||
|
||||
const newCompose = await createCompose({
|
||||
...compose,
|
||||
appName: newAppName,
|
||||
name: input.duplicateInSameProject
|
||||
? `${compose.name} (copy)`
|
||||
: compose.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
for (const mount of mounts) {
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newCompose.composeId,
|
||||
serviceType: "compose",
|
||||
});
|
||||
}
|
||||
|
||||
for (const domain of domains) {
|
||||
const { domainId, ...rest } = domain;
|
||||
await createDomain({
|
||||
...rest,
|
||||
composeId: newCompose.composeId,
|
||||
domainType: "compose",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,6 +648,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
"postgres",
|
||||
"redis",
|
||||
"mongo",
|
||||
"libsql",
|
||||
"mariadb",
|
||||
"sshRouter",
|
||||
"gitProvider",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
updateVolumeBackupSchema,
|
||||
volumeBackups,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import {
|
||||
execAsyncRemote,
|
||||
execAsyncStream,
|
||||
@@ -25,7 +26,6 @@ import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const volumeBackupsRouter = createTRPCRouter({
|
||||
@@ -41,6 +41,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]),
|
||||
}),
|
||||
)
|
||||
@@ -58,6 +59,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
mongo: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
},
|
||||
orderBy: [desc(volumeBackups.createdAt)],
|
||||
});
|
||||
@@ -72,6 +74,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.redisId ||
|
||||
input.libsqlId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -113,6 +116,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.libsqlId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -136,6 +140,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.libsqlId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -161,6 +166,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
existingVb.mariadbId ||
|
||||
existingVb.mongoId ||
|
||||
existingVb.redisId ||
|
||||
existingVb.libsqlId ||
|
||||
existingVb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -220,6 +226,7 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.libsqlId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
keepLatestNBackups,
|
||||
runCommand,
|
||||
runComposeBackup,
|
||||
runLibsqlBackup,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
@@ -38,6 +39,7 @@ export const runJobs = async (job: QueueJob) => {
|
||||
mysql,
|
||||
mongo,
|
||||
mariadb,
|
||||
libsql,
|
||||
compose,
|
||||
backupType,
|
||||
} = backup;
|
||||
@@ -75,6 +77,14 @@ export const runJobs = async (job: QueueJob) => {
|
||||
}
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, server.serverId);
|
||||
} else if (databaseType === "libsql" && libsql) {
|
||||
const server = await findServerById(libsql.serverId as string);
|
||||
if (server.serverStatus === "inactive") {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await runLibsqlBackup(libsql, backup);
|
||||
await keepLatestNBackups(backup, server.serverId);
|
||||
}
|
||||
} else if (backupType === "compose" && compose) {
|
||||
const server = await findServerById(compose.serverId as string);
|
||||
@@ -141,6 +151,7 @@ export const initializeJobs = async () => {
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import Docker from "dockerode";
|
||||
|
||||
export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
||||
|
||||
export const DOKPLOY_DOCKER_API_VERSION =
|
||||
process.env.DOKPLOY_DOCKER_API_VERSION;
|
||||
export const DOKPLOY_DOCKER_HOST = process.env.DOKPLOY_DOCKER_HOST;
|
||||
@@ -10,20 +12,79 @@ export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
|
||||
: undefined;
|
||||
|
||||
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
||||
export const docker = new Docker({
|
||||
...(DOKPLOY_DOCKER_API_VERSION && {
|
||||
version: DOKPLOY_DOCKER_API_VERSION,
|
||||
}),
|
||||
...(DOKPLOY_DOCKER_HOST && {
|
||||
host: DOKPLOY_DOCKER_HOST,
|
||||
}),
|
||||
...(DOKPLOY_DOCKER_PORT && {
|
||||
port: DOKPLOY_DOCKER_PORT,
|
||||
}),
|
||||
});
|
||||
|
||||
type DockerSocketCandidate = {
|
||||
label: string;
|
||||
path: string;
|
||||
};
|
||||
|
||||
const getDockerConfig = (): Docker => {
|
||||
const versionOption = DOKPLOY_DOCKER_API_VERSION
|
||||
? { version: DOKPLOY_DOCKER_API_VERSION }
|
||||
: {};
|
||||
|
||||
// Explicit remote Docker host configuration
|
||||
if (DOKPLOY_DOCKER_HOST) {
|
||||
console.info(
|
||||
`Using remote Docker host: ${DOKPLOY_DOCKER_HOST}${DOKPLOY_DOCKER_PORT ? `:${DOKPLOY_DOCKER_PORT}` : ""}`,
|
||||
);
|
||||
return new Docker({
|
||||
host: DOKPLOY_DOCKER_HOST,
|
||||
...(DOKPLOY_DOCKER_PORT && { port: DOKPLOY_DOCKER_PORT }),
|
||||
...versionOption,
|
||||
});
|
||||
}
|
||||
|
||||
// Local socket auto-detection (Rancher Desktop, Colima, standard Docker)
|
||||
const dockerSocketCandidates: Array<DockerSocketCandidate> = [];
|
||||
|
||||
if (process.env.DOCKER_HOST) {
|
||||
dockerSocketCandidates.push({
|
||||
label: "DOCKER_HOST environment variable",
|
||||
path: process.env.DOCKER_HOST.replace("unix://", ""),
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.HOME) {
|
||||
dockerSocketCandidates.push({
|
||||
label: "Rancher Desktop socket",
|
||||
path: `${process.env.HOME}/.rd/docker.sock`,
|
||||
});
|
||||
}
|
||||
|
||||
dockerSocketCandidates.push({
|
||||
label: "Standard Docker socket",
|
||||
path: "/var/run/docker.sock",
|
||||
});
|
||||
|
||||
for (const candidate of dockerSocketCandidates) {
|
||||
try {
|
||||
if (candidate.path && fs.existsSync(candidate.path)) {
|
||||
console.info(
|
||||
`Using Docker socket (${candidate.label}): ${candidate.path}`,
|
||||
);
|
||||
return new Docker({
|
||||
socketPath: candidate.path,
|
||||
...versionOption,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.info(
|
||||
`Docker socket initialization failed for ${candidate.label} (${candidate.path}): ${e instanceof Error ? e.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.info(
|
||||
"Using default Docker configuration. You can set the DOCKER_HOST environment variable to specify a custom Docker socket path.",
|
||||
);
|
||||
return new Docker({ ...versionOption });
|
||||
};
|
||||
|
||||
export const docker = getDockerConfig();
|
||||
|
||||
// When not set, use the legacy default so 2FA remains working for users who
|
||||
// enabled it before BETTER_AUTH_SECRET was introduced .
|
||||
// enabled it before BETTER_AUTH_SECRET was introduced.
|
||||
export const BETTER_AUTH_SECRET =
|
||||
process.env.BETTER_AUTH_SECRET || "better-auth-secret-123456789";
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { generateAppName } from ".";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -26,6 +27,7 @@ export const databaseType = pgEnum("databaseType", [
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||
@@ -74,6 +76,9 @@ export const backups = pgTable("backup", {
|
||||
mongoId: text("mongoId").references((): AnyPgColumn => mongo.mongoId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references((): AnyPgColumn => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
userId: text("userId").references(() => user.id),
|
||||
// Only for compose backups
|
||||
metadata: jsonb("metadata").$type<
|
||||
@@ -118,6 +123,10 @@ export const backupsRelations = relations(backups, ({ one, many }) => ({
|
||||
fields: [backups.mongoId],
|
||||
references: [mongo.mongoId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [backups.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [backups.userId],
|
||||
references: [user.id],
|
||||
@@ -137,11 +146,19 @@ const createSchema = createInsertSchema(backups, {
|
||||
database: z.string().min(1),
|
||||
schedule: z.string(),
|
||||
keepLatestCount: z.number().optional(),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]),
|
||||
postgresId: z.string().optional(),
|
||||
mariadbId: z.string().optional(),
|
||||
mysqlId: z.string().optional(),
|
||||
mongoId: z.string().optional(),
|
||||
libsqlId: z.string().optional(),
|
||||
userId: z.string().optional(),
|
||||
metadata: z.any().optional(),
|
||||
});
|
||||
@@ -157,6 +174,7 @@ export const apiCreateBackup = createSchema.pick({
|
||||
mysqlId: true,
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
libsqlId: true,
|
||||
databaseType: true,
|
||||
userId: true,
|
||||
backupType: true,
|
||||
@@ -192,7 +210,14 @@ export const apiUpdateBackup = createSchema
|
||||
|
||||
export const apiRestoreBackup = z.object({
|
||||
databaseId: z.string(),
|
||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
databaseName: z.string().min(1),
|
||||
backupFile: z.string().min(1),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -36,12 +37,13 @@ export const environmentRelations = relations(
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
mariadb: many(mariadb),
|
||||
postgres: many(postgres),
|
||||
mysql: many(mysql),
|
||||
redis: many(redis),
|
||||
mongo: many(mongo),
|
||||
compose: many(compose),
|
||||
libsql: many(libsql),
|
||||
mariadb: many(mariadb),
|
||||
mongo: many(mongo),
|
||||
mysql: many(mysql),
|
||||
postgres: many(postgres),
|
||||
redis: many(redis),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export * from "./git-provider";
|
||||
export * from "./gitea";
|
||||
export * from "./github";
|
||||
export * from "./gitlab";
|
||||
export * from "./libsql";
|
||||
export * from "./mariadb";
|
||||
export * from "./mongo";
|
||||
export * from "./mount";
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
boolean,
|
||||
integer,
|
||||
json,
|
||||
pgTable,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { backups } from "./backups";
|
||||
import { environments } from "./environment";
|
||||
import { mounts } from "./mount";
|
||||
import { server } from "./server";
|
||||
import {
|
||||
applicationStatus,
|
||||
type EndpointSpecSwarm,
|
||||
EndpointSpecSwarmSchema,
|
||||
type HealthCheckSwarm,
|
||||
HealthCheckSwarmSchema,
|
||||
type LabelsSwarm,
|
||||
LabelsSwarmSchema,
|
||||
type NetworkSwarm,
|
||||
NetworkSwarmSchema,
|
||||
type PlacementSwarm,
|
||||
PlacementSwarmSchema,
|
||||
type RestartPolicySwarm,
|
||||
RestartPolicySwarmSchema,
|
||||
type ServiceModeSwarm,
|
||||
ServiceModeSwarmSchema,
|
||||
sqldNode,
|
||||
type UpdateConfigSwarm,
|
||||
UpdateConfigSwarmSchema,
|
||||
} from "./shared";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const libsql = pgTable("libsql", {
|
||||
libsqlId: text("libsqlId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull(),
|
||||
appName: text("appName")
|
||||
.notNull()
|
||||
.$defaultFn(() => generateAppName("libsql"))
|
||||
.unique(),
|
||||
description: text("description"),
|
||||
databaseUser: text("databaseUser").notNull(),
|
||||
databasePassword: text("databasePassword").notNull(),
|
||||
sqldNode: sqldNode("sqldNode").notNull().default("primary"),
|
||||
sqldPrimaryUrl: text("sqldPrimaryUrl"),
|
||||
enableNamespaces: boolean("enableNamespaces").notNull().default(false),
|
||||
dockerImage: text("dockerImage").notNull(),
|
||||
command: text("command"),
|
||||
env: text("env"),
|
||||
// RESOURCES
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
cpuLimit: text("cpuLimit"),
|
||||
//
|
||||
externalPort: integer("externalPort"),
|
||||
externalGRPCPort: integer("externalGRPCPort"),
|
||||
externalAdminPort: integer("externalAdminPort"),
|
||||
applicationStatus: applicationStatus("applicationStatus")
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
|
||||
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
|
||||
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
|
||||
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
|
||||
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
|
||||
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
|
||||
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
|
||||
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
|
||||
endpointSpecSwarm: json("endpointSpecSwarm").$type<EndpointSpecSwarm>(),
|
||||
replicas: integer("replicas").default(1).notNull(),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
|
||||
environmentId: text("environmentId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
serverId: text("serverId").references(() => server.serverId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const libsqlRelations = relations(libsql, ({ one, many }) => ({
|
||||
environment: one(environments, {
|
||||
fields: [libsql.environmentId],
|
||||
references: [environments.environmentId],
|
||||
}),
|
||||
backups: many(backups),
|
||||
mounts: many(mounts),
|
||||
server: one(server, {
|
||||
fields: [libsql.serverId],
|
||||
references: [server.serverId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(libsql, {
|
||||
libsqlId: z.string(),
|
||||
name: z.string().min(1),
|
||||
appName: z.string().min(1),
|
||||
createdAt: z.string(),
|
||||
databaseUser: z.string().min(1),
|
||||
databasePassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
}),
|
||||
sqldNode: z.enum(sqldNode.enumValues),
|
||||
sqldPrimaryUrl: z.string().nullable(),
|
||||
enableNamespaces: z.boolean().default(false),
|
||||
dockerImage: z
|
||||
.string()
|
||||
.default("ghcr.io/tursodatabase/libsql-server:v0.24.32"),
|
||||
command: z.string().optional(),
|
||||
env: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
memoryLimit: z.string().optional(),
|
||||
cpuReservation: z.string().optional(),
|
||||
cpuLimit: z.string().optional(),
|
||||
environmentId: z.string(),
|
||||
applicationStatus: z.enum(["idle", "running", "done", "error"]),
|
||||
externalPort: z.number(),
|
||||
externalGRPCPort: z.number(),
|
||||
externalAdminPort: z.number(),
|
||||
description: z.string().optional(),
|
||||
serverId: z.string().optional(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
placementSwarm: PlacementSwarmSchema.nullable(),
|
||||
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
|
||||
modeSwarm: ServiceModeSwarmSchema.nullable(),
|
||||
labelsSwarm: LabelsSwarmSchema.nullable(),
|
||||
networkSwarm: NetworkSwarmSchema.nullable(),
|
||||
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
|
||||
});
|
||||
|
||||
export const apiCreateLibsql = createSchema
|
||||
.pick({
|
||||
name: true,
|
||||
appName: true,
|
||||
dockerImage: true,
|
||||
environmentId: true,
|
||||
description: true,
|
||||
databaseUser: true,
|
||||
databasePassword: true,
|
||||
sqldNode: true,
|
||||
sqldPrimaryUrl: true,
|
||||
enableNamespaces: true,
|
||||
serverId: true,
|
||||
})
|
||||
.required()
|
||||
.superRefine((data, ctx) => {
|
||||
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:
|
||||
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const apiFindOneLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiChangeLibsqlStatus = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
applicationStatus: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveEnvironmentVariablesLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
env: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiSaveExternalPortsLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
externalPort: true,
|
||||
externalGRPCPort: true,
|
||||
externalAdminPort: true,
|
||||
})
|
||||
.required({ libsqlId: true })
|
||||
.superRefine((data, ctx) => {
|
||||
if (
|
||||
data.externalPort === null &&
|
||||
data.externalGRPCPort === null &&
|
||||
data.externalAdminPort === null
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
"Either externalPort, externalGRPCPort or externalAdminPort must be provided.",
|
||||
path: ["externalPort", "externalGRPCPort", "externalAdminPort"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const apiDeployLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiResetLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
appName: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateLibsql = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
libsqlId: z.string().min(1),
|
||||
})
|
||||
.omit({ serverId: true });
|
||||
|
||||
export const apiRebuildLibsql = createSchema
|
||||
.pick({
|
||||
libsqlId: true,
|
||||
})
|
||||
.required();
|
||||
@@ -5,6 +5,7 @@ import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -19,8 +20,11 @@ export const serviceType = pgEnum("serviceType", [
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export type ServiceType = (typeof serviceType.enumValues)[number];
|
||||
|
||||
export const mountType = pgEnum("mountType", ["bind", "volume", "file"]);
|
||||
|
||||
export const mounts = pgTable("mount", {
|
||||
@@ -39,7 +43,10 @@ export const mounts = pgTable("mount", {
|
||||
() => applications.applicationId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
mariadbId: text("mariadbId").references(() => mariadb.mariadbId, {
|
||||
@@ -51,10 +58,10 @@ export const mounts = pgTable("mount", {
|
||||
mysqlId: text("mysqlId").references(() => mysql.mysqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
postgresId: text("postgresId").references(() => postgres.postgresId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
});
|
||||
@@ -64,9 +71,13 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
fields: [mounts.applicationId],
|
||||
references: [applications.applicationId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [mounts.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
compose: one(compose, {
|
||||
fields: [mounts.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [mounts.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
mariadb: one(mariadb, {
|
||||
fields: [mounts.mariadbId],
|
||||
@@ -80,14 +91,14 @@ export const MountssRelations = relations(mounts, ({ one }) => ({
|
||||
fields: [mounts.mysqlId],
|
||||
references: [mysql.mysqlId],
|
||||
}),
|
||||
postgres: one(postgres, {
|
||||
fields: [mounts.postgresId],
|
||||
references: [postgres.postgresId],
|
||||
}),
|
||||
redis: one(redis, {
|
||||
fields: [mounts.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [mounts.composeId],
|
||||
references: [compose.composeId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(mounts, {
|
||||
@@ -107,13 +118,10 @@ const createSchema = createInsertSchema(mounts, {
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]),
|
||||
});
|
||||
|
||||
export type ServiceType = NonNullable<
|
||||
z.infer<typeof createSchema>["serviceType"]
|
||||
>;
|
||||
|
||||
export const apiCreateMount = createSchema
|
||||
.pick({
|
||||
type: true,
|
||||
@@ -121,8 +129,8 @@ export const apiCreateMount = createSchema
|
||||
volumeName: true,
|
||||
content: true,
|
||||
mountPath: true,
|
||||
serviceType: true,
|
||||
filePath: true,
|
||||
serviceType: true,
|
||||
})
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
@@ -142,21 +150,12 @@ export const apiRemoveMount = createSchema
|
||||
.required();
|
||||
|
||||
export const apiFindMountByApplicationId = createSchema
|
||||
.pick({
|
||||
serviceType: true,
|
||||
})
|
||||
.required()
|
||||
.extend({
|
||||
serviceId: z.string().min(1),
|
||||
serviceType: z.enum([
|
||||
"application",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
]),
|
||||
})
|
||||
.pick({
|
||||
serviceId: true,
|
||||
serviceType: true,
|
||||
});
|
||||
|
||||
export const apiUpdateMount = createSchema.partial().extend({
|
||||
|
||||
@@ -15,6 +15,7 @@ import { applications } from "./application";
|
||||
import { certificates } from "./certificate";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { mysql } from "./mysql";
|
||||
@@ -116,6 +117,7 @@ export const serverRelations = relations(server, ({ one, many }) => ({
|
||||
relationName: "applicationBuildServer",
|
||||
}),
|
||||
compose: many(compose),
|
||||
libsql: many(libsql),
|
||||
redis: many(redis),
|
||||
mariadb: many(mariadb),
|
||||
mongo: many(mongo),
|
||||
|
||||
@@ -16,6 +16,8 @@ export const certificateType = pgEnum("certificateType", [
|
||||
|
||||
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
|
||||
|
||||
export const sqldNode = pgEnum("sqldNode", ["primary", "replica"]);
|
||||
|
||||
export interface HealthCheckSwarm {
|
||||
Test?: string[] | undefined;
|
||||
Interval?: number | undefined;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { serviceType } from "./mount";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -7,9 +8,9 @@ import { applications } from "./application";
|
||||
import { compose } from "./compose";
|
||||
import { deployments } from "./deployment";
|
||||
import { destinations } from "./destination";
|
||||
import { libsql } from "./libsql";
|
||||
import { mariadb } from "./mariadb";
|
||||
import { mongo } from "./mongo";
|
||||
import { serviceType } from "./mount";
|
||||
import { mysql } from "./mysql";
|
||||
import { postgres } from "./postgres";
|
||||
import { redis } from "./redis";
|
||||
@@ -53,6 +54,9 @@ export const volumeBackups = pgTable("volume_backup", {
|
||||
redisId: text("redisId").references(() => redis.redisId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
libsqlId: text("libsqlId").references(() => libsql.libsqlId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
composeId: text("composeId").references(() => compose.composeId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
@@ -93,6 +97,10 @@ export const volumeBackupsRelations = relations(
|
||||
fields: [volumeBackups.redisId],
|
||||
references: [redis.redisId],
|
||||
}),
|
||||
libsql: one(libsql, {
|
||||
fields: [volumeBackups.libsqlId],
|
||||
references: [libsql.libsqlId],
|
||||
}),
|
||||
compose: one(compose, {
|
||||
fields: [volumeBackups.composeId],
|
||||
references: [compose.composeId],
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
date: string;
|
||||
|
||||
@@ -22,7 +22,8 @@ export type TemplateProps = {
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "compose"
|
||||
| "libsql";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
|
||||
@@ -22,6 +22,7 @@ export * from "./services/git-provider";
|
||||
export * from "./services/gitea";
|
||||
export * from "./services/github";
|
||||
export * from "./services/gitlab";
|
||||
export * from "./services/libsql";
|
||||
export * from "./services/mariadb";
|
||||
export * from "./services/mongo";
|
||||
export * from "./services/mount";
|
||||
@@ -67,6 +68,7 @@ export * from "./utils/access-log/types";
|
||||
export * from "./utils/access-log/utils";
|
||||
export * from "./utils/backups/compose";
|
||||
export * from "./utils/backups/index";
|
||||
export * from "./utils/backups/libsql";
|
||||
export * from "./utils/backups/mariadb";
|
||||
export * from "./utils/backups/mongo";
|
||||
export * from "./utils/backups/mysql";
|
||||
|
||||
@@ -33,6 +33,7 @@ export const findBackupById = async (backupId: string) => {
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
destination: true,
|
||||
compose: true,
|
||||
},
|
||||
@@ -72,7 +73,7 @@ export const removeBackupById = async (backupId: string) => {
|
||||
|
||||
export const findBackupsByDbId = async (
|
||||
id: string,
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo",
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo" | "libsql",
|
||||
) => {
|
||||
const result = await db.query.backups.findMany({
|
||||
where: eq(backups[`${type}Id`], id),
|
||||
@@ -81,6 +82,7 @@ export const findBackupsByDbId = async (
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -169,6 +169,24 @@ export const findEnvironmentById = async (environmentId: string) => {
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
},
|
||||
});
|
||||
@@ -193,6 +211,7 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
project: true,
|
||||
},
|
||||
columns: {
|
||||
@@ -211,6 +230,7 @@ const environmentHasServices = (
|
||||
return (
|
||||
(env.applications?.length ?? 0) > 0 ||
|
||||
(env.compose?.length ?? 0) > 0 ||
|
||||
(env.libsql?.length ?? 0) > 0 ||
|
||||
(env.mariadb?.length ?? 0) > 0 ||
|
||||
(env.mongo?.length ?? 0) > 0 ||
|
||||
(env.mysql?.length ?? 0) > 0 ||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateLibsql,
|
||||
backups,
|
||||
buildAppName,
|
||||
libsql,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { generatePassword } from "@dokploy/server/templates";
|
||||
import { buildLibsql } from "@dokploy/server/utils/databases/libsql";
|
||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, getTableColumns } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
|
||||
export type Libsql = typeof libsql.$inferSelect;
|
||||
|
||||
export const createLibsql = async (input: z.infer<typeof apiCreateLibsql>) => {
|
||||
const appName = buildAppName("libsql", input.appName);
|
||||
|
||||
const valid = await validUniqueServerAppName(input.appName);
|
||||
if (!valid) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Service with this 'AppName' already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const newLibsql = await db
|
||||
.insert(libsql)
|
||||
.values({
|
||||
...input,
|
||||
databasePassword: input.databasePassword
|
||||
? input.databasePassword
|
||||
: generatePassword(),
|
||||
appName,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newLibsql) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Inserting libsql database",
|
||||
});
|
||||
}
|
||||
|
||||
return newLibsql;
|
||||
};
|
||||
|
||||
// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881
|
||||
export const findLibsqlById = async (libsqlId: string) => {
|
||||
const result = await db.query.libsql.findFirst({
|
||||
where: eq(libsql.libsqlId, libsqlId),
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
mounts: true,
|
||||
server: true,
|
||||
backups: {
|
||||
with: {
|
||||
destination: true,
|
||||
deployments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Libsql not found",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateLibsqlById = async (
|
||||
libsqlId: string,
|
||||
libsqlData: Partial<Libsql>,
|
||||
) => {
|
||||
const { appName, ...rest } = libsqlData;
|
||||
const result = await db
|
||||
.update(libsql)
|
||||
.set({
|
||||
...rest,
|
||||
})
|
||||
.where(eq(libsql.libsqlId, libsqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const removeLibsqlById = async (libsqlId: string) => {
|
||||
const result = await db
|
||||
.delete(libsql)
|
||||
.where(eq(libsql.libsqlId, libsqlId))
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const findLibsqlByBackupId = async (backupId: string) => {
|
||||
const result = await db
|
||||
.select({
|
||||
...getTableColumns(libsql),
|
||||
})
|
||||
.from(libsql)
|
||||
.innerJoin(backups, eq(libsql.libsqlId, backups.libsqlId))
|
||||
.where(eq(backups.backupId, backupId))
|
||||
.limit(1);
|
||||
|
||||
if (!result || !result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Libsql not found",
|
||||
});
|
||||
}
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const deployLibsql = async (
|
||||
libsqlId: string,
|
||||
onData?: (data: any) => void,
|
||||
) => {
|
||||
const libsql = await findLibsqlById(libsqlId);
|
||||
try {
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "running",
|
||||
});
|
||||
onData?.("Starting libsql deployment...");
|
||||
if (libsql.serverId) {
|
||||
await execAsyncRemote(
|
||||
libsql.serverId,
|
||||
`docker pull ${libsql.dockerImage}`,
|
||||
onData,
|
||||
);
|
||||
} else {
|
||||
await pullImage(libsql.dockerImage, onData);
|
||||
}
|
||||
|
||||
await buildLibsql(libsql);
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "done",
|
||||
});
|
||||
onData?.("Deployment completed successfully!");
|
||||
} catch (error) {
|
||||
onData?.(`Error: ${error}`);
|
||||
await updateLibsqlById(libsqlId, {
|
||||
applicationStatus: "error",
|
||||
});
|
||||
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error on deploy libsql${error}`,
|
||||
});
|
||||
}
|
||||
return libsql;
|
||||
};
|
||||
@@ -32,8 +32,11 @@ export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
|
||||
...(input.serviceType === "application" && {
|
||||
applicationId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "postgres" && {
|
||||
postgresId: serviceId,
|
||||
...(input.serviceType === "compose" && {
|
||||
composeId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "libsql" && {
|
||||
libsqlId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "mariadb" && {
|
||||
mariadbId: serviceId,
|
||||
@@ -44,12 +47,12 @@ export const createMount = async (input: z.infer<typeof apiCreateMount>) => {
|
||||
...(input.serviceType === "mysql" && {
|
||||
mysqlId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "postgres" && {
|
||||
postgresId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "redis" && {
|
||||
redisId: serviceId,
|
||||
}),
|
||||
...(input.serviceType === "compose" && {
|
||||
composeId: serviceId,
|
||||
}),
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
@@ -115,7 +118,16 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
compose: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -151,7 +163,7 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
postgres: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -160,7 +172,7 @@ export const findMountById = async (mountId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
redis: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
@@ -186,8 +198,11 @@ export const findMountOrganizationId = async (mountId: string) => {
|
||||
if (mount.application) {
|
||||
return mount.application.environment.project.organizationId;
|
||||
}
|
||||
if (mount.postgres) {
|
||||
return mount.postgres.environment.project.organizationId;
|
||||
if (mount.compose) {
|
||||
return mount.compose.environment.project.organizationId;
|
||||
}
|
||||
if (mount.libsql) {
|
||||
return mount.libsql.environment.project.organizationId;
|
||||
}
|
||||
if (mount.mariadb) {
|
||||
return mount.mariadb.environment.project.organizationId;
|
||||
@@ -198,13 +213,13 @@ export const findMountOrganizationId = async (mountId: string) => {
|
||||
if (mount.mysql) {
|
||||
return mount.mysql.environment.project.organizationId;
|
||||
}
|
||||
if (mount.postgres) {
|
||||
return mount.postgres.environment.project.organizationId;
|
||||
}
|
||||
if (mount.redis) {
|
||||
return mount.redis.environment.project.organizationId;
|
||||
}
|
||||
|
||||
if (mount.compose) {
|
||||
return mount.compose.environment.project.organizationId;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -248,8 +263,8 @@ export const findMountsByApplicationId = async (
|
||||
case "application":
|
||||
sqlChunks.push(eq(mounts.applicationId, serviceId));
|
||||
break;
|
||||
case "postgres":
|
||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
||||
case "libsql":
|
||||
sqlChunks.push(eq(mounts.libsqlId, serviceId));
|
||||
break;
|
||||
case "mariadb":
|
||||
sqlChunks.push(eq(mounts.mariadbId, serviceId));
|
||||
@@ -260,6 +275,9 @@ export const findMountsByApplicationId = async (
|
||||
case "mysql":
|
||||
sqlChunks.push(eq(mounts.mysqlId, serviceId));
|
||||
break;
|
||||
case "postgres":
|
||||
sqlChunks.push(eq(mounts.postgresId, serviceId));
|
||||
break;
|
||||
case "redis":
|
||||
sqlChunks.push(eq(mounts.redisId, serviceId));
|
||||
break;
|
||||
@@ -362,6 +380,10 @@ export const getBaseFilesPath = async (mountId: string) => {
|
||||
const { COMPOSE_PATH } = paths(!!mount.compose.serverId);
|
||||
appName = mount.compose.appName;
|
||||
absoluteBasePath = path.resolve(COMPOSE_PATH);
|
||||
} else if (mount.serviceType === "libsql" && mount.libsql) {
|
||||
const { APPLICATIONS_PATH } = paths(!!mount.libsql.serverId);
|
||||
absoluteBasePath = path.resolve(APPLICATIONS_PATH);
|
||||
appName = mount.libsql.appName;
|
||||
}
|
||||
directoryPath = path.join(absoluteBasePath, appName, "files");
|
||||
|
||||
@@ -391,6 +413,9 @@ export const getServerId = async (mount: MountNested) => {
|
||||
if (mount.serviceType === "compose" && mount?.compose?.serverId) {
|
||||
return mount.compose.serverId;
|
||||
}
|
||||
if (mount.serviceType === "libsql" && mount?.libsql?.serverId) {
|
||||
return mount.libsql.serverId;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
type apiCreateProject,
|
||||
applications,
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -53,12 +54,13 @@ export const findProjectById = async (projectId: string) => {
|
||||
environments: {
|
||||
with: {
|
||||
applications: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
@@ -109,6 +111,9 @@ export const validUniqueServerAppName = async (appName: string) => {
|
||||
applications: {
|
||||
where: eq(applications.appName, appName),
|
||||
},
|
||||
libsql: {
|
||||
where: eq(libsql.appName, appName),
|
||||
},
|
||||
mariadb: {
|
||||
where: eq(mariadb.appName, appName),
|
||||
},
|
||||
@@ -131,6 +136,7 @@ export const validUniqueServerAppName = async (appName: string) => {
|
||||
const nonEmptyProjects = query.filter(
|
||||
(project) =>
|
||||
project.applications.length > 0 ||
|
||||
project.libsql.length > 0 ||
|
||||
project.mariadb.length > 0 ||
|
||||
project.mongo.length > 0 ||
|
||||
project.mysql.length > 0 ||
|
||||
|
||||
@@ -80,11 +80,12 @@ export const haveActiveServices = async (serverId: string) => {
|
||||
with: {
|
||||
applications: true,
|
||||
compose: true,
|
||||
redis: true,
|
||||
libsql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,11 +96,12 @@ export const haveActiveServices = async (serverId: string) => {
|
||||
const total =
|
||||
currentServer?.applications?.length +
|
||||
currentServer?.compose?.length +
|
||||
currentServer?.redis?.length +
|
||||
currentServer?.libsql?.length +
|
||||
currentServer?.mariadb?.length +
|
||||
currentServer?.mongo?.length +
|
||||
currentServer?.mysql?.length +
|
||||
currentServer?.postgres?.length;
|
||||
currentServer?.postgres?.length +
|
||||
currentServer?.redis?.length;
|
||||
|
||||
if (total === 0) {
|
||||
return false;
|
||||
|
||||
@@ -75,6 +75,15 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@ export const initCronJobs = async () => {
|
||||
mariadb: true,
|
||||
mysql: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
user: true,
|
||||
compose: true,
|
||||
},
|
||||
@@ -116,7 +117,8 @@ const getServiceAppName = (backup: BackupSchedule): string => {
|
||||
backup.postgres?.appName ||
|
||||
backup.mysql?.appName ||
|
||||
backup.mariadb?.appName ||
|
||||
backup.mongo?.appName;
|
||||
backup.mongo?.appName ||
|
||||
backup.libsql?.appName;
|
||||
return serviceAppName || backup.appName;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||
import {
|
||||
createDeploymentBackup,
|
||||
updateDeploymentStatus,
|
||||
} from "@dokploy/server/services/deployment";
|
||||
import { findEnvironmentById } from "@dokploy/server/services/environment";
|
||||
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||
import { findProjectById } from "@dokploy/server/services/project";
|
||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
|
||||
|
||||
export const runLibsqlBackup = async (
|
||||
libsql: Libsql,
|
||||
backup: BackupSchedule,
|
||||
) => {
|
||||
const { name, environmentId, appName } = libsql;
|
||||
const environment = await findEnvironmentById(environmentId);
|
||||
const project = await findProjectById(environment.projectId);
|
||||
|
||||
const deployment = await createDeploymentBackup({
|
||||
backupId: backup.backupId,
|
||||
title: "Initializing Backup",
|
||||
description: "Initializing Backup",
|
||||
});
|
||||
const { prefix } = backup;
|
||||
const destination = backup.destination;
|
||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||
const bucketDestination = `${appName}/${normalizeS3Path(prefix)}${backupFileName}`;
|
||||
try {
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||
|
||||
const rcloneCommand = `rclone rcat ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||
|
||||
const backupCommand = getBackupCommand(
|
||||
backup,
|
||||
rcloneCommand,
|
||||
deployment.logPath,
|
||||
);
|
||||
if (libsql.serverId) {
|
||||
await execAsyncRemote(libsql.serverId, backupCommand);
|
||||
} else {
|
||||
await execAsync(backupCommand, {
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
}
|
||||
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "libsql",
|
||||
type: "success",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
} catch (error) {
|
||||
await sendDatabaseBackupNotifications({
|
||||
applicationName: name,
|
||||
projectName: project.name,
|
||||
databaseType: "libsql",
|
||||
type: "error",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error message not provided",
|
||||
organizationId: project.organizationId,
|
||||
databaseName: backup.database,
|
||||
});
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import type { Destination } from "@dokploy/server/services/destination";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { keepLatestNBackups } from ".";
|
||||
import { runComposeBackup } from "./compose";
|
||||
import { runLibsqlBackup } from "./libsql";
|
||||
import { runMariadbBackup } from "./mariadb";
|
||||
import { runMongoBackup } from "./mongo";
|
||||
import { runMySqlBackup } from "./mysql";
|
||||
@@ -19,6 +20,7 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
mysql,
|
||||
mongo,
|
||||
mariadb,
|
||||
libsql,
|
||||
compose,
|
||||
} = backup;
|
||||
scheduleJob(backupId, schedule, async () => {
|
||||
@@ -35,6 +37,9 @@ export const scheduleBackup = (backup: BackupSchedule) => {
|
||||
} else if (databaseType === "mariadb" && mariadb) {
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb.serverId);
|
||||
} else if (databaseType === "libsql" && libsql) {
|
||||
await runLibsqlBackup(libsql, backup);
|
||||
await keepLatestNBackups(backup, libsql.serverId);
|
||||
} else if (databaseType === "web-server") {
|
||||
await runWebServerBackup(backup);
|
||||
await keepLatestNBackups(backup);
|
||||
@@ -107,6 +112,10 @@ export const getMongoBackupCommand = (
|
||||
return `docker exec -i $CONTAINER_ID bash -c "set -o pipefail; mongodump -d '${database}' -u '${databaseUser}' -p '${databasePassword}' --archive --authenticationDatabase admin --gzip"`;
|
||||
};
|
||||
|
||||
export const getLibsqlBackupCommand = (database: string) => {
|
||||
return `docker exec -i $CONTAINER_ID sh -c "tar cf - -C /var/lib/sqld ${database} | gzip"`;
|
||||
};
|
||||
|
||||
export const getServiceContainerCommand = (appName: string) => {
|
||||
return `docker ps -q --filter "status=running" --filter "label=com.docker.swarm.service.name=${appName}" | head -n 1`;
|
||||
};
|
||||
@@ -123,12 +132,24 @@ export const getComposeContainerCommand = (
|
||||
};
|
||||
|
||||
const getContainerSearchCommand = (backup: BackupSchedule) => {
|
||||
const { backupType, postgres, mysql, mariadb, mongo, compose, serviceName } =
|
||||
backup;
|
||||
const {
|
||||
backupType,
|
||||
postgres,
|
||||
mysql,
|
||||
mariadb,
|
||||
mongo,
|
||||
libsql,
|
||||
compose,
|
||||
serviceName,
|
||||
} = backup;
|
||||
|
||||
if (backupType === "database") {
|
||||
const appName =
|
||||
postgres?.appName || mysql?.appName || mariadb?.appName || mongo?.appName;
|
||||
postgres?.appName ||
|
||||
mysql?.appName ||
|
||||
mariadb?.appName ||
|
||||
mongo?.appName ||
|
||||
libsql?.appName;
|
||||
return getServiceContainerCommand(appName || "");
|
||||
}
|
||||
if (backupType === "compose") {
|
||||
@@ -209,6 +230,12 @@ export const generateBackupCommand = (backup: BackupSchedule) => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "libsql": {
|
||||
if (backupType === "database") {
|
||||
return getLibsqlBackupCommand(backup.database);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Database type not supported: ${databaseType}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions, PortConfig } from "dockerode";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
generateConfigContainer,
|
||||
generateFileMounts,
|
||||
generateVolumeMounts,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getRemoteDocker } from "../servers/remote-docker";
|
||||
|
||||
export type LibsqlNested = InferResultType<
|
||||
"libsql",
|
||||
{
|
||||
mounts: true;
|
||||
environment: { with: { project: true } };
|
||||
}
|
||||
>;
|
||||
export const buildLibsql = async (libsql: LibsqlNested) => {
|
||||
const {
|
||||
appName,
|
||||
env,
|
||||
externalPort,
|
||||
externalGRPCPort,
|
||||
externalAdminPort,
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
databaseUser,
|
||||
databasePassword,
|
||||
sqldNode,
|
||||
sqldPrimaryUrl,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
command,
|
||||
mounts,
|
||||
enableNamespaces,
|
||||
} = libsql;
|
||||
|
||||
const basicAuth = Buffer.from(
|
||||
`${databaseUser}:${databasePassword}`,
|
||||
"utf-8",
|
||||
).toString("base64");
|
||||
|
||||
const defaultLibsqlEnv = `SQLD_NODE="${sqldNode}"\nSQLD_HTTP_AUTH="basic:${basicAuth}"${
|
||||
env ? `\n${env}` : ""
|
||||
}${sqldNode === "replica" ? `\nSQLD_PRIMARY_URL="${sqldPrimaryUrl}"` : ""}`;
|
||||
|
||||
const {
|
||||
HealthCheck,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Labels,
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
UpdateConfig,
|
||||
Networks,
|
||||
} = generateConfigContainer(libsql);
|
||||
const resources = calculateResources({
|
||||
memoryLimit,
|
||||
memoryReservation,
|
||||
cpuLimit,
|
||||
cpuReservation,
|
||||
});
|
||||
const envVariables = prepareEnvironmentVariables(
|
||||
defaultLibsqlEnv,
|
||||
libsql.environment.project.env,
|
||||
libsql.environment.env,
|
||||
);
|
||||
const volumesMount = generateVolumeMounts(mounts);
|
||||
const bindsMount = generateBindMounts(mounts);
|
||||
const filesMount = generateFileMounts(appName, libsql);
|
||||
|
||||
const docker = await getRemoteDocker(libsql.serverId);
|
||||
|
||||
let finalCommand =
|
||||
command ??
|
||||
"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";
|
||||
if (enableNamespaces) {
|
||||
finalCommand += " --enable-namespaces";
|
||||
}
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: "ghcr.io/tursodatabase/libsql-server:v0.24.32",
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
|
||||
...(finalCommand
|
||||
? {
|
||||
Command: ["/bin/sh"],
|
||||
Args: ["-c", finalCommand],
|
||||
}
|
||||
: {}),
|
||||
Labels,
|
||||
},
|
||||
Networks,
|
||||
RestartPolicy,
|
||||
Placement,
|
||||
Resources: {
|
||||
...resources,
|
||||
},
|
||||
},
|
||||
Mode,
|
||||
RollbackConfig,
|
||||
EndpointSpec: {
|
||||
Mode: "dnsrr",
|
||||
Ports: [
|
||||
...(externalPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 8080,
|
||||
PublishedPort: externalPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
...(externalGRPCPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 5001,
|
||||
PublishedPort: externalGRPCPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
...(externalAdminPort
|
||||
? [
|
||||
{
|
||||
Protocol: "tcp",
|
||||
TargetPort: 5000,
|
||||
PublishedPort: externalAdminPort,
|
||||
PublishMode: "host",
|
||||
} as PortConfig,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
UpdateConfig,
|
||||
};
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
});
|
||||
} catch {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
redis,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { deployLibsql } from "@dokploy/server/services/libsql";
|
||||
import { deployMariadb } from "@dokploy/server/services/mariadb";
|
||||
import { deployMongo } from "@dokploy/server/services/mongo";
|
||||
import { deployMySql } from "@dokploy/server/services/mysql";
|
||||
@@ -15,7 +17,13 @@ import { eq } from "drizzle-orm";
|
||||
import { removeService } from "../docker/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
type DatabaseType = "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
type DatabaseType =
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
|
||||
export const rebuildDatabase = async (
|
||||
databaseId: string,
|
||||
@@ -41,31 +49,25 @@ export const rebuildDatabase = async (
|
||||
}
|
||||
}
|
||||
|
||||
if (type === "postgres") {
|
||||
await deployPostgres(databaseId);
|
||||
} else if (type === "mysql") {
|
||||
await deployMySql(databaseId);
|
||||
if (type === "libsql") {
|
||||
await deployLibsql(databaseId);
|
||||
} else if (type === "mariadb") {
|
||||
await deployMariadb(databaseId);
|
||||
} else if (type === "mongo") {
|
||||
await deployMongo(databaseId);
|
||||
} else if (type === "mysql") {
|
||||
await deployMySql(databaseId);
|
||||
} else if (type === "postgres") {
|
||||
await deployPostgres(databaseId);
|
||||
} else if (type === "redis") {
|
||||
await deployRedis(databaseId);
|
||||
}
|
||||
};
|
||||
|
||||
const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
|
||||
if (type === "postgres") {
|
||||
return await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mysql") {
|
||||
return await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, databaseId),
|
||||
if (type === "libsql") {
|
||||
return await db.query.libsql.findFirst({
|
||||
where: eq(libsql.libsqlId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
@@ -87,6 +89,22 @@ const findDatabaseById = async (databaseId: string, type: DatabaseType) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "mysql") {
|
||||
return await db.query.mysql.findFirst({
|
||||
where: eq(mysql.mysqlId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "postgres") {
|
||||
return await db.query.postgres.findFirst({
|
||||
where: eq(postgres.postgresId, databaseId),
|
||||
with: {
|
||||
mounts: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (type === "redis") {
|
||||
return await db.query.redis.findFirst({
|
||||
where: eq(redis.redisId, databaseId),
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { ContainerInfo, ResourceRequirements } from "dockerode";
|
||||
import { parse } from "dotenv";
|
||||
import { quote } from "shell-quote";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
import type { LibsqlNested } from "../databases/libsql";
|
||||
import type { MariadbNested } from "../databases/mariadb";
|
||||
import type { MongoNested } from "../databases/mongo";
|
||||
import type { MysqlNested } from "../databases/mysql";
|
||||
@@ -610,6 +611,7 @@ export const generateFileMounts = (
|
||||
appName: string,
|
||||
service:
|
||||
| ApplicationNested
|
||||
| LibsqlNested
|
||||
| MongoNested
|
||||
| MariadbNested
|
||||
| MysqlNested
|
||||
|
||||
@@ -29,7 +29,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb";
|
||||
databaseType: "postgres" | "mysql" | "mongodb" | "mariadb" | "libsql";
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
|
||||
@@ -37,7 +37,8 @@ export const sendVolumeBackupNotifications = async ({
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
| "compose"
|
||||
| "libsql";
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
|
||||
@@ -32,7 +32,7 @@ export const restoreComposeBackup = async (
|
||||
rcloneCommand = `rclone copy ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||
}
|
||||
|
||||
let credentials: DatabaseCredentials;
|
||||
let credentials: DatabaseCredentials = {};
|
||||
|
||||
switch (backupInput.databaseType) {
|
||||
case "postgres":
|
||||
@@ -62,7 +62,11 @@ export const restoreComposeBackup = async (
|
||||
const restoreCommand = getRestoreCommand({
|
||||
appName: appName,
|
||||
serviceName: backupInput.metadata?.serviceName,
|
||||
type: backupInput.databaseType,
|
||||
type: backupInput.databaseType as
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mysql"
|
||||
| "mongo",
|
||||
credentials: {
|
||||
database: backupInput.databaseName,
|
||||
...credentials,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { restoreComposeBackup } from "./compose";
|
||||
export { restoreLibsqlBackup } from "./libsql";
|
||||
export { restoreMariadbBackup } from "./mariadb";
|
||||
export { restoreMongoBackup } from "./mongo";
|
||||
export { restoreMySqlBackup } from "./mysql";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { apiRestoreBackup } from "@dokploy/server/db/schema";
|
||||
import type { Destination } from "@dokploy/server/services/destination";
|
||||
import type { Libsql } from "@dokploy/server/services/libsql";
|
||||
import type { z } from "zod";
|
||||
import { getS3Credentials, getServiceContainerCommand } from "../backups/utils";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
|
||||
export const restoreLibsqlBackup = async (
|
||||
libsql: Libsql,
|
||||
destination: Destination,
|
||||
backupInput: z.infer<typeof apiRestoreBackup>,
|
||||
emit: (log: string) => void,
|
||||
) => {
|
||||
try {
|
||||
const { appName, serverId } = libsql;
|
||||
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const bucketPath = `:s3:${destination.bucket}`;
|
||||
|
||||
const backupPath = `${bucketPath}/${backupInput.backupFile}`;
|
||||
|
||||
const rcloneCommand = `rclone cat ${rcloneFlags.join(" ")} "${backupPath}"`;
|
||||
|
||||
emit("Starting restore...");
|
||||
emit(`Backup path: ${backupPath}`);
|
||||
|
||||
const containerSearch = getServiceContainerCommand(appName);
|
||||
const restoreCommand = `docker exec -i $CONTAINER_ID sh -c "tar xzf - -C /var/lib/sqld"`;
|
||||
|
||||
const command = `CONTAINER_ID=$(${containerSearch}) && ${rcloneCommand} | ${restoreCommand}`;
|
||||
|
||||
emit(`Executing command: ${command}`);
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
|
||||
emit("Restore completed successfully!");
|
||||
} catch (error) {
|
||||
emit(
|
||||
`Error: ${
|
||||
error instanceof Error ? error.message : "Error restoring libsql backup"
|
||||
}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -18,7 +18,8 @@ export const getVolumeServiceAppName = (
|
||||
volumeBackup.mysql?.appName ||
|
||||
volumeBackup.mariadb?.appName ||
|
||||
volumeBackup.mongo?.appName ||
|
||||
volumeBackup.redis?.appName;
|
||||
volumeBackup.redis?.appName ||
|
||||
volumeBackup.libsql?.appName;
|
||||
return serviceAppName || volumeBackup.appName;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const getProjectName = (
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
volumeBackup.libsql,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
@@ -48,6 +49,7 @@ const getOrganizationId = (
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
volumeBackup.libsql,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
|
||||
Reference in New Issue
Block a user