mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 13:45:23 +02:00
Compare commits
1 Commits
dosu/doc-u
...
dosu/doc-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d287e2a56d |
@@ -62,22 +62,6 @@ pnpm install
|
||||
cp apps/dokploy/.env.example apps/dokploy/.env
|
||||
```
|
||||
|
||||
### Optional Docker Configuration
|
||||
|
||||
Docker socket detection is automatic for local development. The system automatically detects and uses Docker sockets in the following order:
|
||||
|
||||
- DOCKER_HOST environment variable (if set)
|
||||
- Rancher Desktop socket (~/.rd/docker.sock)
|
||||
- Standard Docker socket (/var/run/docker.sock)
|
||||
|
||||
Contributors using Docker Desktop, Rancher Desktop, Colima, or other Docker alternatives can run `pnpm run dokploy:setup` without any additional configuration.
|
||||
|
||||
The following environment variables are only needed for remote Docker host configurations:
|
||||
|
||||
- **DOKPLOY_DOCKER_HOST**: Specify a remote Docker daemon host
|
||||
- **DOKPLOY_DOCKER_PORT**: Specify a remote Docker daemon port
|
||||
- **DOKPLOY_DOCKER_API_VERSION**: Specify which Docker API version to use (optional)
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Docker](/GUIDES.md#docker)
|
||||
@@ -187,11 +171,6 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.
|
||||
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
|
||||
- **Large Features:** Pull Requests that introduce very large or broad features **will not be accepted** unless the idea is first outlined and discussed in a GitHub issue. Large features should be designed together with the Dokploy team so the project stays coherent and moves in the same direction. Open an issue to propose and align on the design before implementing.
|
||||
|
||||
### Pull Request Guidelines
|
||||
|
||||
- **Keep PRs small and focused.** Avoid very large PRs; prefer several smaller PRs (e.g., one template or one logical change per PR). This speeds up review and keeps the history clear.
|
||||
- **Test before submitting.** Any PR that has not been tested by the contributor will be closed. This keeps the PR queue tidy and ensures that only contributions that have been verified by their authors are considered.
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
## Templates
|
||||
|
||||
@@ -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, libsql, and Redis.
|
||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||
- **Backups**: Automate backups for databases to an external storage destination.
|
||||
- **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.
|
||||
|
||||
@@ -34,29 +34,15 @@ Optional configuration for filtering and pagination:
|
||||
|
||||
### Docker Configuration
|
||||
|
||||
Dokploy automatically detects Docker sockets in the following priority order:
|
||||
|
||||
1. **DOCKER_HOST** environment variable (if set)
|
||||
2. Rancher Desktop socket (`~/.rd/docker.sock`)
|
||||
3. Standard Docker socket (`/var/run/docker.sock`)
|
||||
|
||||
This automatic detection means that Docker Desktop, Rancher Desktop, Colima, and other Docker alternatives work out-of-the-box without manual configuration.
|
||||
|
||||
**Optional Environment Variables:**
|
||||
|
||||
- **DOCKER_HOST** (optional) - Specifies a custom Docker socket path (e.g., `unix:///path/to/docker.sock`). When set, this takes priority over automatic socket detection.
|
||||
Optional configuration for customizing Docker daemon connections:
|
||||
|
||||
- **DOCKER_API_VERSION** (optional) - Specifies which Docker API version to use when connecting to the Docker daemon. If not set, the Docker client uses the default API version.
|
||||
|
||||
**Remote Docker Host Configuration:**
|
||||
- **DOKPLOY_DOCKER_HOST** (optional) - Specifies the Docker daemon host to connect to. If not set, uses the default Docker socket connection.
|
||||
|
||||
For connecting to remote Docker daemons, use the following variables:
|
||||
- **DOKPLOY_DOCKER_PORT** (optional) - Specifies the port for connecting to the Docker daemon. If not set, uses the default port.
|
||||
|
||||
- **DOKPLOY_DOCKER_HOST** (optional) - Specifies the remote Docker daemon host to connect to (e.g., `tcp://remote-host`).
|
||||
|
||||
- **DOKPLOY_DOCKER_PORT** (optional) - Specifies the port for connecting to the remote Docker daemon.
|
||||
|
||||
Note: `DOKPLOY_DOCKER_HOST` and `DOKPLOY_DOCKER_PORT` are intended for remote Docker host configurations. For local Docker installations, the automatic socket detection handles connection setup without requiring these variables.
|
||||
These variables allow advanced users to customize how the Dokploy API server connects to Docker, which can be useful for connecting to remote Docker daemons or using specific API versions.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
@@ -91,29 +77,6 @@ Returns an array of deployment job objects with the same shape as BullMQ queue j
|
||||
|
||||
This endpoint is used by the UI to display deployment queue information in the dashboard.
|
||||
|
||||
### POST /drop-deployment
|
||||
|
||||
Upload and deploy application code via ZIP file.
|
||||
|
||||
**Content-Type:** `multipart/form-data`
|
||||
|
||||
**Form Fields:**
|
||||
- `applicationId` (required) - The ID of the application to deploy
|
||||
- `zip` (required) - A ZIP file containing the application code
|
||||
- `dropBuildPath` (optional) - Custom build path within the ZIP file
|
||||
|
||||
**Response:**
|
||||
Initiates a deployment using the uploaded ZIP file.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X POST https://your-dokploy-instance.com/api/drop-deployment \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-F "applicationId=YOUR_APP_ID" \
|
||||
-F "zip=@/path/to/your/app.zip" \
|
||||
-F "dropBuildPath=optional/build/path"
|
||||
```
|
||||
|
||||
## Search Endpoints
|
||||
|
||||
The following search endpoints provide flexible querying capabilities with pagination support. All search endpoints respect member permissions, returning only resources the user has access to.
|
||||
@@ -292,21 +255,34 @@ The following database services all share the same search interface:
|
||||
|
||||
## Database Service Update Endpoints
|
||||
|
||||
All database services support update operations with flexible configuration options. The following database services share a common update interface:
|
||||
The following database services support update operations with similar parameters:
|
||||
- **postgres.update** (apiUpdatePostgres)
|
||||
- **mysql.update** (apiUpdateMySql)
|
||||
- **mariadb.update** (apiUpdateMariaDB)
|
||||
- **mongo.update** (apiUpdateMongo)
|
||||
- **redis.update** (apiUpdateRedis)
|
||||
|
||||
**Common Parameters:**
|
||||
**Input Parameters:**
|
||||
|
||||
All database update endpoints accept their respective ID field (e.g., `postgresId`, `mysqlId`, `mariadbId`, `mongoId`, `redisId`) as a required parameter, along with optional configuration fields.
|
||||
Each update endpoint accepts the following parameters (all optional except the service ID):
|
||||
|
||||
**Optional Configuration:**
|
||||
- `dockerImage` (optional string) - Specifies a custom Docker image for the database service. This allows users to use specific versions or custom-built images instead of the default image for the database type. Available for all five database services (PostgreSQL, MySQL, MariaDB, MongoDB, and Redis).
|
||||
- `postgresId` / `mysqlId` / `mariadbId` / `mongoId` / `redisId` (required) - The ID of the database service to update
|
||||
- `dockerImage` (optional string) - Custom Docker image to use for the database service
|
||||
- Other service-specific parameters as defined in the schema
|
||||
|
||||
Additional service-specific parameters are available depending on the database type. The `dockerImage` field provides enhanced configuration flexibility for advanced use cases such as version pinning or using specialized database distributions.
|
||||
**Example Input:**
|
||||
```json
|
||||
{
|
||||
"postgresId": "string",
|
||||
"dockerImage": "postgres:16-alpine"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
Returns the updated database service object.
|
||||
|
||||
**Purpose:**
|
||||
The `dockerImage` parameter allows users to specify custom Docker images for database services, providing flexibility for version selection, custom builds, or alternative distributions.
|
||||
|
||||
## Whitelabeling Endpoints
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ const ENTERPRISE_RESOURCES = [
|
||||
"domain",
|
||||
"destination",
|
||||
"notification",
|
||||
"tag",
|
||||
"logs",
|
||||
"monitoring",
|
||||
"auditLog",
|
||||
|
||||
@@ -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:
|
||||
| "application"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ import { AddSwarmSettings } from "./modify-swarm-settings";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
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,13 +65,12 @@ 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]
|
||||
@@ -106,11 +105,11 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
|
||||
const onSubmit = async (data: AddCommand) => {
|
||||
await mutateAsync({
|
||||
applicationId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
mysqlId: id || "",
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
...(type === "application"
|
||||
? {
|
||||
registryId:
|
||||
|
||||
@@ -28,14 +28,7 @@ export const endpointSpecFormSchema = z.object({
|
||||
|
||||
interface EndpointSpecFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||
@@ -51,7 +44,6 @@ 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]()
|
||||
@@ -64,7 +56,6 @@ 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]
|
||||
@@ -103,7 +94,6 @@ export const EndpointSpecForm = ({ id, type }: EndpointSpecFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
endpointSpecSwarm: hasAnyValue ? formData : null,
|
||||
});
|
||||
|
||||
|
||||
@@ -26,14 +26,7 @@ export const healthCheckFormSchema = z.object({
|
||||
|
||||
interface HealthCheckFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
@@ -49,7 +42,6 @@ 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]()
|
||||
@@ -62,7 +54,6 @@ 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]
|
||||
@@ -113,7 +104,6 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
healthCheckSwarm: hasAnyValue ? formData : null,
|
||||
});
|
||||
|
||||
|
||||
@@ -29,14 +29,7 @@ export const labelsFormSchema = z.object({
|
||||
|
||||
interface LabelsFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||
@@ -52,7 +45,6 @@ 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]()
|
||||
@@ -65,7 +57,6 @@ 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]
|
||||
@@ -121,7 +112,6 @@ export const LabelsForm = ({ id, type }: LabelsFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
labelsSwarm: labelsToSend,
|
||||
});
|
||||
|
||||
|
||||
@@ -23,14 +23,7 @@ import { api } from "@/utils/api";
|
||||
|
||||
interface ModeFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
@@ -46,7 +39,6 @@ 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]()
|
||||
@@ -59,7 +51,6 @@ 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]
|
||||
@@ -104,7 +95,6 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
modeSwarm: null,
|
||||
});
|
||||
toast.success("Mode updated successfully");
|
||||
@@ -132,7 +122,6 @@ export const ModeForm = ({ id, type }: ModeFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
modeSwarm: modeData,
|
||||
});
|
||||
|
||||
|
||||
@@ -35,14 +35,7 @@ export const networkFormSchema = z.object({
|
||||
|
||||
interface NetworkFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||
@@ -58,7 +51,6 @@ 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]()
|
||||
@@ -71,7 +63,6 @@ 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]
|
||||
@@ -141,7 +132,6 @@ export const NetworkForm = ({ id, type }: NetworkFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
networkSwarm: networksToSend,
|
||||
});
|
||||
|
||||
|
||||
@@ -34,14 +34,7 @@ export const placementFormSchema = z.object({
|
||||
|
||||
interface PlacementFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||
@@ -57,7 +50,6 @@ 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]()
|
||||
@@ -70,7 +62,6 @@ 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]
|
||||
@@ -123,7 +114,6 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
placementSwarm: hasAnyValue
|
||||
? {
|
||||
...formData,
|
||||
|
||||
@@ -32,14 +32,7 @@ export const restartPolicyFormSchema = z.object({
|
||||
|
||||
interface RestartPolicyFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||
@@ -55,7 +48,6 @@ 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]()
|
||||
@@ -68,7 +60,6 @@ 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]
|
||||
@@ -113,7 +104,6 @@ export const RestartPolicyForm = ({ id, type }: RestartPolicyFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
restartPolicySwarm: hasAnyValue ? formData : null,
|
||||
});
|
||||
|
||||
|
||||
@@ -34,14 +34,7 @@ export const rollbackConfigFormSchema = z.object({
|
||||
|
||||
interface RollbackConfigFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||
@@ -57,7 +50,6 @@ 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]()
|
||||
@@ -70,7 +62,6 @@ 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]
|
||||
@@ -112,7 +103,6 @@ export const RollbackConfigForm = ({ id, type }: RollbackConfigFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
rollbackConfigSwarm: (hasAnyValue ? formData : null) as any,
|
||||
});
|
||||
|
||||
|
||||
@@ -23,14 +23,7 @@ const hasStopGracePeriodSwarm = (
|
||||
|
||||
interface StopGracePeriodFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||
@@ -46,7 +39,6 @@ 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]()
|
||||
@@ -59,7 +51,6 @@ 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]
|
||||
@@ -97,7 +88,6 @@ export const StopGracePeriodForm = ({ id, type }: StopGracePeriodFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
stopGracePeriodSwarm: formData.value,
|
||||
});
|
||||
|
||||
|
||||
@@ -34,14 +34,7 @@ export const updateConfigFormSchema = z.object({
|
||||
|
||||
interface UpdateConfigFormProps {
|
||||
id: string;
|
||||
type:
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "redis"
|
||||
| "application"
|
||||
| "libsql";
|
||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||
}
|
||||
|
||||
export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||
@@ -57,7 +50,6 @@ 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]()
|
||||
@@ -70,7 +62,6 @@ 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]
|
||||
@@ -118,7 +109,6 @@ export const UpdateConfigForm = ({ id, type }: UpdateConfigFormProps) => {
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
mongoId: id || "",
|
||||
libsqlId: id || "",
|
||||
updateConfigSwarm: (hasAnyValue ? formData : null) as any,
|
||||
});
|
||||
|
||||
|
||||
@@ -89,13 +89,12 @@ const ULIMIT_PRESETS = [
|
||||
];
|
||||
|
||||
export type ServiceType =
|
||||
| "application"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "application";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -106,29 +105,27 @@ 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]
|
||||
@@ -158,20 +155,19 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
cpuReservation: data?.cpuReservation || undefined,
|
||||
memoryLimit: data?.memoryLimit || undefined,
|
||||
memoryReservation: data?.memoryReservation || undefined,
|
||||
ulimitsSwarm: (data as any)?.ulimitsSwarm || [],
|
||||
ulimitsSwarm: data?.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"
|
||||
| "compose"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
| "redis"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "compose";
|
||||
refetch: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -29,25 +29,23 @@ 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"
|
||||
| "compose"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
| "redis"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "compose";
|
||||
}
|
||||
|
||||
export const UpdateVolume = ({
|
||||
@@ -253,7 +253,7 @@ export const UpdateVolume = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full max-w-[45rem]">
|
||||
<FormItem className="max-w-full max-w-[45rem]">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -39,16 +39,15 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const canWrite = permissions?.envVars.write ?? false;
|
||||
const queryMap = {
|
||||
compose: () =>
|
||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||
libsql: () => api.libsql.one.useQuery({ libsqlId: id }, { enabled: !!id }),
|
||||
mariadb: () =>
|
||||
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
|
||||
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
|
||||
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
|
||||
postgres: () =>
|
||||
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 }),
|
||||
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]()
|
||||
@@ -56,13 +55,12 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
||||
const [isEnvVisible, setIsEnvVisible] = useState(true);
|
||||
|
||||
const mutationMap = {
|
||||
compose: () => api.compose.update.useMutation(),
|
||||
libsql: () => api.libsql.update.useMutation(),
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
mysql: () => api.mysql.update.useMutation(),
|
||||
postgres: () => api.postgres.update.useMutation(),
|
||||
redis: () => api.redis.update.useMutation(),
|
||||
mysql: () => api.mysql.update.useMutation(),
|
||||
mariadb: () => api.mariadb.update.useMutation(),
|
||||
mongo: () => api.mongo.update.useMutation(),
|
||||
compose: () => api.compose.update.useMutation(),
|
||||
};
|
||||
const { mutateAsync, isPending } = mutationMap[type]
|
||||
? mutationMap[type]()
|
||||
@@ -89,13 +87,12 @@ 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,7 +71,6 @@ const formSchema = z
|
||||
"mongo",
|
||||
"mysql",
|
||||
"redis",
|
||||
"libsql",
|
||||
]),
|
||||
serviceName: z.string(),
|
||||
destinationId: z.string().min(1, "Destination required"),
|
||||
|
||||
@@ -57,7 +57,6 @@ 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 }),
|
||||
@@ -73,7 +72,6 @@ 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(),
|
||||
@@ -100,7 +98,6 @@ export const DeleteService = ({ id, type }: Props) => {
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
libsqlId: id || "",
|
||||
applicationId: id || "",
|
||||
composeId: id || "",
|
||||
deleteVolumes,
|
||||
|
||||
@@ -65,13 +65,7 @@ import { ScheduleFormField } from "../../application/schedules/handle-schedules"
|
||||
|
||||
type CacheType = "cache" | "fetch";
|
||||
|
||||
type DatabaseType =
|
||||
| "postgres"
|
||||
| "mariadb"
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "web-server"
|
||||
| "libsql";
|
||||
type DatabaseType = "postgres" | "mariadb" | "mysql" | "mongo" | "web-server";
|
||||
|
||||
const Schema = z
|
||||
.object({
|
||||
@@ -83,7 +77,7 @@ const Schema = z
|
||||
keepLatestCount: z.coerce.number().optional(),
|
||||
serviceName: z.string().nullable(),
|
||||
databaseType: z
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||
.optional(),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
metadata: z
|
||||
@@ -215,12 +209,7 @@ export const HandleBackup = ({
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
database:
|
||||
databaseType === "web-server"
|
||||
? "dokploy"
|
||||
: databaseType === "libsql"
|
||||
? "iku.db"
|
||||
: "",
|
||||
database: databaseType === "web-server" ? "dokploy" : "",
|
||||
destinationId: "",
|
||||
enabled: true,
|
||||
prefix: "/",
|
||||
@@ -257,9 +246,7 @@ export const HandleBackup = ({
|
||||
? backup?.database
|
||||
: databaseType === "web-server"
|
||||
? "dokploy"
|
||||
: databaseType === "libsql"
|
||||
? "iku.db"
|
||||
: "",
|
||||
: "",
|
||||
destinationId: backup?.destinationId ?? "",
|
||||
enabled: backup?.enabled ?? true,
|
||||
prefix: backup?.prefix ?? "/",
|
||||
@@ -294,15 +281,11 @@ export const HandleBackup = ({
|
||||
? {
|
||||
mongoId: id,
|
||||
}
|
||||
: databaseType === "libsql"
|
||||
: databaseType === "web-server"
|
||||
? {
|
||||
libsqlId: id,
|
||||
userId: id,
|
||||
}
|
||||
: databaseType === "web-server"
|
||||
? {
|
||||
userId: id,
|
||||
}
|
||||
: undefined;
|
||||
: undefined;
|
||||
|
||||
await createBackup({
|
||||
destinationId: data.destinationId,
|
||||
@@ -585,10 +568,7 @@ export const HandleBackup = ({
|
||||
<FormLabel>Database</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={
|
||||
databaseType === "web-server" ||
|
||||
databaseType === "libsql"
|
||||
}
|
||||
disabled={databaseType === "web-server"}
|
||||
placeholder={"dokploy"}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
@@ -88,7 +88,7 @@ const RestoreBackupSchema = z
|
||||
message: "Database name is required",
|
||||
}),
|
||||
databaseType: z
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server", "libsql"])
|
||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||
.optional(),
|
||||
backupType: z.enum(["database", "compose"]).default("database"),
|
||||
metadata: z
|
||||
@@ -211,12 +211,7 @@ export const RestoreBackup = ({
|
||||
defaultValues: {
|
||||
destinationId: "",
|
||||
backupFile: "",
|
||||
databaseName:
|
||||
databaseType === "web-server"
|
||||
? "dokploy"
|
||||
: databaseType === "libsql"
|
||||
? "iku.db"
|
||||
: "",
|
||||
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||
databaseType:
|
||||
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
||||
backupType: backupType,
|
||||
@@ -528,10 +523,7 @@ export const RestoreBackup = ({
|
||||
<Input
|
||||
placeholder="Enter database name"
|
||||
{...field}
|
||||
disabled={
|
||||
databaseType === "web-server" ||
|
||||
databaseType === "libsql"
|
||||
}
|
||||
disabled={databaseType === "web-server"}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
@@ -53,16 +53,14 @@ 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(),
|
||||
}
|
||||
: {
|
||||
@@ -79,11 +77,10 @@ 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(),
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const DockerProviderSchema = z.object({
|
||||
externalPort: z.preprocess((a) => {
|
||||
if (a === null || a === undefined || a === "") return null;
|
||||
const parsed = Number.parseInt(String(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
externalGRPCPort: z.preprocess((a) => {
|
||||
if (a === null || a === undefined || a === "") return null;
|
||||
const parsed = Number.parseInt(String(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
externalAdminPort: z.preprocess((a) => {
|
||||
if (a === null || a === undefined || a === "") return null;
|
||||
const parsed = Number.parseInt(String(a), 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}, z
|
||||
.number()
|
||||
.gte(0, "Range must be 0 - 65535")
|
||||
.lte(65535, "Range must be 0 - 65535")
|
||||
.nullable()),
|
||||
});
|
||||
|
||||
type DockerProvider = z.infer<typeof DockerProviderSchema>;
|
||||
|
||||
interface Props {
|
||||
libsqlId: string;
|
||||
}
|
||||
export const ShowExternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
||||
const { data: ip } = api.settings.getIp.useQuery();
|
||||
const { data, refetch } = api.libsql.one.useQuery({ libsqlId });
|
||||
const { mutateAsync, isPending } = api.libsql.saveExternalPorts.useMutation();
|
||||
const [connectionUrl, setConnectionUrl] = useState("");
|
||||
const [connectionGRPCUrl, setGRPCConnectionUrl] = useState("");
|
||||
const getIp = data?.server?.ipAddress || ip;
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {},
|
||||
resolver: zodResolver(DockerProviderSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
externalPort: data.externalPort,
|
||||
externalGRPCPort: data.externalGRPCPort,
|
||||
externalAdminPort: data.externalAdminPort,
|
||||
});
|
||||
}
|
||||
}, [form.reset, data, form]);
|
||||
|
||||
const onSubmit = async (values: DockerProvider) => {
|
||||
await mutateAsync({
|
||||
externalPort: values.externalPort,
|
||||
externalGRPCPort: values.externalGRPCPort,
|
||||
externalAdminPort: values.externalAdminPort,
|
||||
libsqlId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("External port/ports updated");
|
||||
await refetch();
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
toast.error(error?.message || "Error saving the external port/ports");
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const port = form.watch("externalPort") || data?.externalPort;
|
||||
setConnectionUrl(
|
||||
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${port}`,
|
||||
);
|
||||
|
||||
if (data?.sqldNode !== "replica") {
|
||||
const grpcPort = form.watch("externalGRPCPort") || data?.externalGRPCPort;
|
||||
setGRPCConnectionUrl(
|
||||
`http://${data?.databaseUser}:${data?.databasePassword}@${getIp}:${grpcPort}`,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
data?.externalGRPCPort,
|
||||
data?.databasePassword,
|
||||
form,
|
||||
data?.databaseUser,
|
||||
getIp,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">External Credentials</CardTitle>
|
||||
<CardDescription>
|
||||
In order to make the database reachable through the internet, you
|
||||
must set a port and ensure that the port is not being used by
|
||||
another application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-col gap-4">
|
||||
{!getIp && (
|
||||
<AlertBlock type="warning">
|
||||
You need to set an IP address in your{" "}
|
||||
<Link href="/dashboard/settings/server" className="text-primary">
|
||||
{data?.serverId
|
||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||
: "Web Server -> Server -> Update Server IP"}
|
||||
</Link>{" "}
|
||||
to fix the database url connection.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="externalPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>External Port (Internet)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="8080"
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!!data?.externalPort && (
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External Host</Label>
|
||||
<ToggleVisibilityInput value={connectionUrl} disabled />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="externalAdminPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>External Admin Port (Internet)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="5000"
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data?.sqldNode !== "replica" && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2 space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="externalGRPCPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>External GRPC Port (Internet)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="5001"
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!!data?.externalGRPCPort && (
|
||||
<div className="grid w-full gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>External GRPC Host</Label>
|
||||
<ToggleVisibilityInput
|
||||
value={connectionGRPCUrl}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" isLoading={isPending}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,268 +0,0 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { DrawerLogs } from "@/components/shared/drawer-logs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { api } from "@/utils/api";
|
||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||
|
||||
interface Props {
|
||||
libsqlId: string;
|
||||
}
|
||||
|
||||
export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
const { data, refetch } = api.libsql.one.useQuery(
|
||||
{
|
||||
libsqlId,
|
||||
},
|
||||
{ enabled: !!libsqlId },
|
||||
);
|
||||
|
||||
const { mutateAsync: reload, isPending: isReloading } =
|
||||
api.libsql.reload.useMutation();
|
||||
|
||||
const { mutateAsync: start, isPending: isStarting } =
|
||||
api.libsql.start.useMutation();
|
||||
|
||||
const { mutateAsync: stop, isPending: isStopping } =
|
||||
api.libsql.stop.useMutation();
|
||||
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||
const [isDeploying, setIsDeploying] = useState(false);
|
||||
api.libsql.deployWithLogs.useSubscription(
|
||||
{
|
||||
libsqlId: libsqlId,
|
||||
},
|
||||
{
|
||||
enabled: isDeploying,
|
||||
onData(log) {
|
||||
if (!isDrawerOpen) {
|
||||
setIsDrawerOpen(true);
|
||||
}
|
||||
|
||||
if (log === "Deployment completed successfully!") {
|
||||
setIsDeploying(false);
|
||||
}
|
||||
const parsedLogs = parseLogs(log);
|
||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||
},
|
||||
onError(error) {
|
||||
console.error("Deployment logs error:", error);
|
||||
setIsDeploying(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Deploy Libsql"
|
||||
description="Are you sure you want to deploy this Libsql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
setIsDeploying(true);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="default"
|
||||
isLoading={data?.applicationStatus === "running"}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Rocket className="size-4 mr-1" />
|
||||
Deploy
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the Libsql database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Reload Libsql"
|
||||
description="Are you sure you want to reload this libsql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await reload({
|
||||
libsqlId: libsqlId,
|
||||
appName: data?.appName || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Libsql reloaded successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error reloading Libsql");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isReloading}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<RefreshCcw className="size-4 mr-1" />
|
||||
Reload
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the Libsql service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
{data?.applicationStatus === "idle" ? (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Start Libsql"
|
||||
description="Are you sure you want to start this Libsql?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await start({
|
||||
libsqlId: libsqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Libsql started successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error starting Libsql");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
isLoading={isStarting}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle2 className="size-4 mr-1" />
|
||||
Start
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the Libsql database (requires a previous
|
||||
successful setup)
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<DialogAction
|
||||
title="Stop Libsql"
|
||||
description="Are you sure you want to stop this Libsql?"
|
||||
onClick={async () => {
|
||||
await stop({
|
||||
libsqlId: libsqlId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Libsql stopped successfully");
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error stopping Libsql");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="destructive"
|
||||
isLoading={isStopping}
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Ban className="size-4 mr-1" />
|
||||
Stop
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running Libsql database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center">
|
||||
<Terminal className="size-4 mr-1" />
|
||||
Open Terminal
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the Libsql container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</DockerTerminalModal>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<DrawerLogs
|
||||
isOpen={isDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsDrawerOpen(false);
|
||||
setFilteredLogs([]);
|
||||
setIsDeploying(false);
|
||||
refetch();
|
||||
}}
|
||||
filteredLogs={filteredLogs}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import { SelectGroup } from "@radix-ui/react-select";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
libsqlId: string;
|
||||
}
|
||||
export const ShowInternalLibsqlCredentials = ({ libsqlId }: Props) => {
|
||||
const { data } = api.libsql.one.useQuery({ libsqlId });
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full flex-col gap-5 ">
|
||||
<Card className="bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Internal Credentials</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full flex-row gap-4">
|
||||
<div className="grid w-full md:grid-cols-2 gap-4 md:gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>User</Label>
|
||||
<Input disabled value={data?.databaseUser} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Sqld Node</Label>
|
||||
<Select value={data?.sqldNode} disabled>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Node type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{["primary", "replica"].map((node) => (
|
||||
<SelectItem key={node} value={node}>
|
||||
{node.charAt(0).toUpperCase() + node.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Password</Label>
|
||||
<div className="flex flex-row gap-4">
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={data?.databasePassword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Label>Internal Port (Container)</Label>
|
||||
<Input disabled value="8080" />
|
||||
</div>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Label>Internal GRPC Port (Container)</Label>
|
||||
<Input disabled value="5001" />
|
||||
</div>
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<Label>Internal Admin Port (Container)</Label>
|
||||
<Input disabled value="5000" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Internal Host</Label>
|
||||
<Input disabled value={data?.appName} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Enable Namespaces</Label>
|
||||
<Select
|
||||
disabled
|
||||
defaultValue={
|
||||
data?.enableNamespaces
|
||||
? String(data?.enableNamespaces)
|
||||
: "false"
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={"false"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{["false", "true"].map((node) => (
|
||||
<SelectItem key={node} value={node}>
|
||||
{node.charAt(0).toUpperCase() + node.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Connection URL </Label>
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`http://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:8080`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 md:col-span-2">
|
||||
<Label>Internal Replication Connection URL </Label>
|
||||
<ToggleVisibilityInput
|
||||
disabled
|
||||
value={`http://${data?.databaseUser}:${data?.databasePassword}@${data?.appName}:5001`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,163 +0,0 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const updateLibsqlSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateLibsql = z.infer<typeof updateLibsqlSchema>;
|
||||
|
||||
interface Props {
|
||||
libsqlId: string;
|
||||
}
|
||||
|
||||
export const UpdateLibsql = ({ libsqlId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isPending } =
|
||||
api.libsql.update.useMutation();
|
||||
const { data } = api.libsql.one.useQuery(
|
||||
{
|
||||
libsqlId,
|
||||
},
|
||||
{
|
||||
enabled: !!libsqlId,
|
||||
},
|
||||
);
|
||||
const form = useForm<UpdateLibsql>({
|
||||
defaultValues: {
|
||||
description: data?.description ?? "",
|
||||
name: data?.name ?? "",
|
||||
},
|
||||
resolver: zodResolver(updateLibsqlSchema),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
description: data.description ?? "",
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (formData: UpdateLibsql) => {
|
||||
await mutateAsync({
|
||||
name: formData.name,
|
||||
libsqlId: libsqlId,
|
||||
description: formData.description || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Libsql updated successfully");
|
||||
utils.libsql.one.invalidate({
|
||||
libsqlId: libsqlId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the Libsql");
|
||||
})
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Modify Libsql</DialogTitle>
|
||||
<DialogDescription>Update the Libsql data</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid items-center gap-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
id="hook-form-update-libsql"
|
||||
className="grid w-full gap-4 "
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Vandelay Industries" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Description about your project..."
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isPending}
|
||||
form="hook-form-update-libsql"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -42,7 +42,6 @@ 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: () =>
|
||||
@@ -57,7 +56,6 @@ 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(),
|
||||
@@ -86,7 +84,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
form.reset({
|
||||
dockerImage: data.dockerImage,
|
||||
command: data.command || "",
|
||||
args: (data as any).args?.map((arg: string) => ({ value: arg })) || [],
|
||||
args: data.args?.map((arg) => ({ value: arg })) || [],
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
@@ -97,7 +95,6 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
postgresId: id || "",
|
||||
redisId: id || "",
|
||||
mysqlId: id || "",
|
||||
libsqlId: id || "",
|
||||
mariadbId: id || "",
|
||||
dockerImage: formData?.dockerImage,
|
||||
command: formData?.command,
|
||||
@@ -147,14 +144,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Command</FormLabel>
|
||||
<FormControl>
|
||||
<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}
|
||||
/>
|
||||
<Input placeholder="/bin/sh" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -79,7 +79,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
api.compose.create.useMutation();
|
||||
|
||||
// Get environment data to extract projectId
|
||||
// const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
|
||||
const hasServers = servers && servers.length > 0;
|
||||
// Show dropdown logic based on cloud environment
|
||||
@@ -117,8 +117,6 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
await utils.environment.one.invalidate({
|
||||
environmentId,
|
||||
});
|
||||
// Invalidate the project query to refresh the project data for the advance-breadcrumb
|
||||
await utils.project.all.invalidate();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the compose");
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
LibsqlIcon,
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
MysqlIcon,
|
||||
@@ -56,7 +55,6 @@ 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",
|
||||
@@ -68,9 +66,8 @@ const databasesUserDefaultPlaceholder: Record<
|
||||
Exclude<DbType, "redis">,
|
||||
string
|
||||
> = {
|
||||
libsql: "libsql",
|
||||
mariadb: "mariadb",
|
||||
mongo: "mongo",
|
||||
mariadb: "mariadb",
|
||||
mysql: "mysql",
|
||||
postgres: "postgres",
|
||||
};
|
||||
@@ -97,88 +94,56 @@ const baseDatabaseSchema = z.object({
|
||||
serverId: z.string().nullable(),
|
||||
});
|
||||
|
||||
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"],
|
||||
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@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"sqldPrimaryUrl should not be provided when sqldNode is not 'replica'.",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databaseUser: z.string().default("mysql"),
|
||||
databaseName: z.string().default("mysql"),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("mariadb"),
|
||||
dockerImage: z.string().default("mariadb:4"),
|
||||
databaseRootPassword: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
|
||||
message:
|
||||
"Password contains invalid characters. Please avoid: $ ! ' \" \\ / and space characters for database compatibility",
|
||||
})
|
||||
.optional(),
|
||||
databaseUser: z.string().default("mariadb"),
|
||||
databaseName: z.string().default("mariadb"),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
]);
|
||||
|
||||
const databasesMap = {
|
||||
postgres: {
|
||||
@@ -201,10 +166,6 @@ const databasesMap = {
|
||||
icon: <RedisIcon />,
|
||||
label: "Redis",
|
||||
},
|
||||
libsql: {
|
||||
icon: <LibsqlIcon className="size-10" />,
|
||||
label: "libSQL",
|
||||
},
|
||||
};
|
||||
|
||||
type AddDatabase = z.infer<typeof mySchema>;
|
||||
@@ -220,12 +181,11 @@ 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 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 mongoMutation = api.mongo.create.useMutation();
|
||||
const redisMutation = api.redis.create.useMutation();
|
||||
const mariadbMutation = api.mariadb.create.useMutation();
|
||||
const mysqlMutation = api.mysql.create.useMutation();
|
||||
|
||||
// Get environment data to extract projectId
|
||||
const { data: environment } = api.environment.one.useQuery({ environmentId });
|
||||
@@ -250,15 +210,13 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
},
|
||||
resolver: zodResolver(mySchema),
|
||||
});
|
||||
const sqldNode = form.watch("sqldNode");
|
||||
const type = form.watch("type");
|
||||
const activeMutation = {
|
||||
libsql: libsqlMutation,
|
||||
mariadb: mariadbMutation,
|
||||
mongo: mongoMutation,
|
||||
mysql: mysqlMutation,
|
||||
postgres: postgresMutation,
|
||||
mongo: mongoMutation,
|
||||
redis: redisMutation,
|
||||
mariadb: mariadbMutation,
|
||||
mysql: mysqlMutation,
|
||||
};
|
||||
|
||||
const onSubmit = async (data: AddDatabase) => {
|
||||
@@ -275,23 +233,12 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
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({
|
||||
if (data.type === "postgres") {
|
||||
promise = postgresMutation.mutateAsync({
|
||||
...commonParams,
|
||||
databasePassword: data.databasePassword,
|
||||
databaseRootPassword: data.databaseRootPassword || "",
|
||||
databaseName: data.databaseName || "mariadb",
|
||||
databaseName: data.databaseName || "postgres",
|
||||
|
||||
databaseUser:
|
||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||
serverId: data.serverId === "dokploy" ? null : data.serverId,
|
||||
@@ -305,6 +252,22 @@ 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,
|
||||
@@ -315,21 +278,6 @@ 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) {
|
||||
@@ -357,7 +305,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={setVisible}>
|
||||
<DialogTrigger className="w-full">
|
||||
@@ -559,8 +506,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{(type === "mariadb" ||
|
||||
type === "mysql" ||
|
||||
{(type === "mysql" ||
|
||||
type === "mariadb" ||
|
||||
type === "postgres") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -577,101 +524,10 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{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 === "mysql" ||
|
||||
type === "mariadb" ||
|
||||
type === "mongo" ||
|
||||
type === "mysql" ||
|
||||
type === "postgres") && (
|
||||
type === "postgres" ||
|
||||
type === "mongo") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databaseUser"
|
||||
@@ -712,7 +568,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{(type === "mariadb" || type === "mysql") && (
|
||||
{(type === "mysql" || type === "mariadb") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databaseRootPassword"
|
||||
|
||||
@@ -332,7 +332,6 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
viewMode === "detailed" && "border-b",
|
||||
)}
|
||||
>
|
||||
{/** biome-ignore lint/performance/noImgElement: this is a valid use for img tag */}
|
||||
<img
|
||||
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
|
||||
className={cn(
|
||||
|
||||
@@ -92,8 +92,6 @@ export const AdvancedEnvironmentSelector = ({
|
||||
|
||||
toast.success("Environment created successfully");
|
||||
utils.environment.byProjectId.invalidate({ projectId });
|
||||
// Invalidate the project query to refresh the project data for the advance-breadcrumb
|
||||
utils.project.all.invalidate();
|
||||
setIsCreateDialogOpen(false);
|
||||
setName("");
|
||||
setDescription("");
|
||||
|
||||
@@ -28,14 +28,13 @@ export type Services = {
|
||||
serverId?: string | null;
|
||||
name: string;
|
||||
type:
|
||||
| "application"
|
||||
| "compose"
|
||||
| "libsql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "mysql"
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "redis";
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { TagSelector } from "@/components/shared/tag-selector";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -63,7 +62,6 @@ interface Props {
|
||||
export const HandleProject = ({ projectId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
||||
|
||||
const { mutateAsync, error, isError } = projectId
|
||||
? api.project.update.useMutation()
|
||||
@@ -77,10 +75,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
||||
enabled: !!projectId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: availableTags = [] } = api.tag.all.useQuery();
|
||||
const bulkAssignMutation = api.tag.bulkAssign.useMutation();
|
||||
|
||||
const router = useRouter();
|
||||
const form = useForm<AddProject>({
|
||||
defaultValues: {
|
||||
@@ -95,13 +89,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
||||
description: data?.description ?? "",
|
||||
name: data?.name ?? "",
|
||||
});
|
||||
// Load existing tags when editing a project
|
||||
if (data?.projectTags) {
|
||||
const tagIds = data.projectTags.map((pt) => pt.tagId);
|
||||
setSelectedTagIds(tagIds);
|
||||
} else {
|
||||
setSelectedTagIds([]);
|
||||
}
|
||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
||||
|
||||
const onSubmit = async (data: AddProject) => {
|
||||
@@ -111,26 +98,12 @@ export const HandleProject = ({ projectId }: Props) => {
|
||||
projectId: projectId || "",
|
||||
})
|
||||
.then(async (data) => {
|
||||
// Assign tags to the project (both create and update)
|
||||
const projectIdToUse =
|
||||
projectId ||
|
||||
(data && "project" in data ? data.project.projectId : undefined);
|
||||
|
||||
if (projectIdToUse) {
|
||||
try {
|
||||
await bulkAssignMutation.mutateAsync({
|
||||
projectId: projectIdToUse,
|
||||
tagIds: selectedTagIds,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Failed to assign tags to project");
|
||||
}
|
||||
}
|
||||
|
||||
await utils.project.all.invalidate();
|
||||
toast.success(projectId ? "Project Updated" : "Project Created");
|
||||
setIsOpen(false);
|
||||
if (!projectId) {
|
||||
const projectIdToUse =
|
||||
data && "project" in data ? data.project.projectId : undefined;
|
||||
const environmentIdToUse =
|
||||
data && "environment" in data
|
||||
? data.environment.environmentId
|
||||
@@ -217,20 +190,6 @@ export const HandleProject = ({ projectId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<TagSelector
|
||||
tags={availableTags.map((tag) => ({
|
||||
id: tag.tagId,
|
||||
name: tag.name,
|
||||
color: tag.color ?? undefined,
|
||||
}))}
|
||||
selectedTags={selectedTagIds}
|
||||
onTagsChange={setSelectedTagIds}
|
||||
placeholder="Select tags..."
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -15,8 +15,6 @@ import { toast } from "sonner";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||
import { TagBadge } from "@/components/shared/tag-badge";
|
||||
import { TagFilter } from "@/components/shared/tag-filter";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -51,6 +49,7 @@ 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";
|
||||
@@ -64,7 +63,6 @@ export const ShowProjects = () => {
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { mutateAsync } = api.project.remove.useMutation();
|
||||
const { data: availableTags } = api.tag.all.useQuery();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState(
|
||||
router.isReady && typeof router.query.q === "string" ? router.query.q : "",
|
||||
@@ -78,31 +76,10 @@ export const ShowProjects = () => {
|
||||
return "createdAt-desc";
|
||||
});
|
||||
|
||||
const [selectedTagIds, setSelectedTagIds] = useState<string[]>(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem("projectsTagFilter");
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("projectsSort", sortBy);
|
||||
}, [sortBy]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("projectsTagFilter", JSON.stringify(selectedTagIds));
|
||||
}, [selectedTagIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableTags) return;
|
||||
const validIds = new Set(availableTags.map((t) => t.tagId));
|
||||
setSelectedTagIds((prev) => {
|
||||
const filtered = prev.filter((id) => validIds.has(id));
|
||||
return filtered.length === prev.length ? prev : filtered;
|
||||
});
|
||||
}, [availableTags]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return;
|
||||
const urlQuery = typeof router.query.q === "string" ? router.query.q : "";
|
||||
@@ -130,7 +107,7 @@ export const ShowProjects = () => {
|
||||
const filteredProjects = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
let filtered = data.filter(
|
||||
const filtered = data.filter(
|
||||
(project) =>
|
||||
project.name
|
||||
.toLowerCase()
|
||||
@@ -140,15 +117,6 @@ export const ShowProjects = () => {
|
||||
.includes(debouncedSearchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
// Filter by selected tags (OR logic: show projects with ANY selected tag)
|
||||
if (selectedTagIds.length > 0) {
|
||||
filtered = filtered.filter((project) =>
|
||||
project.projectTags?.some((pt) =>
|
||||
selectedTagIds.includes(pt.tag.tagId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Then sort the filtered results
|
||||
const [field, direction] = sortBy.split("-");
|
||||
return [...filtered].sort((a, b) => {
|
||||
@@ -194,7 +162,7 @@ export const ShowProjects = () => {
|
||||
}
|
||||
return direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}, [data, debouncedSearchQuery, sortBy, selectedTagIds]);
|
||||
}, [data, debouncedSearchQuery, sortBy]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -240,44 +208,29 @@ export const ShowProjects = () => {
|
||||
|
||||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<TagFilter
|
||||
tags={
|
||||
availableTags?.map((tag) => ({
|
||||
id: tag.tagId,
|
||||
name: tag.name,
|
||||
color: tag.color || undefined,
|
||||
})) || []
|
||||
}
|
||||
selectedTags={selectedTagIds}
|
||||
onTagsChange={setSelectedTagIds}
|
||||
/>
|
||||
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
||||
<ArrowUpDown className="size-4 text-muted-foreground" />
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Sort by..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||
<SelectItem value="name-desc">
|
||||
Name (Z-A)
|
||||
</SelectItem>
|
||||
<SelectItem value="createdAt-desc">
|
||||
Newest first
|
||||
</SelectItem>
|
||||
<SelectItem value="createdAt-asc">
|
||||
Oldest first
|
||||
</SelectItem>
|
||||
<SelectItem value="services-desc">
|
||||
Most services
|
||||
</SelectItem>
|
||||
<SelectItem value="services-asc">
|
||||
Least services
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
|
||||
<ArrowUpDown className="size-4 text-muted-foreground" />
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Sort by..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
|
||||
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
|
||||
<SelectItem value="createdAt-desc">
|
||||
Newest first
|
||||
</SelectItem>
|
||||
<SelectItem value="createdAt-asc">
|
||||
Oldest first
|
||||
</SelectItem>
|
||||
<SelectItem value="services-desc">
|
||||
Most services
|
||||
</SelectItem>
|
||||
<SelectItem value="services-asc">
|
||||
Least services
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
{filteredProjects?.length === 0 && (
|
||||
@@ -294,27 +247,26 @@ 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.redis.length === 0 &&
|
||||
env.applications.length === 0 &&
|
||||
env.compose.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.redis.length +
|
||||
env.applications.length +
|
||||
env.compose.length,
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr, 0);
|
||||
|
||||
@@ -357,19 +309,6 @@ export const ShowProjects = () => {
|
||||
{project.description}
|
||||
</span>
|
||||
|
||||
{project.projectTags &&
|
||||
project.projectTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||
{project.projectTags.map((pt) => (
|
||||
<TagBadge
|
||||
key={pt.tag.tagId}
|
||||
name={pt.tag.name}
|
||||
color={pt.tag.color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasNoEnvironments && (
|
||||
<div className="flex flex-row gap-2 items-center rounded-lg bg-yellow-50 p-2 mt-2 dark:bg-yellow-950">
|
||||
<AlertTriangle className="size-4 text-yellow-600 dark:text-yellow-400 shrink-0" />
|
||||
@@ -490,7 +429,7 @@ export const ShowProjects = () => {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardFooter className="pt-4">
|
||||
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||
<DateTooltip date={project.createdAt}>
|
||||
Created
|
||||
</DateTooltip>
|
||||
|
||||
@@ -91,10 +91,7 @@ export const ShowBilling = () => {
|
||||
api.stripe.upgradeSubscription.useMutation();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
||||
const [startupServerQuantity, setStartupServerQuantity] = useState(
|
||||
STARTUP_SERVERS_INCLUDED,
|
||||
);
|
||||
const [serverQuantity, setServerQuantity] = useState(3);
|
||||
const [isAnnual, setIsAnnual] = useState(false);
|
||||
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
||||
null,
|
||||
@@ -114,12 +111,6 @@ export const ShowBilling = () => {
|
||||
productId: string,
|
||||
) => {
|
||||
const stripe = await stripePromise;
|
||||
const serverQuantity =
|
||||
tier === "startup"
|
||||
? startupServerQuantity
|
||||
: tier === "hobby"
|
||||
? hobbyServerQuantity
|
||||
: hobbyServerQuantity;
|
||||
if (data && data.subscriptions.length === 0) {
|
||||
createCheckoutSession({
|
||||
tier,
|
||||
@@ -688,7 +679,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
$
|
||||
{calculatePriceHobby(
|
||||
hobbyServerQuantity,
|
||||
serverQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}
|
||||
/{isAnnual ? "yr" : "mo"}
|
||||
@@ -701,8 +692,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
$
|
||||
{(
|
||||
calculatePriceHobby(hobbyServerQuantity, true) /
|
||||
12
|
||||
calculatePriceHobby(serverQuantity, true) / 12
|
||||
).toFixed(2)}
|
||||
/mo
|
||||
</p>
|
||||
@@ -734,19 +724,19 @@ export const ShowBilling = () => {
|
||||
Servers:
|
||||
</span>
|
||||
<Button
|
||||
disabled={hobbyServerQuantity <= 1}
|
||||
disabled={serverQuantity <= 1}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
||||
setServerQuantity((q) => Math.max(1, q - 1))
|
||||
}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={hobbyServerQuantity}
|
||||
value={serverQuantity}
|
||||
onChange={(e) =>
|
||||
setHobbyServerQuantity(
|
||||
setServerQuantity(
|
||||
Math.max(
|
||||
1,
|
||||
Number(
|
||||
@@ -760,7 +750,7 @@ export const ShowBilling = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
||||
onClick={() => setServerQuantity((q) => q + 1)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -785,7 +775,7 @@ export const ShowBilling = () => {
|
||||
onClick={() =>
|
||||
handleCheckout("hobby", data!.hobbyProductId!)
|
||||
}
|
||||
disabled={hobbyServerQuantity < 1}
|
||||
disabled={serverQuantity < 1}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
@@ -816,7 +806,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
$
|
||||
{calculatePriceStartup(
|
||||
startupServerQuantity,
|
||||
serverQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}
|
||||
/{isAnnual ? "yr" : "mo"}
|
||||
@@ -829,10 +819,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
$
|
||||
{(
|
||||
calculatePriceStartup(
|
||||
startupServerQuantity,
|
||||
true,
|
||||
) / 12
|
||||
calculatePriceStartup(serverQuantity, true) / 12
|
||||
).toFixed(2)}
|
||||
/mo
|
||||
</p>
|
||||
@@ -869,14 +856,13 @@ export const ShowBilling = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
disabled={
|
||||
startupServerQuantity <=
|
||||
STARTUP_SERVERS_INCLUDED
|
||||
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() =>
|
||||
setStartupServerQuantity((q) =>
|
||||
setServerQuantity((q) =>
|
||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||
)
|
||||
}
|
||||
@@ -884,9 +870,9 @@ export const ShowBilling = () => {
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={startupServerQuantity}
|
||||
value={serverQuantity}
|
||||
onChange={(e) =>
|
||||
setStartupServerQuantity(
|
||||
setServerQuantity(
|
||||
Math.max(
|
||||
STARTUP_SERVERS_INCLUDED,
|
||||
Number(
|
||||
@@ -901,9 +887,7 @@ export const ShowBilling = () => {
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() =>
|
||||
setStartupServerQuantity((q) => q + 1)
|
||||
}
|
||||
onClick={() => setServerQuantity((q) => q + 1)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -933,7 +917,7 @@ export const ShowBilling = () => {
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
||||
serverQuantity < STARTUP_SERVERS_INCLUDED
|
||||
}
|
||||
>
|
||||
Get Started
|
||||
@@ -1025,7 +1009,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(
|
||||
hobbyServerQuantity,
|
||||
serverQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}{" "}
|
||||
USD
|
||||
@@ -1034,10 +1018,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||
${" "}
|
||||
{(
|
||||
calculatePrice(
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
) / 12
|
||||
calculatePrice(serverQuantity, isAnnual) / 12
|
||||
).toFixed(2)}{" "}
|
||||
/ Month USD
|
||||
</p>
|
||||
@@ -1045,10 +1026,9 @@ export const ShowBilling = () => {
|
||||
) : (
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}{" "}
|
||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
||||
2,
|
||||
)}{" "}
|
||||
USD
|
||||
</p>
|
||||
)}
|
||||
@@ -1091,28 +1071,26 @@ export const ShowBilling = () => {
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{hobbyServerQuantity} Servers
|
||||
{serverQuantity} Servers
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled={hobbyServerQuantity <= 1}
|
||||
disabled={serverQuantity <= 1}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (hobbyServerQuantity <= 1) return;
|
||||
if (serverQuantity <= 1) return;
|
||||
|
||||
setHobbyServerQuantity(
|
||||
hobbyServerQuantity - 1,
|
||||
);
|
||||
setServerQuantity(serverQuantity - 1);
|
||||
}}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={hobbyServerQuantity}
|
||||
value={serverQuantity}
|
||||
onChange={(e) => {
|
||||
setHobbyServerQuantity(
|
||||
setServerQuantity(
|
||||
e.target.value as unknown as number,
|
||||
);
|
||||
}}
|
||||
@@ -1121,9 +1099,7 @@ export const ShowBilling = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setHobbyServerQuantity(
|
||||
hobbyServerQuantity + 1,
|
||||
);
|
||||
setServerQuantity(serverQuantity + 1);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
@@ -1149,7 +1125,7 @@ export const ShowBilling = () => {
|
||||
onClick={async () => {
|
||||
handleCheckout("legacy", product.id);
|
||||
}}
|
||||
disabled={hobbyServerQuantity < 1}
|
||||
disabled={serverQuantity < 1}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
|
||||
@@ -14,13 +14,13 @@ export const extractExpirationDate = (certData: string): Date | null => {
|
||||
|
||||
// Helper: read ASN.1 length field
|
||||
function readLength(pos: number): { length: number; offset: number } {
|
||||
// biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation
|
||||
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||
let len = der[pos++];
|
||||
if (len & 0x80) {
|
||||
const bytes = len & 0x7f;
|
||||
len = 0;
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
// biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation
|
||||
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||
len = (len << 8) + der[pos++];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Palette, PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { TagBadge } from "@/components/shared/tag-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const TagSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, "Tag name is required")
|
||||
.max(50, "Tag name must be less than 50 characters")
|
||||
.refine(
|
||||
(name) => {
|
||||
const trimmedName = name.trim();
|
||||
const validNameRegex =
|
||||
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_.-]*[\p{L}\p{N}_-]$/u;
|
||||
return validNameRegex.test(trimmedName);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Tag name must start and end with a letter, number, hyphen or underscore. Spaces are allowed in between.",
|
||||
},
|
||||
)
|
||||
.transform((name) => name.trim()),
|
||||
color: z.string().optional(),
|
||||
});
|
||||
|
||||
type Tag = z.infer<typeof TagSchema>;
|
||||
|
||||
interface HandleTagProps {
|
||||
tagId?: string;
|
||||
}
|
||||
|
||||
export const HandleTag = ({ tagId }: HandleTagProps) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { mutateAsync, error, isError } = tagId
|
||||
? api.tag.update.useMutation()
|
||||
: api.tag.create.useMutation();
|
||||
|
||||
const { data: tag } = api.tag.one.useQuery(
|
||||
{
|
||||
tagId: tagId || "",
|
||||
},
|
||||
{
|
||||
enabled: !!tagId,
|
||||
},
|
||||
);
|
||||
|
||||
const form = useForm<Tag>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
color: "#3b82f6",
|
||||
},
|
||||
resolver: zodResolver(TagSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (tag) {
|
||||
form.reset({
|
||||
name: tag.name ?? "",
|
||||
color: tag.color ?? "#3b82f6",
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
name: "",
|
||||
color: "#3b82f6",
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, tag]);
|
||||
|
||||
const onSubmit = async (data: Tag) => {
|
||||
await mutateAsync({
|
||||
name: data.name,
|
||||
color: data.color,
|
||||
tagId: tagId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.tag.all.invalidate();
|
||||
toast.success(tagId ? "Tag Updated" : "Tag Created");
|
||||
setIsOpen(false);
|
||||
form.reset();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(tagId ? "Error updating tag" : "Error creating tag");
|
||||
});
|
||||
};
|
||||
|
||||
const colorValue = form.watch("color");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{tagId ? (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<PenBoxIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Tag
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{tagId ? "Update" : "Create"} Tag</DialogTitle>
|
||||
<DialogDescription>
|
||||
{tagId
|
||||
? "Update the tag name and color"
|
||||
: "Create a new tag to organize your projects"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-tag"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="e.g., Production, Client, Internal"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="color"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Color (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3">
|
||||
<FormLabel
|
||||
className="relative flex items-center justify-center w-12 h-12 rounded-md border-2 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
style={{
|
||||
backgroundColor: field.value || "#3b82f6",
|
||||
}}
|
||||
onClick={() => colorInputRef.current?.click()}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
{!field.value && (
|
||||
<Palette className="h-5 w-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={colorInputRef}
|
||||
type="color"
|
||||
className="absolute opacity-0 pointer-events-none w-12 h-12 top-0 left-0"
|
||||
value={field.value || "#3b82f6"}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormLabel>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="#3b82f6"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value.startsWith("#") || value === "") {
|
||||
field.onChange(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormDescription className="mt-1">
|
||||
Choose a color to easily identify this tag
|
||||
</FormDescription>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{colorValue && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Preview:</span>
|
||||
<TagBadge
|
||||
name={form.watch("name") || "Tag Name"}
|
||||
color={colorValue}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form-tag"
|
||||
type="submit"
|
||||
>
|
||||
{tagId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Loader2, TagIcon, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { TagBadge } from "@/components/shared/tag-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { HandleTag } from "./handle-tag";
|
||||
|
||||
export const TagManager = () => {
|
||||
const utils = api.useUtils();
|
||||
const { data: tags, isPending } = api.tag.all.useQuery();
|
||||
const { mutateAsync: deleteTag, isPending: isRemoving } =
|
||||
api.tag.remove.useMutation();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<TagIcon className="size-6 text-muted-foreground self-center" />
|
||||
Tags
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Create and manage tags to organize your projects
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
{isPending ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!tags || tags.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<TagIcon className="size-6 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
No tags yet. Create your first tag to start organizing
|
||||
projects.
|
||||
</span>
|
||||
{permissions?.tag.create && <HandleTag />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
<div className="flex flex-col gap-4 rounded-lg">
|
||||
{tags.map((tag) => (
|
||||
<div
|
||||
key={tag.tagId}
|
||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<TagBadge name={tag.name} color={tag.color} />
|
||||
{tag.color && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{tag.color}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 items-center">
|
||||
{permissions?.tag.update && (
|
||||
<HandleTag tagId={tag.tagId} />
|
||||
)}
|
||||
{permissions?.tag.delete && (
|
||||
<DialogAction
|
||||
title="Delete Tag"
|
||||
description={`Are you sure you want to delete the tag "${tag.name}"? This will remove the tag from all projects. This action cannot be undone.`}
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await deleteTag({
|
||||
tagId: tag.tagId,
|
||||
})
|
||||
.then(async () => {
|
||||
await utils.tag.all.invalidate();
|
||||
toast.success(
|
||||
"Tag deleted successfully",
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting tag");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10"
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{permissions?.tag.create && (
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleTag />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -46,8 +46,7 @@ export type Services = {
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose"
|
||||
| "libsql";
|
||||
| "compose";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -137,18 +136,6 @@ 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,
|
||||
@@ -156,7 +143,6 @@ export const extractServices = (data: Environment | undefined) => {
|
||||
...postgres,
|
||||
...mariadb,
|
||||
...compose,
|
||||
...libsql,
|
||||
);
|
||||
|
||||
applications.sort((a, b) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { format } from "date-fns";
|
||||
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -37,19 +36,10 @@ export const ShowUsers = () => {
|
||||
const { data, isPending, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync } = api.user.remove.useMutation();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: hasValidLicense } =
|
||||
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||
|
||||
const utils = api.useUtils();
|
||||
const { data: session } = api.user.session.useQuery();
|
||||
|
||||
const FREE_ROLES = ["owner", "admin", "member"];
|
||||
const membersWithCustomRoles = data?.filter(
|
||||
(member) => !FREE_ROLES.includes(member.role),
|
||||
);
|
||||
const hasCustomRolesWithoutLicense =
|
||||
!hasValidLicense && (membersWithCustomRoles?.length ?? 0) > 0;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
@@ -80,18 +70,6 @@ export const ShowUsers = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
{hasCustomRolesWithoutLicense && (
|
||||
<AlertBlock type="warning">
|
||||
You have{" "}
|
||||
{membersWithCustomRoles?.length === 1
|
||||
? "1 user"
|
||||
: `${membersWithCustomRoles?.length} users`}{" "}
|
||||
assigned to custom roles. Custom roles will not work
|
||||
without a valid Enterprise license. Please activate your
|
||||
license or change these users to a free role (Admin or
|
||||
Member).
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
HardDriveDownload,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { HardDriveDownload, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -22,70 +15,11 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
type ServiceStatus = {
|
||||
status: "healthy" | "unhealthy";
|
||||
message?: string;
|
||||
};
|
||||
|
||||
type HealthResult = {
|
||||
postgres: ServiceStatus;
|
||||
redis: ServiceStatus;
|
||||
traefik: ServiceStatus;
|
||||
};
|
||||
|
||||
type ModalState = "idle" | "checking" | "results" | "updating";
|
||||
|
||||
const ServiceStatusItem = ({
|
||||
name,
|
||||
service,
|
||||
}: {
|
||||
name: string;
|
||||
service: ServiceStatus;
|
||||
}) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{service.status === "healthy" ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<span className="text-sm font-medium">{name}</span>
|
||||
{service.status === "unhealthy" && service.message && (
|
||||
<span className="text-xs text-muted-foreground">— {service.message}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const UpdateWebServer = () => {
|
||||
const [modalState, setModalState] = useState<ModalState>("idle");
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [healthResult, setHealthResult] = useState<HealthResult | null>(null);
|
||||
|
||||
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
|
||||
const { refetch: checkHealth } =
|
||||
api.settings.checkInfrastructureHealth.useQuery(undefined, {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const handleVerify = async () => {
|
||||
setModalState("checking");
|
||||
setHealthResult(null);
|
||||
|
||||
try {
|
||||
const result = await checkHealth();
|
||||
if (result.data) {
|
||||
setHealthResult(result.data);
|
||||
}
|
||||
} catch {
|
||||
// checkHealth failed entirely
|
||||
}
|
||||
setModalState("results");
|
||||
};
|
||||
|
||||
const allHealthy =
|
||||
healthResult &&
|
||||
healthResult.postgres.status === "healthy" &&
|
||||
healthResult.redis.status === "healthy" &&
|
||||
healthResult.traefik.status === "healthy";
|
||||
|
||||
const checkIsUpdateFinished = async () => {
|
||||
try {
|
||||
@@ -99,24 +33,28 @@ export const UpdateWebServer = () => {
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
// Allow seeing the toast before reloading
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} catch {
|
||||
// Delay each request
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
// Keep running until it returns 200
|
||||
void checkIsUpdateFinished();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
setModalState("updating");
|
||||
setUpdating(true);
|
||||
await updateServer();
|
||||
|
||||
// Give some time for docker service restart before starting to check status
|
||||
await new Promise((resolve) => setTimeout(resolve, 8000));
|
||||
|
||||
await checkIsUpdateFinished();
|
||||
} catch (error) {
|
||||
setModalState("results");
|
||||
setUpdating(false);
|
||||
console.error("Error updating server:", error);
|
||||
toast.error(
|
||||
"An error occurred while updating the server, please try again.",
|
||||
@@ -124,14 +62,6 @@ export const UpdateWebServer = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (modalState !== "updating") {
|
||||
setOpen(false);
|
||||
setModalState("idle");
|
||||
setHealthResult(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={open}>
|
||||
<AlertDialogTrigger asChild>
|
||||
@@ -151,111 +81,36 @@ export const UpdateWebServer = () => {
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{modalState === "idle" && "Are you absolutely sure?"}
|
||||
{modalState === "checking" && "Verifying Services..."}
|
||||
{modalState === "results" &&
|
||||
(allHealthy ? "Ready to Update" : "Service Issues Detected")}
|
||||
{modalState === "updating" && "Server update in progress"}
|
||||
{updating
|
||||
? "Server update in progress"
|
||||
: "Are you absolutely sure?"}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription asChild>
|
||||
<div>
|
||||
{modalState === "idle" && (
|
||||
<span>
|
||||
This will update the web server to the new version. You will
|
||||
not be able to use the panel during the update process. The
|
||||
page will be reloaded once the update is finished.
|
||||
<br />
|
||||
<br />
|
||||
We recommend verifying that all services are running before
|
||||
updating.
|
||||
</span>
|
||||
)}
|
||||
|
||||
{modalState === "checking" && (
|
||||
<span className="flex items-center gap-2">
|
||||
<Loader2 className="animate-spin h-4 w-4" />
|
||||
Checking PostgreSQL, Redis and Traefik...
|
||||
</span>
|
||||
)}
|
||||
|
||||
{modalState === "results" && healthResult && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<ServiceStatusItem
|
||||
name="PostgreSQL"
|
||||
service={healthResult.postgres}
|
||||
/>
|
||||
<ServiceStatusItem
|
||||
name="Redis"
|
||||
service={healthResult.redis}
|
||||
/>
|
||||
<ServiceStatusItem
|
||||
name="Traefik"
|
||||
service={healthResult.traefik}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!allHealthy && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
Some services are not healthy. You can still proceed
|
||||
with the update.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allHealthy && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
All services are running. You can proceed with the update.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalState === "results" && !healthResult && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
|
||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||
Could not verify services. You can still proceed with the
|
||||
update.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalState === "updating" && (
|
||||
<span className="flex items-center gap-2">
|
||||
<Loader2 className="animate-spin h-4 w-4" />
|
||||
The server is being updated, please wait...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<AlertDialogDescription>
|
||||
{updating ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Loader2 className="animate-spin" />
|
||||
The server is being updated, please wait...
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
This action cannot be undone. This will update the web server to
|
||||
the new version. You will not be able to use the panel during
|
||||
the update process. The page will be reloaded once the update is
|
||||
finished.
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
{modalState === "idle" && (
|
||||
{!updating && (
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
|
||||
<Button variant="secondary" onClick={handleVerify}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Verify Status
|
||||
</Button>
|
||||
<AlertDialogCancel onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirm}>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
)}
|
||||
{modalState === "results" && (
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
|
||||
<Button variant="secondary" onClick={handleVerify}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Re-check
|
||||
</Button>
|
||||
<AlertDialogAction onClick={handleConfirm}>
|
||||
{allHealthy ? "Confirm" : "Confirm Anyway"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
)}
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
|
||||
@@ -17,18 +17,17 @@ import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
}
|
||||
|
||||
export const RebuildDatabase = ({ id, type }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const mutationMap = {
|
||||
libsql: () => api.libsql.rebuild.useMutation(),
|
||||
postgres: () => api.postgres.rebuild.useMutation(),
|
||||
mysql: () => api.mysql.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(),
|
||||
};
|
||||
|
||||
@@ -37,11 +36,10 @@ export const RebuildDatabase = ({ id, type }: Props) => {
|
||||
const handleRebuild = async () => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
libsqlId: type === "libsql" ? id : "",
|
||||
postgresId: type === "postgres" ? id : "",
|
||||
mysqlId: type === "mysql" ? 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,20 +6,14 @@ import { RebuildDatabase } from "./rebuild-database";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: "libsql" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
|
||||
type: "postgres" | "mysql" | "mariadb" | "mongo" | "redis";
|
||||
}
|
||||
|
||||
export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<ShowCustomCommand id={id} type={type} />
|
||||
{type === "mariadb" ||
|
||||
type === "mongo" ||
|
||||
type === "mysql" ||
|
||||
type === "postgres" ||
|
||||
type === "redis" ? (
|
||||
<ShowClusterSettings id={id} type={type} />
|
||||
) : null}
|
||||
<ShowClusterSettings id={id} type={type} />
|
||||
<ShowVolumes id={id} type={type} />
|
||||
<ShowResources id={id} type={type} />
|
||||
<RebuildDatabase id={id} type={type} />
|
||||
|
||||
@@ -156,61 +156,6 @@ export const RedisIcon = ({ className }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const LibsqlIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
aria-label="libsql"
|
||||
height="1em"
|
||||
width="1em"
|
||||
viewBox="0 0 217.2 217.2"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
|
||||
d="M118,87.2c.7,0,1.3,0,1.9.3.4.1.8.3,1.2.5.2.1.4.2.6.3.4.3.7.5,1.1.9l5.2,5.2c2.7,2.7,2.7,7,0,9.7l-95.1,95.1c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l85.1-85.1,10-10c.3-.3.7-.6,1.1-.9.2-.1.4-.2.6-.3.4-.2.8-.4,1.2-.5.6-.2,1.3-.3,1.9-.3M118,71.2c-2.2,0-4.4.3-6.5.9-1.4.4-2.8,1-4.1,1.7-.7.4-1.3.7-2,1.2-1.3.8-2.5,1.8-3.6,2.9l-10,10L6.7,173c-8.9,8.9-8.9,23.4,0,32.3l5.2,5.2c4.3,4.3,10.1,6.7,16.2,6.7s11.8-2.4,16.2-6.7l95.1-95.1c4.3-4.3,6.7-10.1,6.7-16.2s-2.4-11.8-6.7-16.2l-5.2-5.2c-1.1-1.1-2.3-2.1-3.6-2.9-.6-.4-1.3-.8-1.9-1.2-1.3-.7-2.7-1.3-4.1-1.7-2.1-.6-4.3-.9-6.5-.9h0Z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
style={{ fill: "#72f5cf", strokeWidth: "0px" }}
|
||||
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,0c-8.4,0-16.3,3.3-22.3,9.2L17.4,88.9c-8.5,8.5-11.4,20.8-7.6,32.1l16.5,49.9c3.1,9.4,10.6,16.9,20,20l49.9,16.5c3.2,1.1,6.5,1.6,9.9,1.6,8.4,0,16.3-3.3,22.3-9.2l79.7-79.7c6.3-6.3,9.7-15.1,9.2-24.1l-3.8-66.5c-.8-13.9-11.9-24.9-25.7-25.7L121.2,0c-.6,0-1.2,0-1.8,0h0Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#a8f7d9", strokeWidth: "0px" }}
|
||||
d="M24.9,116.1l16.5,49.9c1.5,4.7,5.2,8.3,9.8,9.8l49.9,16.5c5.6,1.8,11.7.4,15.8-3.8l79.7-79.7c3.1-3.1,4.8-7.4,4.5-11.8l-3.8-66.5c-.3-5.8-4.9-10.3-10.7-10.7l-66.5-3.8c-4.4-.3-8.7,1.4-11.8,4.5L28.7,100.3c-4.1,4.1-5.6,10.3-3.8,15.8Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M119.4,16c.3,0,.6,0,.9,0l66.5,3.8c5.8.3,10.3,4.9,10.7,10.7l3.8,66.5c.3,4.4-1.4,8.7-4.5,11.8l-79.7,79.7c-3,3-6.9,4.5-11,4.5s-3.3-.3-4.9-.8l-49.9-16.5c-4.7-1.5-8.3-5.2-9.8-9.8l-16.5-49.9c-1.8-5.6-.4-11.7,3.8-15.8L108.4,20.5c2.9-2.9,6.9-4.5,11-4.5M119.4,6c-6.8,0-13.2,2.7-18,7.5L21.6,93.2c-6.8,6.8-9.2,16.8-6.2,26l16.5,49.9c2.5,7.6,8.6,13.7,16.2,16.2l49.9,16.5c2.6.9,5.3,1.3,8,1.3,6.8,0,13.2-2.7,18-7.5l79.7-79.7c5.1-5.1,7.8-12.2,7.4-19.5l-3.8-66.5c-.6-10.8-9.3-19.5-20.1-20.1l-66.5-3.8c-.5,0-1,0-1.5,0h0Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M136.7,173.7l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M166.5,143.9l6.9-6.9c-.2-.1-.4-.2-.6-.3l-27.6-9.1-6.9,6.9,28.3,9.3Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M43.5,80.5l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M73.3,50.7l6.9-6.9c.1.2.2.4.3.6l9.1,27.6-6.9,6.9-9.3-28.3Z"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
style={{ fill: "#79ac91", strokeWidth: "0px" }}
|
||||
d="M130.6,101.5l-97.7,97.7c-2.7,2.7-7,2.7-9.7,0l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c2.8-.3,5.1,2.1,4.9,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5Z"
|
||||
/>
|
||||
<path
|
||||
style={{ fill: "#141b1f", strokeWidth: "0px" }}
|
||||
d="M129.5,83.3c2.6,0,4.7,2.2,4.4,4.9l-.8,7.9c-.2,2.1-1.1,4-2.6,5.5l-97.7,97.7c-1.3,1.3-3.1,2-4.8,2s-3.5-.7-4.8-2l-5.2-5.2c-2.7-2.7-2.7-7,0-9.7l97.7-97.7c1.5-1.5,3.4-2.4,5.5-2.6l7.9-.8c.1,0,.3,0,.4,0M129.5,73.3h0c-.5,0-.9,0-1.4,0l-7.9.8c-4.4.4-8.5,2.4-11.5,5.5L10.9,177.3c-6.6,6.6-6.6,17.3,0,23.8l5.2,5.2c3.2,3.2,7.4,4.9,11.9,4.9s8.7-1.8,11.9-4.9l97.7-97.7c3.1-3.1,5-7.2,5.5-11.5l.8-7.9c.4-4-.9-8.1-3.7-11.1-2.7-3-6.6-4.7-10.7-4.7h0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GitlabIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
Server,
|
||||
ShieldCheck,
|
||||
Star,
|
||||
Tags,
|
||||
Trash2,
|
||||
User,
|
||||
Users,
|
||||
@@ -326,13 +325,6 @@ const MENU: Menu = {
|
||||
isSingle: true,
|
||||
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Tags",
|
||||
url: "/dashboard/settings/tags",
|
||||
icon: Tags,
|
||||
isEnabled: ({ permissions }) => !!permissions?.tag.read,
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
title: "Git",
|
||||
@@ -916,7 +908,6 @@ export default function Page({ children }: Props) {
|
||||
onOpenChange={(open) => {
|
||||
setDefaultOpen(open);
|
||||
|
||||
// biome-ignore lint/suspicious/noDocumentCookie: this sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`;
|
||||
}}
|
||||
style={
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import {
|
||||
Loader2,
|
||||
PlusIcon,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
TrashIcon,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { Loader2, PlusIcon, ShieldCheck, TrashIcon, Users } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -31,6 +24,11 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -40,11 +38,6 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
@@ -143,10 +136,6 @@ const RESOURCE_META: Record<string, { label: string; description: string }> = {
|
||||
description:
|
||||
"Manage notification providers (Slack, Discord, Telegram, etc.)",
|
||||
},
|
||||
tag: {
|
||||
label: "Tags",
|
||||
description: "Manage tags to organize and categorize projects",
|
||||
},
|
||||
member: {
|
||||
label: "Users",
|
||||
description: "Manage organization members, invitations, and roles",
|
||||
@@ -383,12 +372,6 @@ const ACTION_META: Record<
|
||||
},
|
||||
delete: { label: "Delete", description: "Remove notification providers" },
|
||||
},
|
||||
tag: {
|
||||
read: { label: "Read", description: "View tags" },
|
||||
create: { label: "Create", description: "Create new tags" },
|
||||
update: { label: "Update", description: "Edit existing tags" },
|
||||
delete: { label: "Delete", description: "Delete tags" },
|
||||
},
|
||||
member: {
|
||||
read: {
|
||||
label: "Read",
|
||||
@@ -424,116 +407,6 @@ const ACTION_META: Record<
|
||||
/** Resources that should be hidden from the custom role editor (better-auth internals) */
|
||||
const HIDDEN_RESOURCES = ["organization", "invitation", "team", "ac"];
|
||||
|
||||
/** Predefined role presets with sensible permission defaults */
|
||||
const ROLE_PRESETS: {
|
||||
name: string;
|
||||
label: string;
|
||||
description: string;
|
||||
permissions: Record<string, string[]>;
|
||||
}[] = [
|
||||
{
|
||||
name: "viewer",
|
||||
label: "Viewer",
|
||||
description: "Read-only access across all resources",
|
||||
permissions: {
|
||||
service: ["read"],
|
||||
environment: ["read"],
|
||||
docker: ["read"],
|
||||
sshKeys: ["read"],
|
||||
gitProviders: ["read"],
|
||||
traefikFiles: ["read"],
|
||||
api: ["read"],
|
||||
volume: ["read"],
|
||||
deployment: ["read"],
|
||||
envVars: ["read"],
|
||||
projectEnvVars: ["read"],
|
||||
environmentEnvVars: ["read"],
|
||||
server: ["read"],
|
||||
registry: ["read"],
|
||||
certificate: ["read"],
|
||||
backup: ["read"],
|
||||
volumeBackup: ["read"],
|
||||
schedule: ["read"],
|
||||
domain: ["read"],
|
||||
destination: ["read"],
|
||||
notification: ["read"],
|
||||
tag: ["read"],
|
||||
member: ["read"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
auditLog: ["read"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "developer",
|
||||
label: "Developer",
|
||||
description: "Deploy services, manage env vars, domains, and view logs",
|
||||
permissions: {
|
||||
project: ["create"],
|
||||
service: ["create", "read"],
|
||||
environment: ["create", "read"],
|
||||
docker: ["read"],
|
||||
gitProviders: ["read"],
|
||||
api: ["read"],
|
||||
volume: ["read", "create", "delete"],
|
||||
deployment: ["read", "create", "cancel"],
|
||||
envVars: ["read", "write"],
|
||||
projectEnvVars: ["read"],
|
||||
environmentEnvVars: ["read"],
|
||||
domain: ["read", "create", "delete"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deployer",
|
||||
label: "Deployer",
|
||||
description: "Trigger and manage deployments only",
|
||||
permissions: {
|
||||
service: ["read"],
|
||||
environment: ["read"],
|
||||
deployment: ["read", "create", "cancel"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "devops",
|
||||
label: "DevOps",
|
||||
description:
|
||||
"Full infrastructure access: servers, registries, certs, backups, and deployments",
|
||||
permissions: {
|
||||
project: ["create", "delete"],
|
||||
service: ["create", "read", "delete"],
|
||||
environment: ["create", "read", "delete"],
|
||||
docker: ["read"],
|
||||
sshKeys: ["read", "create", "delete"],
|
||||
gitProviders: ["read", "create", "delete"],
|
||||
traefikFiles: ["read", "write"],
|
||||
api: ["read"],
|
||||
volume: ["read", "create", "delete"],
|
||||
deployment: ["read", "create", "cancel"],
|
||||
envVars: ["read", "write"],
|
||||
projectEnvVars: ["read", "write"],
|
||||
environmentEnvVars: ["read", "write"],
|
||||
server: ["read", "create", "delete"],
|
||||
registry: ["read", "create", "delete"],
|
||||
certificate: ["read", "create", "delete"],
|
||||
backup: ["read", "create", "delete", "restore"],
|
||||
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||
schedule: ["read", "create", "update", "delete"],
|
||||
domain: ["read", "create", "delete"],
|
||||
destination: ["read", "create", "delete"],
|
||||
notification: ["read", "create", "delete"],
|
||||
tag: ["read", "create", "update", "delete"],
|
||||
logs: ["read"],
|
||||
monitoring: ["read"],
|
||||
auditLog: ["read"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const createRoleSchema = z.object({
|
||||
roleName: z
|
||||
.string()
|
||||
@@ -679,7 +552,7 @@ function HandleCustomRole({
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] sm:max-w-5xl overflow-y-auto space-y-2">
|
||||
<DialogContent className="max-h-[85vh] sm:max-w-5xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEdit ? "Edit Role" : "Create Custom Role"}
|
||||
@@ -714,32 +587,6 @@ function HandleCustomRole({
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
{!isEdit && (
|
||||
<div className="space-y-2 mt-4">
|
||||
<p className="text-sm font-medium flex items-center gap-1.5">
|
||||
<Sparkles className="size-3.5 text-muted-foreground" />
|
||||
Start from a preset
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{ROLE_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset.name}
|
||||
type="button"
|
||||
className="rounded-lg border p-3 text-left hover:bg-muted/50 transition-colors cursor-pointer space-y-1"
|
||||
onClick={() => {
|
||||
form.setValue("roleName", preset.name);
|
||||
setPermissions({ ...preset.permissions });
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-medium">{preset.label}</p>
|
||||
<p className="text-xs text-muted-foreground leading-snug">
|
||||
{preset.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<PermissionEditor
|
||||
resources={visibleResources}
|
||||
permissions={permissions}
|
||||
@@ -996,7 +843,7 @@ function PermissionEditor({
|
||||
onToggle: (resource: string, action: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3 mt-4">
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium">Permissions</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{resources.map(([resource, actions]) => {
|
||||
|
||||
@@ -1,635 +0,0 @@
|
||||
import type { ServiceType } from "@dokploy/server/db/schema";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircuitBoard,
|
||||
FolderInput,
|
||||
GlobeIcon,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ComponentType, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
LibsqlIcon,
|
||||
MariadbIcon,
|
||||
MongodbIcon,
|
||||
MysqlIcon,
|
||||
PostgresqlIcon,
|
||||
RedisIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
|
||||
type ProjectItem = RouterOutputs["project"]["all"][number];
|
||||
type ProjectEnvironment = ProjectItem["environments"][number];
|
||||
type EnvironmentDetails = RouterOutputs["environment"]["one"];
|
||||
|
||||
type ServiceItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ServiceType;
|
||||
};
|
||||
|
||||
type NamedService = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type EnvironmentServiceCollections = {
|
||||
applications: (NamedService & { applicationId: string })[];
|
||||
compose: (NamedService & { composeId: string })[];
|
||||
postgres: (NamedService & { postgresId: string })[];
|
||||
mysql: (NamedService & { mysqlId: string })[];
|
||||
mariadb: (NamedService & { mariadbId: string })[];
|
||||
redis: (NamedService & { redisId: string })[];
|
||||
mongo: (NamedService & { mongoId: string })[];
|
||||
libsql: (NamedService & { libsqlId: string })[];
|
||||
};
|
||||
|
||||
type ServiceCollections = Pick<
|
||||
ProjectEnvironment,
|
||||
| "applications"
|
||||
| "compose"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "mongo"
|
||||
| "libsql"
|
||||
>;
|
||||
|
||||
const SERVICE_COLLECTION_KEYS = [
|
||||
"applications",
|
||||
"compose",
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"redis",
|
||||
"mongo",
|
||||
"libsql",
|
||||
] as const satisfies ReadonlyArray<keyof ServiceCollections>;
|
||||
|
||||
const SERVICE_QUERY_KEYS = [
|
||||
"applicationId",
|
||||
"composeId",
|
||||
"postgresId",
|
||||
"mysqlId",
|
||||
"mariadbId",
|
||||
"redisId",
|
||||
"mongoId",
|
||||
"libsqlId",
|
||||
] as const;
|
||||
|
||||
const SERVICE_ICONS: Record<
|
||||
ServiceType,
|
||||
ComponentType<{ className?: string }>
|
||||
> = {
|
||||
application: GlobeIcon,
|
||||
compose: CircuitBoard,
|
||||
postgres: PostgresqlIcon,
|
||||
mysql: MysqlIcon,
|
||||
mariadb: MariadbIcon,
|
||||
redis: RedisIcon,
|
||||
mongo: MongodbIcon,
|
||||
libsql: LibsqlIcon,
|
||||
};
|
||||
|
||||
const getStringQueryParam = (value: string | string[] | undefined) =>
|
||||
typeof value === "string" ? value : null;
|
||||
|
||||
const includesSearch = (value: string | null | undefined, search: string) =>
|
||||
value?.toLowerCase().includes(search.toLowerCase()) ?? false;
|
||||
|
||||
const getServiceIcon = (type: ServiceType, className = "size-4") => {
|
||||
const Icon = SERVICE_ICONS[type];
|
||||
return <Icon className={className} />;
|
||||
};
|
||||
|
||||
const countEnvironmentServices = (environment: ServiceCollections): number =>
|
||||
SERVICE_COLLECTION_KEYS.reduce(
|
||||
(total, key) => total + environment[key].length,
|
||||
0,
|
||||
);
|
||||
|
||||
const mapServices = <T extends { name: string }>(
|
||||
items: readonly T[],
|
||||
getId: (item: T) => string,
|
||||
type: ServiceType,
|
||||
): ServiceItem[] =>
|
||||
items.map((item) => ({
|
||||
id: getId(item),
|
||||
name: item.name,
|
||||
type,
|
||||
}));
|
||||
|
||||
const extractServicesFromEnvironment = (
|
||||
environment: EnvironmentDetails | null | undefined,
|
||||
): ServiceItem[] => {
|
||||
if (!environment) return [];
|
||||
|
||||
const servicesByType =
|
||||
environment as unknown as EnvironmentServiceCollections;
|
||||
|
||||
return [
|
||||
...mapServices(
|
||||
servicesByType.applications,
|
||||
(item) => item.applicationId,
|
||||
"application",
|
||||
),
|
||||
...mapServices(servicesByType.compose, (item) => item.composeId, "compose"),
|
||||
...mapServices(
|
||||
servicesByType.postgres,
|
||||
(item) => item.postgresId,
|
||||
"postgres",
|
||||
),
|
||||
...mapServices(servicesByType.mysql, (item) => item.mysqlId, "mysql"),
|
||||
...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"),
|
||||
];
|
||||
};
|
||||
|
||||
const getTargetEnvironmentId = (
|
||||
project: ProjectItem,
|
||||
selectedEnvironmentId?: string,
|
||||
) => {
|
||||
if (selectedEnvironmentId) return selectedEnvironmentId;
|
||||
|
||||
const productionEnvironment = project.environments.find(
|
||||
(environment) => environment.name === "production",
|
||||
);
|
||||
|
||||
return (
|
||||
productionEnvironment?.environmentId ??
|
||||
project.environments[0]?.environmentId
|
||||
);
|
||||
};
|
||||
|
||||
export const AdvanceBreadcrumb = () => {
|
||||
const router = useRouter();
|
||||
const { query } = router;
|
||||
|
||||
// Read IDs from URL (dynamic route segments)
|
||||
const projectId = getStringQueryParam(query.projectId);
|
||||
const environmentId = getStringQueryParam(query.environmentId);
|
||||
const serviceId =
|
||||
SERVICE_QUERY_KEYS.map((key) => getStringQueryParam(query[key])).find(
|
||||
(value): value is string => !!value,
|
||||
) ?? null;
|
||||
|
||||
const [projectOpen, setProjectOpen] = useState(false);
|
||||
const [serviceOpen, setServiceOpen] = useState(false);
|
||||
const [environmentOpen, setEnvironmentOpen] = useState(false);
|
||||
const [projectSearch, setProjectSearch] = useState("");
|
||||
const [serviceSearch, setServiceSearch] = useState("");
|
||||
const [environmentSearch, setEnvironmentSearch] = useState("");
|
||||
const [expandedProjectId, setExpandedProjectId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Fetch all projects
|
||||
const { data: allProjects } = api.project.all.useQuery();
|
||||
|
||||
// Fetch current project data
|
||||
const { data: currentProject } = api.project.one.useQuery(
|
||||
{ projectId: projectId ?? "" },
|
||||
{ enabled: !!projectId },
|
||||
);
|
||||
|
||||
// Fetch current environment
|
||||
const { data: currentEnvironment } = api.environment.one.useQuery(
|
||||
{ environmentId: environmentId ?? "" },
|
||||
{ enabled: !!environmentId },
|
||||
);
|
||||
|
||||
// Fetch environments for current project
|
||||
const { data: projectEnvironments } = api.environment.byProjectId.useQuery(
|
||||
{ projectId: projectId ?? "" },
|
||||
{ enabled: !!projectId },
|
||||
);
|
||||
|
||||
// Close dropdowns on escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setProjectOpen(false);
|
||||
setServiceOpen(false);
|
||||
setEnvironmentOpen(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
const services = useMemo(
|
||||
() => extractServicesFromEnvironment(currentEnvironment),
|
||||
[currentEnvironment],
|
||||
);
|
||||
|
||||
const currentService = useMemo(
|
||||
() => services.find((service) => service.id === serviceId),
|
||||
[serviceId, services],
|
||||
);
|
||||
|
||||
// Navigate to project's default environment
|
||||
const handleProjectSelect = (
|
||||
selectedProjectId: string,
|
||||
selectedEnvironmentId?: string,
|
||||
) => {
|
||||
const project = allProjects?.find((p) => p.projectId === selectedProjectId);
|
||||
if (project) {
|
||||
const targetEnvironmentId = getTargetEnvironmentId(
|
||||
project,
|
||||
selectedEnvironmentId,
|
||||
);
|
||||
|
||||
if (targetEnvironmentId) {
|
||||
router.push(
|
||||
`/dashboard/project/${selectedProjectId}/environment/${targetEnvironmentId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
setProjectOpen(false);
|
||||
setExpandedProjectId(null);
|
||||
};
|
||||
|
||||
// Navigate to environment
|
||||
const handleEnvironmentSelect = (envId: string) => {
|
||||
router.push(`/dashboard/project/${projectId}/environment/${envId}`);
|
||||
setEnvironmentOpen(false);
|
||||
};
|
||||
|
||||
// Navigate to service
|
||||
const handleServiceSelect = (service: ServiceItem) => {
|
||||
if (!environmentId) return;
|
||||
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`,
|
||||
);
|
||||
setServiceOpen(false);
|
||||
};
|
||||
|
||||
const filteredProjects = useMemo(
|
||||
() =>
|
||||
(allProjects ?? []).filter(
|
||||
(project) =>
|
||||
includesSearch(project.name, projectSearch) ||
|
||||
includesSearch(project.description, projectSearch),
|
||||
),
|
||||
[allProjects, projectSearch],
|
||||
);
|
||||
|
||||
const filteredServices = useMemo(
|
||||
() =>
|
||||
services.filter((service) => includesSearch(service.name, serviceSearch)),
|
||||
[serviceSearch, services],
|
||||
);
|
||||
|
||||
const filteredEnvironments = useMemo(
|
||||
() =>
|
||||
(projectEnvironments ?? []).filter((environment) =>
|
||||
includesSearch(environment.name, environmentSearch),
|
||||
),
|
||||
[environmentSearch, projectEnvironments],
|
||||
);
|
||||
|
||||
// If we're just on the projects page, show simple breadcrumb
|
||||
if (!projectId) {
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderInput className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium">Projects</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
|
||||
<div className="flex items-center">
|
||||
{/* Project Selector */}
|
||||
<Popover open={projectOpen} onOpenChange={setProjectOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-expanded={projectOpen}
|
||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||
>
|
||||
<FolderInput className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium max-w-[150px] truncate">
|
||||
{currentProject?.name || "Select Project"}
|
||||
</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[380px] p-0"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
placeholder="Find Project..."
|
||||
value={projectSearch}
|
||||
onValueChange={setProjectSearch}
|
||||
className="w-full focus-visible:ring-0"
|
||||
/>
|
||||
<kbd className="pointer-events-none h-5 absolute right-2 top-1/2 -translate-y-1/2 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 flex">
|
||||
Esc
|
||||
</kbd>
|
||||
</div>
|
||||
<CommandList>
|
||||
<CommandEmpty>No projects found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<ScrollArea className="h-[300px]">
|
||||
{filteredProjects.map((project) => {
|
||||
const totalServices = project.environments.reduce(
|
||||
(total, env) => total + countEnvironmentServices(env),
|
||||
0,
|
||||
);
|
||||
const isSelected = project.projectId === projectId;
|
||||
const isExpanded =
|
||||
expandedProjectId === project.projectId;
|
||||
|
||||
return (
|
||||
<div key={project.projectId}>
|
||||
<CommandItem
|
||||
value={project.projectId}
|
||||
onSelect={() => {
|
||||
if (project.environments.length > 1) {
|
||||
setExpandedProjectId(
|
||||
isExpanded ? null : project.projectId,
|
||||
);
|
||||
} else {
|
||||
handleProjectSelect(project.projectId);
|
||||
}
|
||||
}}
|
||||
className="flex items-center justify-between py-3 px-2 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center size-8 rounded-md bg-muted text-xs font-semibold uppercase">
|
||||
{project.name.slice(0, 2)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{project.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
{project.environments.length} env
|
||||
{project.environments.length !== 1
|
||||
? "s"
|
||||
: ""}{" "}
|
||||
· {totalServices} service
|
||||
{totalServices !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isSelected && (
|
||||
<Check className="size-4 text-primary" />
|
||||
)}
|
||||
{project.environments.length > 1 && (
|
||||
<ChevronRight
|
||||
className={`size-4 text-muted-foreground transition-transform ${isExpanded ? "rotate-90" : ""}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
|
||||
{/* Expanded environments */}
|
||||
{isExpanded && (
|
||||
<div className="ml-11 border-l pl-3 py-1 space-y-1">
|
||||
{project.environments.map((env) => {
|
||||
const envServices =
|
||||
countEnvironmentServices(env);
|
||||
const isEnvSelected =
|
||||
env.environmentId === environmentId;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={env.environmentId}
|
||||
value={env.environmentId}
|
||||
onSelect={() =>
|
||||
handleProjectSelect(
|
||||
project.projectId,
|
||||
env.environmentId,
|
||||
)
|
||||
}
|
||||
className="flex items-center justify-between py-2 px-2 cursor-pointer text-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-xs">{env.name}</p>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{envServices} service
|
||||
{envServices !== 1 ? "s" : ""}
|
||||
</span>
|
||||
</div>
|
||||
{isEnvSelected && (
|
||||
<Check className="size-3 text-primary" />
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Environment Selector */}
|
||||
{projectEnvironments && projectEnvironments.length > 1 && (
|
||||
<Popover open={environmentOpen} onOpenChange={setEnvironmentOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-expanded={environmentOpen}
|
||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||
>
|
||||
<span className="font-medium max-w-[150px] truncate">
|
||||
{currentEnvironment?.name || "production"}
|
||||
</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[350px] p-0"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
placeholder="Find Environment..."
|
||||
value={environmentSearch}
|
||||
onValueChange={setEnvironmentSearch}
|
||||
className="w-full focus-visible:ring-0"
|
||||
/>
|
||||
<kbd className="pointer-events-none h-5 absolute right-2 top-1/2 -translate-y-1/2 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 flex">
|
||||
Esc
|
||||
</kbd>
|
||||
</div>
|
||||
<CommandList>
|
||||
<CommandEmpty>No environments found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<ScrollArea className="h-[300px]">
|
||||
{filteredEnvironments.map((env) => {
|
||||
const isSelected =
|
||||
env.environmentId === environmentId;
|
||||
return (
|
||||
<CommandItem
|
||||
key={env.environmentId}
|
||||
value={env.environmentId}
|
||||
onSelect={() =>
|
||||
handleEnvironmentSelect(env.environmentId)
|
||||
}
|
||||
className="flex items-center justify-between py-2 cursor-pointer"
|
||||
>
|
||||
<span className="font-medium">{env.name}</span>
|
||||
{isSelected && (
|
||||
<Check className="size-4 text-primary" />
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
{projectEnvironments && projectEnvironments.length === 1 && (
|
||||
<p className="text-sm font-normal ml-1">
|
||||
{currentEnvironment?.name || "production"}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Service Selector - only show when viewing a service */}
|
||||
{serviceId && currentService && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-6" />
|
||||
|
||||
<Popover open={serviceOpen} onOpenChange={setServiceOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-expanded={serviceOpen}
|
||||
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
|
||||
>
|
||||
{getServiceIcon(currentService.type)}
|
||||
<span className="font-medium max-w-[150px] truncate">
|
||||
{currentService.name}
|
||||
</span>
|
||||
<ChevronDown className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-[350px] p-0"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
placeholder="Find Service..."
|
||||
value={serviceSearch}
|
||||
onValueChange={setServiceSearch}
|
||||
className="w-full focus-visible:ring-0"
|
||||
/>
|
||||
<kbd className="pointer-events-none h-5 select-none absolute right-2 top-1/2 -translate-y-1/2 items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 flex">
|
||||
Esc
|
||||
</kbd>
|
||||
</div>
|
||||
<CommandList>
|
||||
<CommandEmpty>No services found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<ScrollArea className="h-[300px]">
|
||||
{filteredServices.map((service) => {
|
||||
const isSelected = service.id === serviceId;
|
||||
return (
|
||||
<CommandItem
|
||||
key={service.id}
|
||||
value={service.id}
|
||||
onSelect={() => handleServiceSelect(service)}
|
||||
className="flex items-center justify-between py-2 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center justify-center size-8 rounded-md bg-muted">
|
||||
{getServiceIcon(service.type)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">
|
||||
{service.name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground capitalize">
|
||||
{service.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<Check className="size-4 text-primary" />
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* Close button to go back to environment */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 ml-1"
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<X className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -8,7 +8,6 @@ interface Props {
|
||||
export const Logo = ({ className = "size-14", logoUrl }: Props) => {
|
||||
if (logoUrl) {
|
||||
return (
|
||||
// biome-ignore lint/performance/noImgElement: this is for dynamic logo loading
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt="Organization Logo"
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TagBadgeProps {
|
||||
name: string;
|
||||
color?: string | null;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function TagBadge({ name, color, className, children }: TagBadgeProps) {
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: color ? `${color}33` : undefined,
|
||||
color: color || undefined,
|
||||
borderColor: color ? `${color}66` : undefined,
|
||||
}}
|
||||
className={cn("border", className)}
|
||||
>
|
||||
{name}
|
||||
{children}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Tags } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { HandleTag } from "@/components/dashboard/settings/tags/handle-tag";
|
||||
import { TagBadge } from "@/components/shared/tag-badge";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface TagFilterProps {
|
||||
tags: Tag[];
|
||||
selectedTags: string[];
|
||||
onTagsChange: (tagIds: string[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TagFilter({
|
||||
tags,
|
||||
selectedTags,
|
||||
onTagsChange,
|
||||
className,
|
||||
}: TagFilterProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleTagToggle = (tagId: string) => {
|
||||
if (selectedTags.includes(tagId)) {
|
||||
onTagsChange(selectedTags.filter((id) => id !== tagId));
|
||||
} else {
|
||||
onTagsChange([...selectedTags, tagId]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAll = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onTagsChange([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("gap-2", selectedTags.length > 0 && "border-primary")}
|
||||
>
|
||||
<Tags className="h-4 w-4" />
|
||||
<span>Tags</span>
|
||||
{selectedTags.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1 px-1 py-0">
|
||||
{selectedTags.length}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 p-0" align="start">
|
||||
<Command>
|
||||
<div className="flex items-center border-b px-3">
|
||||
<CommandInput
|
||||
placeholder="Search tags..."
|
||||
className="h-9 focus-visible:ring-0"
|
||||
/>
|
||||
{selectedTags.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClearAll}
|
||||
className="h-8 px-2 text-xs"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<div className="flex flex-col items-center gap-2 py-1">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No tags found.
|
||||
</span>
|
||||
<HandleTag />
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tags.map((tag) => {
|
||||
const isSelected = selectedTags.includes(tag.id);
|
||||
return (
|
||||
<CommandItem
|
||||
key={tag.id}
|
||||
onSelect={() => handleTagToggle(tag.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
className="mr-2"
|
||||
onCheckedChange={() => handleTagToggle(tag.id)}
|
||||
/>
|
||||
<TagBadge name={tag.name} color={tag.color} />
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { Check, ChevronsUpDown, X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { HandleTag } from "@/components/dashboard/settings/tags/handle-tag";
|
||||
import { TagBadge } from "@/components/shared/tag-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
name: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface TagSelectorProps {
|
||||
tags: Tag[];
|
||||
selectedTags: string[];
|
||||
onTagsChange: (tagIds: string[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function TagSelector({
|
||||
tags,
|
||||
selectedTags,
|
||||
onTagsChange,
|
||||
placeholder = "Select tags...",
|
||||
className,
|
||||
disabled = false,
|
||||
}: TagSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleTagToggle = (tagId: string) => {
|
||||
if (selectedTags.includes(tagId)) {
|
||||
onTagsChange(selectedTags.filter((id) => id !== tagId));
|
||||
} else {
|
||||
onTagsChange([...selectedTags, tagId]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagRemove = (tagId: string, e?: React.MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
onTagsChange(selectedTags.filter((id) => id !== tagId));
|
||||
};
|
||||
|
||||
const selectedTagObjects = tags.filter((tag) =>
|
||||
selectedTags.includes(tag.id),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"w-full justify-between min-h-10 h-auto bg-input",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1 flex-1">
|
||||
{selectedTagObjects.length > 0 ? (
|
||||
selectedTagObjects.map((tag) => (
|
||||
<TagBadge
|
||||
key={tag.id}
|
||||
name={tag.name}
|
||||
color={tag.color}
|
||||
className="flex items-center gap-1 pr-1"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleTagRemove(tag.id, e)}
|
||||
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
disabled={disabled}
|
||||
>
|
||||
<X className="h-3 w-3 hover:opacity-70" />
|
||||
<span className="sr-only">Remove {tag.name}</span>
|
||||
</button>
|
||||
</TagBadge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted-foreground">{placeholder}</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search tags..."
|
||||
className="focus-visible:ring-0"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<div className="flex flex-col items-center gap-2 py-1">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
No tags found.
|
||||
</span>
|
||||
<HandleTag />
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tags.map((tag) => {
|
||||
const isSelected = selectedTags.includes(tag.id);
|
||||
return (
|
||||
<CommandItem
|
||||
key={tag.id}
|
||||
onSelect={() => handleTagToggle(tag.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
className="mr-2"
|
||||
onCheckedChange={() => handleTagToggle(tag.id)}
|
||||
/>
|
||||
<TagBadge
|
||||
name={tag.name}
|
||||
color={tag.color}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
isSelected ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
"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",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -82,7 +82,7 @@ const SidebarProvider = React.forwardRef<
|
||||
|
||||
_setOpen(value);
|
||||
|
||||
// biome-ignore lint/suspicious/noDocumentCookie: This sets the cookie to keep the sidebar state.
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
ALTER TABLE "apikey" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD COLUMN "config_id" text DEFAULT 'default' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD COLUMN "reference_id" text;--> statement-breakpoint
|
||||
UPDATE "apikey" SET "reference_id" = "user_id" WHERE "reference_id" IS NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ALTER COLUMN "reference_id" SET NOT NULL;
|
||||
@@ -1,4 +0,0 @@
|
||||
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_reference_id_user_id_fk" FOREIGN KEY ("reference_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" DROP COLUMN "user_id";
|
||||
@@ -1,19 +0,0 @@
|
||||
CREATE TABLE "project_tag" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"projectId" text NOT NULL,
|
||||
"tagId" text NOT NULL,
|
||||
CONSTRAINT "unique_project_tag" UNIQUE("projectId","tagId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "tag" (
|
||||
"tagId" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"color" text,
|
||||
"createdAt" text NOT NULL,
|
||||
"organizationId" text NOT NULL,
|
||||
CONSTRAINT "unique_org_tag_name" UNIQUE("organizationId","name")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "project_tag" ADD CONSTRAINT "project_tag_projectId_project_projectId_fk" FOREIGN KEY ("projectId") REFERENCES "public"."project"("projectId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "project_tag" ADD CONSTRAINT "project_tag_tagId_tag_tagId_fk" FOREIGN KEY ("tagId") REFERENCES "public"."tag"("tagId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "tag" ADD CONSTRAINT "tag_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -1,49 +0,0 @@
|
||||
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1051,34 +1051,6 @@
|
||||
"when": 1773637297592,
|
||||
"tag": "0149_rare_radioactive_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 150,
|
||||
"version": "7",
|
||||
"when": 1773870095817,
|
||||
"tag": "0150_nappy_blue_blade",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 151,
|
||||
"version": "7",
|
||||
"when": 1773872561300,
|
||||
"tag": "0151_modern_sunfire",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 152,
|
||||
"version": "7",
|
||||
"when": 1773903778014,
|
||||
"tag": "0152_odd_firelord",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 153,
|
||||
"version": "7",
|
||||
"when": 1774322599182,
|
||||
"tag": "0153_motionless_mastermind",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,12 +6,11 @@ import { useCallback, useEffect, useState } from "react";
|
||||
const PAGES = [
|
||||
"compose",
|
||||
"application",
|
||||
"libsql",
|
||||
"mariadb",
|
||||
"mongodb",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"redis",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongodb",
|
||||
] as const;
|
||||
type Page = (typeof PAGES)[number];
|
||||
|
||||
@@ -64,12 +63,11 @@ const REDIS_SHORTCUTS: Shortcuts = {
|
||||
const SHORTCUTS: ShortcutsDictionary = {
|
||||
application: APPLICATION_SHORTCUTS,
|
||||
compose: COMPOSE_SHORTCUTS,
|
||||
libsql: POSTGRES_SHORTCUTS,
|
||||
mariadb: POSTGRES_SHORTCUTS,
|
||||
mongodb: POSTGRES_SHORTCUTS,
|
||||
mysql: POSTGRES_SHORTCUTS,
|
||||
postgres: POSTGRES_SHORTCUTS,
|
||||
redis: REDIS_SHORTCUTS,
|
||||
mysql: POSTGRES_SHORTCUTS,
|
||||
mariadb: POSTGRES_SHORTCUTS,
|
||||
mongodb: POSTGRES_SHORTCUTS,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.28.8",
|
||||
"version": "v0.28.6",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -57,7 +57,7 @@
|
||||
"@codemirror/search": "^6.6.0",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.18",
|
||||
"@dokploy/trpc-openapi": "0.0.17",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
@@ -120,7 +120,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
"next": "^16.2.0",
|
||||
"next": "^16.1.6",
|
||||
"next-themes": "^0.2.1",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-os-utils": "2.0.1",
|
||||
|
||||
@@ -38,5 +38,3 @@ export const redirectWithError = (res: NextApiResponse, error: string) => {
|
||||
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
|
||||
);
|
||||
};
|
||||
|
||||
export default findGitea;
|
||||
|
||||
@@ -37,7 +37,6 @@ 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,
|
||||
@@ -45,8 +44,8 @@ import {
|
||||
RedisIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||
@@ -112,8 +111,7 @@ export type Services = {
|
||||
| "mysql"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose"
|
||||
| "libsql";
|
||||
| "compose";
|
||||
description?: string | null;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -250,27 +248,14 @@ 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) => {
|
||||
@@ -398,8 +383,7 @@ const EnvironmentPage = (
|
||||
(currentEnvironment.postgres?.length || 0) === 0 &&
|
||||
(currentEnvironment.redis?.length || 0) === 0 &&
|
||||
(currentEnvironment.applications?.length || 0) === 0 &&
|
||||
(currentEnvironment.compose?.length || 0) === 0 &&
|
||||
(currentEnvironment.libsql?.length || 0) === 0);
|
||||
(currentEnvironment.compose?.length || 0) === 0);
|
||||
|
||||
const applications = extractServicesFromEnvironment(currentEnvironment);
|
||||
|
||||
@@ -412,7 +396,6 @@ 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[]>([]);
|
||||
@@ -878,7 +861,18 @@ const EnvironmentPage = (
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: projectData?.name || "",
|
||||
},
|
||||
{
|
||||
name: currentEnvironment.name,
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Environment: {currentEnvironment.name} | {projectData?.name} |{" "}
|
||||
@@ -1542,9 +1536,6 @@ const EnvironmentPage = (
|
||||
{service.type === "compose" && (
|
||||
<CircuitBoard className="h-6 w-6" />
|
||||
)}
|
||||
{service.type === "libsql" && (
|
||||
<LibsqlIcon className="h-6 w-6" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</CardTitle>
|
||||
|
||||
@@ -35,7 +35,7 @@ import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||
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 { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -108,7 +108,22 @@ const Service = (
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="application" />
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Application: {data?.name} - {data?.environment.project.name} |{" "}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { ShowBackups } from "@/components/dashboard/database/backups/show-backup
|
||||
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
|
||||
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -97,7 +97,22 @@ const Service = (
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="compose" />
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Compose: {data?.name} - {data?.environment?.project?.name} | {appName}
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
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: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -78,7 +78,22 @@ const Mariadb = (
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="mariadb" />
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Head>
|
||||
<title>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -77,7 +77,22 @@ const Mongo = (
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="mongodb" />
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -76,7 +76,22 @@ const MySql = (
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="mysql" />
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Head>
|
||||
<title>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres"
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -76,7 +76,22 @@ const Postgresql = (
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="postgres" />
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -76,7 +76,22 @@ const Redis = (
|
||||
return (
|
||||
<div className="pb-10">
|
||||
<UseKeyboardNav forPage="redis" />
|
||||
<AdvanceBreadcrumb />
|
||||
<BreadcrumbSidebar
|
||||
list={[
|
||||
{ name: "Projects", href: "/dashboard/projects" },
|
||||
{
|
||||
name: data?.environment?.project?.name || "",
|
||||
href: `/dashboard/project/${projectId}/environment/${environmentId}`,
|
||||
},
|
||||
{
|
||||
name: data?.environment?.name || "",
|
||||
dropdownItems: environmentDropdownItems,
|
||||
},
|
||||
{
|
||||
name: data?.name || "",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Head>
|
||||
<title>
|
||||
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { validateRequest } from "@dokploy/server";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
import { TagManager } from "@/components/dashboard/settings/tags/tag-manager";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<TagManager />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
Page.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout>{page}</DashboardLayout>;
|
||||
};
|
||||
export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
try {
|
||||
await helpers.user.get.prefetch();
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||
|
||||
if (!userPermissions?.tag.read) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ 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";
|
||||
@@ -24,15 +23,15 @@ import { mysqlRouter } from "./routers/mysql";
|
||||
import { notificationRouter } from "./routers/notification";
|
||||
import { organizationRouter } from "./routers/organization";
|
||||
import { patchRouter } from "./routers/patch";
|
||||
import { portRouter } from "./routers/port";
|
||||
import { postgresRouter } from "./routers/postgres";
|
||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||
import { projectRouter } from "./routers/project";
|
||||
import { auditLogRouter } from "./routers/proprietary/audit-log";
|
||||
import { customRoleRouter } from "./routers/proprietary/custom-role";
|
||||
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||
import { ssoRouter } from "./routers/proprietary/sso";
|
||||
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
||||
import { portRouter } from "./routers/port";
|
||||
import { postgresRouter } from "./routers/postgres";
|
||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||
import { projectRouter } from "./routers/project";
|
||||
import { redirectsRouter } from "./routers/redirects";
|
||||
import { redisRouter } from "./routers/redis";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
@@ -44,7 +43,6 @@ import { settingsRouter } from "./routers/settings";
|
||||
import { sshRouter } from "./routers/ssh-key";
|
||||
import { stripeRouter } from "./routers/stripe";
|
||||
import { swarmRouter } from "./routers/swarm";
|
||||
import { tagRouter } from "./routers/tag";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { volumeBackupsRouter } from "./routers/volume-backups";
|
||||
/**
|
||||
@@ -55,40 +53,39 @@ import { volumeBackupsRouter } from "./routers/volume-backups";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
admin: adminRouter,
|
||||
application: applicationRouter,
|
||||
backup: backupRouter,
|
||||
bitbucket: bitbucketRouter,
|
||||
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,
|
||||
application: applicationRouter,
|
||||
mysql: mysqlRouter,
|
||||
postgres: postgresRouter,
|
||||
redis: redisRouter,
|
||||
registry: registryRouter,
|
||||
security: securityRouter,
|
||||
server: serverRouter,
|
||||
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,
|
||||
github: githubRouter,
|
||||
server: serverRouter,
|
||||
stripe: stripeRouter,
|
||||
swarm: swarmRouter,
|
||||
user: userRouter,
|
||||
ai: aiRouter,
|
||||
organization: organizationRouter,
|
||||
licenseKey: licenseKeyRouter,
|
||||
@@ -100,7 +97,6 @@ export const appRouter = createTRPCRouter({
|
||||
rollback: rollbackRouter,
|
||||
volumeBackups: volumeBackupsRouter,
|
||||
environment: environmentRouter,
|
||||
tag: tagRouter,
|
||||
patch: patchRouter,
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { zfd } from "zod-form-data";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
@@ -770,17 +769,21 @@ export const applicationRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
dropDeployment: protectedProcedure
|
||||
.input(
|
||||
zfd.formData({
|
||||
applicationId: z.string(),
|
||||
zip: zfd.file(),
|
||||
dropBuildPath: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.meta({
|
||||
openapi: {
|
||||
path: "/drop-deployment",
|
||||
method: "POST",
|
||||
override: true,
|
||||
enabled: false,
|
||||
},
|
||||
})
|
||||
.input(z.instanceof(FormData))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const zipFile = input.zip;
|
||||
const applicationId = input.applicationId;
|
||||
const dropBuildPath = input.dropBuildPath ?? null;
|
||||
const formData = input;
|
||||
|
||||
const zipFile = formData.get("zip") as File;
|
||||
const applicationId = formData.get("applicationId") as string;
|
||||
const dropBuildPath = formData.get("dropBuildPath") as string | null;
|
||||
|
||||
await checkServicePermissionAndAccess(ctx, applicationId, {
|
||||
deployment: ["create"],
|
||||
|
||||
@@ -3,8 +3,6 @@ import {
|
||||
findBackupById,
|
||||
findComposeByBackupId,
|
||||
findComposeById,
|
||||
findLibsqlByBackupId,
|
||||
findLibsqlById,
|
||||
findMariadbByBackupId,
|
||||
findMariadbById,
|
||||
findMongoByBackupId,
|
||||
@@ -18,7 +16,6 @@ import {
|
||||
keepLatestNBackups,
|
||||
removeBackupById,
|
||||
removeScheduleBackup,
|
||||
runLibsqlBackup,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
@@ -39,7 +36,6 @@ import {
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import {
|
||||
restoreComposeBackup,
|
||||
restoreLibsqlBackup,
|
||||
restoreMariadbBackup,
|
||||
restoreMongoBackup,
|
||||
restoreMySqlBackup,
|
||||
@@ -86,7 +82,6 @@ export const backupRouter = createTRPCRouter({
|
||||
input.mysqlId ||
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.libsqlId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -108,8 +103,6 @@ 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
|
||||
@@ -161,7 +154,6 @@ export const backupRouter = createTRPCRouter({
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.libsqlId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -181,7 +173,6 @@ export const backupRouter = createTRPCRouter({
|
||||
existing.mysqlId ||
|
||||
existing.mariadbId ||
|
||||
existing.mongoId ||
|
||||
existing.libsqlId ||
|
||||
existing.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -238,7 +229,6 @@ export const backupRouter = createTRPCRouter({
|
||||
backup.mysqlId ||
|
||||
backup.mariadbId ||
|
||||
backup.mongoId ||
|
||||
backup.libsqlId ||
|
||||
backup.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -410,33 +400,6 @@ 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 }) => {
|
||||
@@ -573,12 +536,6 @@ 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,12 +38,6 @@ 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),
|
||||
),
|
||||
@@ -59,6 +53,9 @@ 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({
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
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,7 +3,6 @@ import {
|
||||
deleteMount,
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findLibsqlById,
|
||||
findMariadbById,
|
||||
findMongoById,
|
||||
findMountById,
|
||||
@@ -14,14 +13,13 @@ 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,
|
||||
@@ -30,6 +28,7 @@ import {
|
||||
apiUpdateMount,
|
||||
} from "@/server/db/schema";
|
||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { audit } from "@/server/api/utils/audit";
|
||||
|
||||
async function getServiceOrganizationId(
|
||||
serviceId: string,
|
||||
@@ -64,10 +63,6 @@ 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;
|
||||
}
|
||||
@@ -100,7 +95,6 @@ export const mountRouter = createTRPCRouter({
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.libsqlId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -126,7 +120,6 @@ export const mountRouter = createTRPCRouter({
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.libsqlId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -146,7 +139,6 @@ export const mountRouter = createTRPCRouter({
|
||||
mount.mongoId ||
|
||||
mount.mysqlId ||
|
||||
mount.redisId ||
|
||||
mount.libsqlId ||
|
||||
mount.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -177,6 +169,7 @@ 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,7 +3,6 @@ import {
|
||||
createBackup,
|
||||
createCompose,
|
||||
createDomain,
|
||||
createLibsql,
|
||||
createMariadb,
|
||||
createMongo,
|
||||
createMount,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
findApplicationById,
|
||||
findComposeById,
|
||||
findEnvironmentById,
|
||||
findLibsqlById,
|
||||
findMariadbById,
|
||||
findMongoById,
|
||||
findMySqlById,
|
||||
@@ -30,7 +28,6 @@ import {
|
||||
IS_CLOUD,
|
||||
updateProjectById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
addNewEnvironment,
|
||||
addNewProject,
|
||||
@@ -38,16 +35,17 @@ 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,
|
||||
@@ -56,7 +54,6 @@ import {
|
||||
applications,
|
||||
compose,
|
||||
environments,
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
@@ -141,9 +138,6 @@ export const projectRouter = createTRPCRouter({
|
||||
accessedServices,
|
||||
),
|
||||
},
|
||||
libsql: {
|
||||
where: buildServiceFilter(libsql.libsqlId, accessedServices),
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(
|
||||
mariadb.mariadbId,
|
||||
@@ -167,11 +161,6 @@ export const projectRouter = createTRPCRouter({
|
||||
},
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
with: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -233,14 +222,6 @@ 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: {
|
||||
@@ -299,11 +280,6 @@ export const projectRouter = createTRPCRouter({
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
with: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
});
|
||||
@@ -352,11 +328,6 @@ export const projectRouter = createTRPCRouter({
|
||||
composeStatus: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
@@ -364,11 +335,6 @@ export const projectRouter = createTRPCRouter({
|
||||
isDefault: true,
|
||||
},
|
||||
},
|
||||
projectTags: {
|
||||
with: {
|
||||
tag: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(projects.createdAt),
|
||||
@@ -469,17 +435,6 @@ export const projectRouter = createTRPCRouter({
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
libsql: {
|
||||
columns: {
|
||||
libsqlId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -652,13 +607,12 @@ export const projectRouter = createTRPCRouter({
|
||||
id: z.string(),
|
||||
type: z.enum([
|
||||
"application",
|
||||
"compose",
|
||||
"libsql",
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"mysql",
|
||||
"postgres",
|
||||
"redis",
|
||||
"compose",
|
||||
]),
|
||||
}),
|
||||
)
|
||||
@@ -802,27 +756,21 @@ export const projectRouter = createTRPCRouter({
|
||||
|
||||
break;
|
||||
}
|
||||
case "compose": {
|
||||
const {
|
||||
composeId,
|
||||
mounts,
|
||||
domains,
|
||||
appName,
|
||||
refreshToken,
|
||||
...compose
|
||||
} = await findComposeById(id);
|
||||
case "postgres": {
|
||||
const { postgresId, mounts, backups, appName, ...postgres } =
|
||||
await findPostgresById(id);
|
||||
|
||||
const newAppName = appName.substring(
|
||||
0,
|
||||
appName.lastIndexOf("-"),
|
||||
);
|
||||
|
||||
const newCompose = await createCompose({
|
||||
...compose,
|
||||
const newPostgres = await createPostgres({
|
||||
...postgres,
|
||||
appName: newAppName,
|
||||
name: input.duplicateInSameProject
|
||||
? `${compose.name} (copy)`
|
||||
: compose.name,
|
||||
? `${postgres.name} (copy)`
|
||||
: postgres.name,
|
||||
environmentId: targetProject?.environmentId || "",
|
||||
});
|
||||
|
||||
@@ -830,49 +778,18 @@ export const projectRouter = createTRPCRouter({
|
||||
const { mountId, ...rest } = mount;
|
||||
await createMount({
|
||||
...rest,
|
||||
serviceId: newCompose.composeId,
|
||||
serviceType: "compose",
|
||||
serviceId: newPostgres.postgresId,
|
||||
serviceType: "postgres",
|
||||
});
|
||||
}
|
||||
|
||||
for (const domain of domains) {
|
||||
const { domainId, ...rest } = domain;
|
||||
await createDomain({
|
||||
for (const backup of backups) {
|
||||
const { backupId, appName: _appName, ...rest } = backup;
|
||||
await createBackup({
|
||||
...rest,
|
||||
composeId: newCompose.composeId,
|
||||
domainType: "compose",
|
||||
postgresId: newPostgres.postgresId,
|
||||
});
|
||||
}
|
||||
|
||||
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": {
|
||||
@@ -983,42 +900,6 @@ 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);
|
||||
@@ -1046,6 +927,50 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ import {
|
||||
CLEANUP_CRON_JOB,
|
||||
checkGPUStatus,
|
||||
checkPortInUse,
|
||||
checkPostgresHealth,
|
||||
checkRedisHealth,
|
||||
checkTraefikHealth,
|
||||
cleanupAll,
|
||||
cleanupAllBackground,
|
||||
cleanupBuilders,
|
||||
@@ -47,8 +44,8 @@ import {
|
||||
writeTraefikConfigInPath,
|
||||
writeTraefikSetup,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { checkPermission } from "@dokploy/server/services/permission";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
@@ -648,7 +645,6 @@ export const settingsRouter = createTRPCRouter({
|
||||
"postgres",
|
||||
"redis",
|
||||
"mongo",
|
||||
"libsql",
|
||||
"mariadb",
|
||||
"sshRouter",
|
||||
"gitProvider",
|
||||
@@ -657,18 +653,6 @@ export const settingsRouter = createTRPCRouter({
|
||||
"github",
|
||||
"gitlab",
|
||||
"gitea",
|
||||
"tag",
|
||||
"patch",
|
||||
"server",
|
||||
"volumeBackups",
|
||||
"environment",
|
||||
"auditLog",
|
||||
"customRole",
|
||||
"whitelabeling",
|
||||
"sso",
|
||||
"licenseKey",
|
||||
"organization",
|
||||
"previewDeployment",
|
||||
],
|
||||
});
|
||||
|
||||
@@ -880,23 +864,6 @@ export const settingsRouter = createTRPCRouter({
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
checkInfrastructureHealth: adminProcedure.query(async () => {
|
||||
if (IS_CLOUD) {
|
||||
return {
|
||||
postgres: { status: "healthy" as const },
|
||||
redis: { status: "healthy" as const },
|
||||
traefik: { status: "healthy" as const },
|
||||
};
|
||||
}
|
||||
|
||||
const [postgres, redis, traefik] = await Promise.all([
|
||||
checkPostgresHealth(),
|
||||
checkRedisHealth(),
|
||||
checkTraefikHealth(),
|
||||
]);
|
||||
|
||||
return { postgres, redis, traefik };
|
||||
}),
|
||||
setupGPU: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -21,12 +21,7 @@ import {
|
||||
STARTUP_PRODUCT_ID,
|
||||
WEBSITE_URL,
|
||||
} from "@/server/utils/stripe";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "../trpc";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
|
||||
@@ -319,18 +314,16 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { ok: true };
|
||||
}),
|
||||
|
||||
canCreateMoreServers: withPermission("server", "create").query(
|
||||
async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const servers = await findServersByUserId(user.id);
|
||||
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const servers = await findServersByUserId(user.id);
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
if (!IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return servers.length < user.serversQuantity;
|
||||
},
|
||||
),
|
||||
return servers.length < user.serversQuantity;
|
||||
}),
|
||||
|
||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
import { findMemberByUserId } from "@dokploy/server/services/permission";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
apiCreateTag,
|
||||
apiFindOneTag,
|
||||
apiRemoveTag,
|
||||
apiUpdateTag,
|
||||
projects,
|
||||
projectTags,
|
||||
tags,
|
||||
} from "@/server/db/schema";
|
||||
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
|
||||
|
||||
export const tagRouter = createTRPCRouter({
|
||||
create: withPermission("tag", "create")
|
||||
.input(apiCreateTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const newTag = await db
|
||||
.insert(tags)
|
||||
.values({
|
||||
name: input.name,
|
||||
color: input.color,
|
||||
organizationId: ctx.session.activeOrganizationId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newTag[0];
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("unique_org_tag_name")
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "A tag with this name already exists in your organization",
|
||||
});
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error creating tag: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
all: protectedProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
const organizationTags = await db.query.tags.findMany({
|
||||
where: eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: (tags, { asc }) => [asc(tags.name)],
|
||||
});
|
||||
|
||||
return organizationTags;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error fetching tags: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
one: protectedProcedure.input(apiFindOneTag).query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const tag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
eq(tags.tagId, input.tagId),
|
||||
eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Tag not found",
|
||||
});
|
||||
}
|
||||
|
||||
return tag;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error fetching tag: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
update: withPermission("tag", "update")
|
||||
.input(apiUpdateTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// First verify the tag belongs to the user's organization
|
||||
const existingTag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
eq(tags.tagId, input.tagId),
|
||||
eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!existingTag) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Tag not found or you don't have permission to update it",
|
||||
});
|
||||
}
|
||||
|
||||
const updatedTag = await db
|
||||
.update(tags)
|
||||
.set({
|
||||
...(input.name !== undefined && { name: input.name }),
|
||||
...(input.color !== undefined && { color: input.color }),
|
||||
})
|
||||
.where(eq(tags.tagId, input.tagId))
|
||||
.returning();
|
||||
|
||||
return updatedTag[0];
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("unique_org_tag_name")
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "A tag with this name already exists in your organization",
|
||||
});
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error updating tag: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
remove: withPermission("tag", "delete")
|
||||
.input(apiRemoveTag)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
// First verify the tag belongs to the user's organization
|
||||
const existingTag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
eq(tags.tagId, input.tagId),
|
||||
eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!existingTag) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Tag not found or you don't have permission to delete it",
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the tag - cascade delete will handle projectTags associations
|
||||
await db.delete(tags).where(eq(tags.tagId, input.tagId));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error deleting tag: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
assignToProject: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
tagId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const memberRecord = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
// Verify the project belongs to the user's organization
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: and(
|
||||
eq(projects.projectId, input.projectId),
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"Project not found or you don't have permission to modify it",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the member has access to the project
|
||||
if (
|
||||
memberRecord.role !== "owner" &&
|
||||
memberRecord.role !== "admin" &&
|
||||
!memberRecord.accessedProjects.includes(input.projectId)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the tag belongs to the user's organization
|
||||
const tag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
eq(tags.tagId, input.tagId),
|
||||
eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Tag not found or you don't have permission to use it",
|
||||
});
|
||||
}
|
||||
|
||||
// Insert the project-tag association
|
||||
const newAssociation = await db
|
||||
.insert(projectTags)
|
||||
.values({
|
||||
projectId: input.projectId,
|
||||
tagId: input.tagId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newAssociation[0];
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("unique_project_tag")
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "This tag is already assigned to this project",
|
||||
});
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error assigning tag to project: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
removeFromProject: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
tagId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const memberRecord = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
// Verify the project belongs to the user's organization
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: and(
|
||||
eq(projects.projectId, input.projectId),
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"Project not found or you don't have permission to modify it",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the member has access to the project
|
||||
if (
|
||||
memberRecord.role !== "owner" &&
|
||||
memberRecord.role !== "admin" &&
|
||||
!memberRecord.accessedProjects.includes(input.projectId)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the tag belongs to the user's organization
|
||||
const tag = await db.query.tags.findFirst({
|
||||
where: and(
|
||||
eq(tags.tagId, input.tagId),
|
||||
eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!tag) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Tag not found or you don't have permission to use it",
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the project-tag association
|
||||
await db
|
||||
.delete(projectTags)
|
||||
.where(
|
||||
and(
|
||||
eq(projectTags.projectId, input.projectId),
|
||||
eq(projectTags.tagId, input.tagId),
|
||||
),
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error removing tag from project: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
bulkAssign: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string().min(1),
|
||||
tagIds: z.array(z.string().min(1)),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const memberRecord = await findMemberByUserId(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
// Verify the project belongs to the user's organization
|
||||
const project = await db.query.projects.findFirst({
|
||||
where: and(
|
||||
eq(projects.projectId, input.projectId),
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message:
|
||||
"Project not found or you don't have permission to modify it",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the member has access to the project
|
||||
if (
|
||||
memberRecord.role !== "owner" &&
|
||||
memberRecord.role !== "admin" &&
|
||||
!memberRecord.accessedProjects.includes(input.projectId)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this project",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify all tags belong to the user's organization
|
||||
if (input.tagIds.length > 0) {
|
||||
const tagCount = await db.query.tags.findMany({
|
||||
where: and(
|
||||
eq(tags.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
|
||||
const validTagIds = tagCount.map((tag) => tag.tagId);
|
||||
const invalidTags = input.tagIds.filter(
|
||||
(id) => !validTagIds.includes(id),
|
||||
);
|
||||
|
||||
if (invalidTags.length > 0) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "One or more tags not found in your organization",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all existing tag associations for this project
|
||||
await db
|
||||
.delete(projectTags)
|
||||
.where(eq(projectTags.projectId, input.projectId));
|
||||
|
||||
// Insert new tag associations
|
||||
if (input.tagIds.length > 0) {
|
||||
await db.insert(projectTags).values(
|
||||
input.tagIds.map((tagId) => ({
|
||||
projectId: input.projectId,
|
||||
tagId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `Error bulk assigning tags to project: ${error instanceof Error ? error.message : error}`,
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -465,7 +465,7 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (apiKeyToDelete.referenceId !== ctx.user.id) {
|
||||
if (apiKeyToDelete.userId !== ctx.user.id) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this API key",
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
updateVolumeBackupSchema,
|
||||
volumeBackups,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import {
|
||||
execAsyncRemote,
|
||||
execAsyncStream,
|
||||
@@ -26,6 +25,7 @@ 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,7 +41,6 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
"mongo",
|
||||
"redis",
|
||||
"compose",
|
||||
"libsql",
|
||||
]),
|
||||
}),
|
||||
)
|
||||
@@ -59,7 +58,6 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
mongo: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
libsql: true,
|
||||
},
|
||||
orderBy: [desc(volumeBackups.createdAt)],
|
||||
});
|
||||
@@ -74,7 +72,6 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
input.mariadbId ||
|
||||
input.mongoId ||
|
||||
input.redisId ||
|
||||
input.libsqlId ||
|
||||
input.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -116,7 +113,6 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.libsqlId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -140,7 +136,6 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.libsqlId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -166,7 +161,6 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
existingVb.mariadbId ||
|
||||
existingVb.mongoId ||
|
||||
existingVb.redisId ||
|
||||
existingVb.libsqlId ||
|
||||
existingVb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
@@ -226,7 +220,6 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
vb.mariadbId ||
|
||||
vb.mongoId ||
|
||||
vb.redisId ||
|
||||
vb.libsqlId ||
|
||||
vb.composeId;
|
||||
if (serviceId) {
|
||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { exit } from "node:process";
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
keepLatestNBackups,
|
||||
runCommand,
|
||||
runComposeBackup,
|
||||
runLibsqlBackup,
|
||||
runMariadbBackup,
|
||||
runMongoBackup,
|
||||
runMySqlBackup,
|
||||
@@ -39,7 +38,6 @@ export const runJobs = async (job: QueueJob) => {
|
||||
mysql,
|
||||
mongo,
|
||||
mariadb,
|
||||
libsql,
|
||||
compose,
|
||||
backupType,
|
||||
} = backup;
|
||||
@@ -77,14 +75,6 @@ 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);
|
||||
@@ -151,7 +141,6 @@ export const initializeJobs = async () => {
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
mongo: true,
|
||||
libsql: true,
|
||||
compose: true,
|
||||
},
|
||||
});
|
||||
|
||||
70175
openapi.json
70175
openapi.json
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,7 @@
|
||||
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;
|
||||
@@ -12,79 +10,20 @@ export const DOKPLOY_DOCKER_PORT = process.env.DOKPLOY_DOCKER_PORT
|
||||
: undefined;
|
||||
|
||||
export const CLEANUP_CRON_JOB = "50 23 * * *";
|
||||
|
||||
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();
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
||||
// 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";
|
||||
|
||||
|
||||
@@ -214,8 +214,7 @@ export const apikey = pgTable("apikey", {
|
||||
start: text("start"),
|
||||
prefix: text("prefix"),
|
||||
key: text("key").notNull(),
|
||||
configId: text("config_id").default("default").notNull(),
|
||||
referenceId: text("reference_id")
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refill_interval"),
|
||||
@@ -237,7 +236,7 @@ export const apikey = pgTable("apikey", {
|
||||
|
||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [apikey.referenceId],
|
||||
fields: [apikey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -15,7 +15,6 @@ 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";
|
||||
@@ -27,7 +26,6 @@ export const databaseType = pgEnum("databaseType", [
|
||||
"mysql",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export const backupType = pgEnum("backupType", ["database", "compose"]);
|
||||
@@ -76,9 +74,6 @@ 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<
|
||||
@@ -123,10 +118,6 @@ 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],
|
||||
@@ -146,19 +137,11 @@ 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",
|
||||
"libsql",
|
||||
]),
|
||||
databaseType: z.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"]),
|
||||
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(),
|
||||
});
|
||||
@@ -174,7 +157,6 @@ export const apiCreateBackup = createSchema.pick({
|
||||
mysqlId: true,
|
||||
postgresId: true,
|
||||
mongoId: true,
|
||||
libsqlId: true,
|
||||
databaseType: true,
|
||||
userId: true,
|
||||
backupType: true,
|
||||
@@ -210,14 +192,7 @@ export const apiUpdateBackup = createSchema
|
||||
|
||||
export const apiRestoreBackup = z.object({
|
||||
databaseId: z.string(),
|
||||
databaseType: z.enum([
|
||||
"postgres",
|
||||
"mysql",
|
||||
"mariadb",
|
||||
"mongo",
|
||||
"web-server",
|
||||
"libsql",
|
||||
]),
|
||||
databaseType: z.enum(["postgres", "mysql", "mariadb", "mongo", "web-server"]),
|
||||
backupType: z.enum(["database", "compose"]),
|
||||
databaseName: z.string().min(1),
|
||||
backupFile: z.string().min(1),
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -37,13 +36,12 @@ export const environmentRelations = relations(
|
||||
references: [projects.projectId],
|
||||
}),
|
||||
applications: many(applications),
|
||||
compose: many(compose),
|
||||
libsql: many(libsql),
|
||||
mariadb: many(mariadb),
|
||||
mongo: many(mongo),
|
||||
mysql: many(mysql),
|
||||
postgres: many(postgres),
|
||||
mysql: many(mysql),
|
||||
redis: many(redis),
|
||||
mongo: many(mongo),
|
||||
compose: many(compose),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ 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";
|
||||
@@ -36,7 +35,6 @@ export * from "./session";
|
||||
export * from "./shared";
|
||||
export * from "./ssh-key";
|
||||
export * from "./sso";
|
||||
export * from "./tag";
|
||||
export * from "./user";
|
||||
export * from "./utils";
|
||||
export * from "./volume-backups";
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
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();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user