Compare commits

..

1 Commits

Author SHA1 Message Date
dosubot[bot]
d9b41a258e docs: add comprehensive API and environment setup guide 2026-03-19 04:51:41 +00:00
156 changed files with 1648 additions and 39880 deletions

View File

@@ -16,6 +16,7 @@ jobs:
steps:
- uses: peakoss/anti-slop@v0
with:
max-failures: 4
blocked-commit-authors: "claude,copilot"
require-description: true
min-account-age: 5

View File

@@ -99,14 +99,7 @@ pnpm run dokploy:build
## Docker
To build the docker image first run commands to copy .env files
```bash
cp apps/dokploy/.env.production.example .env.production
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
```
then run build command
To build the docker image
```bash
pnpm run docker:build

View File

@@ -19,8 +19,8 @@ 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.
- **Backups**: Automate backups for databases to external storage destinations (S3, SFTP, FTP, Google Drive).
- **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.
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.

View File

@@ -6,3 +6,387 @@ npm run dev
```
open http://localhost:3000
```
## Environment Variables
The API server requires the following environment variables for configuration:
### Inngest Configuration
Required for the GET /jobs endpoint to list deployment jobs:
- **INNGEST_BASE_URL** - The base URL for the Inngest instance
- Self-hosted: `http://localhost:8288`
- Production: `https://dev-inngest.dokploy.com`
- **INNGEST_SIGNING_KEY** - The signing key for authenticating with Inngest
Optional configuration for filtering and pagination:
- **INNGEST_EVENTS_RECEIVED_AFTER** (optional) - An RFC3339 timestamp to filter events received after a specific date (e.g., `2024-01-01T00:00:00Z`). If unset, no date filter is applied.
- **INNGEST_JOBS_MAX_EVENTS** (optional) - Maximum number of events to fetch when listing jobs. Default is 100, maximum is 10000. Used for pagination with cursor.
### Lemon Squeezy Integration
- **LEMON_SQUEEZY_API_KEY** - API key for Lemon Squeezy integration
- **LEMON_SQUEEZY_STORE_ID** - Store ID for Lemon Squeezy integration
### Docker Configuration
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.
- **DOKPLOY_DOCKER_HOST** (optional) - Specifies the Docker daemon host to connect to. If not set, uses the default Docker socket connection.
- **DOKPLOY_DOCKER_PORT** (optional) - Specifies the port for connecting to the Docker daemon. If not set, uses the default port.
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
### GET /jobs
Lists deployment jobs (Inngest runs) for a specified server.
**Query Parameters:**
- `serverId` (required) - The ID of the server to list deployment jobs for
**Response:**
Returns an array of deployment job objects with the same shape as BullMQ queue jobs:
```json
[
{
"id": "string",
"name": "string",
"data": {},
"timestamp": 0,
"processedOn": 0,
"finishedOn": 0,
"failedReason": "string",
"state": "string"
}
]
```
**Error Responses:**
- `400` - serverId is not provided
- `503` - INNGEST_BASE_URL is not configured
- `200` - Empty array on other errors
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.
### application.search
Search applications across name, appName, description, repository, owner, and dockerImage fields.
**Query Parameters:**
- `q` (optional string) - General search term that searches across name, appName, description, repository, owner, and dockerImage
- `name` (optional string) - Filter by application name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `repository` (optional string) - Filter by repository
- `owner` (optional string) - Filter by owner
- `dockerImage` (optional string) - Filter by Docker image
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum number of results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"applicationId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### compose.search
Search compose services with filtering by name, appName, and description.
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"composeId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"composeStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### environment.search
Search environments by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"environmentId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"env": "string",
"projectId": "string",
"isDefault": true
}
],
"total": 0
}
```
### project.search
Search projects by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"projectId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"organizationId": "string",
"env": "string"
}
],
"total": 0
}
```
### Database Service Search Endpoints
The following database services all share the same search interface:
- **postgres.search**
- **mysql.search**
- **mariadb.search**
- **mongo.search**
- **redis.search**
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"postgresId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"createdAt": "string"
}
],
"total": 0
}
```
*Note: The response shape is similar across all database services, with the ID field varying (e.g., `mysqlId`, `mariadbId`, `mongoId`, `redisId`).*
**Search Behavior:**
- All searches use case-insensitive pattern matching with wildcards
- Results are ordered by creation date (descending)
- Members only see services they have access to
- Returns total count for pagination UI
## Database Service Update Endpoints
All database services support update operations with flexible configuration options. The following database services share a common update interface:
- **postgres.update** (apiUpdatePostgres)
- **mysql.update** (apiUpdateMySql)
- **mariadb.update** (apiUpdateMariaDB)
- **mongo.update** (apiUpdateMongo)
- **redis.update** (apiUpdateRedis)
**Common 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.
**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).
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.
## Whitelabeling Endpoints
The whitelabeling endpoints allow enterprise/self-hosted Dokploy instances to customize branding, logos, colors, and UI appearance. These endpoints are only available in self-hosted mode (not cloud).
### whitelabeling.get
Get the current whitelabeling configuration.
**Requirements:**
- Enterprise license required
- Only available for self-hosted (not cloud)
**Response:**
Returns the whitelabeling configuration object or null if not configured.
```json
{
"appName": "string | null",
"appDescription": "string | null",
"logoUrl": "string | null",
"faviconUrl": "string | null",
"primaryColor": "string | null",
"customCss": "string | null",
"loginLogoUrl": "string | null",
"supportUrl": "string | null",
"docsUrl": "string | null",
"errorPageTitle": "string | null",
"errorPageDescription": "string | null",
"metaTitle": "string | null",
"footerText": "string | null"
}
```
### whitelabeling.update
Update the whitelabeling configuration.
**Requirements:**
- Enterprise license required
- Owner role required
- Only available for self-hosted (not cloud)
**Input:**
```json
{
"whitelabelingConfig": {
"appName": "string | null",
"appDescription": "string | null",
"logoUrl": "string | null",
"faviconUrl": "string | null",
"primaryColor": "string | null",
"customCss": "string | null",
"loginLogoUrl": "string | null",
"supportUrl": "string | null",
"docsUrl": "string | null",
"errorPageTitle": "string | null",
"errorPageDescription": "string | null",
"metaTitle": "string | null",
"footerText": "string | null"
}
}
```
**Response:**
```json
{
"success": true
}
```
### whitelabeling.reset
Reset whitelabeling configuration to default values (all fields set to null).
**Requirements:**
- Enterprise license required
- Owner role required
- Only available for self-hosted (not cloud)
**Response:**
```json
{
"success": true
}
```
### whitelabeling.getPublic
Public endpoint to fetch whitelabeling configuration. This endpoint can be accessed without authentication, allowing the whitelabeling settings to be applied globally (including on the login page before auth).
**Requirements:**
- No authentication required
- Only available for self-hosted (not cloud)
**Response:**
Returns the whitelabeling configuration object or null if not configured. Response shape is identical to `whitelabeling.get`.

View File

@@ -35,7 +35,6 @@ const ENTERPRISE_RESOURCES = [
"domain",
"destination",
"notification",
"tag",
"logs",
"monitoring",
"auditLog",

View File

@@ -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) => {

View File

@@ -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:

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 () => {

View File

@@ -71,7 +71,6 @@ const formSchema = z
"mongo",
"mysql",
"redis",
"libsql",
]),
serviceName: z.string(),
destinationId: z.string().min(1, "Destination required"),

View File

@@ -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,

View File

@@ -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}
/>

View File

@@ -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 />

View File

@@ -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(),
}
: {

View File

@@ -1,66 +0,0 @@
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string;
}
export const RemoveContainerDialog = ({ containerId, serverId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, isPending } = api.docker.removeContainer.useMutation();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
onSelect={(e) => e.preventDefault()}
>
Remove Container
</DropdownMenuItem>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently remove the container{" "}
<span className="font-semibold">{containerId}</span>. If the
container is running, it will be forcefully stopped and removed.
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={isPending}
onClick={async () => {
await mutateAsync({ containerId, serverId })
.then(async () => {
toast.success("Container removed successfully");
await utils.docker.getContainers.invalidate();
})
.catch((err) => {
toast.error(err.message);
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -10,7 +10,6 @@ import {
} from "@/components/ui/dropdown-menu";
import { ShowContainerConfig } from "../config/show-container-config";
import { ShowDockerModalLogs } from "../logs/show-docker-modal-logs";
import { RemoveContainerDialog } from "../remove/remove-container";
import { DockerTerminalModal } from "../terminal/docker-terminal-modal";
import type { Container } from "./show-containers";
@@ -128,10 +127,6 @@ export const columns: ColumnDef<Container>[] = [
>
Terminal
</DockerTerminalModal>
<RemoveContainerDialog
containerId={container.containerId}
serverId={container.serverId ?? undefined}
/>
</DropdownMenuContent>
</DropdownMenu>
);

View File

@@ -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>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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>
);
};

View File

@@ -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 />

View File

@@ -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");

View File

@@ -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"

View File

@@ -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(

View File

@@ -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("");

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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++];
}
}

View File

@@ -1,7 +1,7 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -35,10 +35,6 @@ import {
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "@dokploy/server/db/validations/destination";
import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({
@@ -50,16 +46,6 @@ const addDestination = z.object({
region: z.string(),
endpoint: z.string().min(1, "Endpoint is required"),
serverId: z.string().optional(),
additionalFlags: z
.array(
z.object({
value: z
.string()
.min(1, "Flag cannot be empty")
.regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR),
}),
)
.optional(),
});
type AddDestination = z.infer<typeof addDestination>;
@@ -103,16 +89,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: "",
secretAccessKey: "",
endpoint: "",
additionalFlags: [],
},
resolver: zodResolver(addDestination),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "additionalFlags",
});
useEffect(() => {
if (destination) {
form.reset({
@@ -123,8 +102,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
bucket: destination.bucket,
region: destination.region,
endpoint: destination.endpoint,
additionalFlags:
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
});
} else {
form.reset();
@@ -141,7 +118,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: data.region,
secretAccessKey: data.secretAccessKey,
destinationId: destinationId || "",
additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [],
})
.then(async () => {
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
@@ -151,12 +127,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
}
setOpen(false);
})
.catch((e) => {
.catch(() => {
toast.error(
`Error ${destinationId ? "Updating" : "Creating"} the Destination`,
{
description: e.message,
},
);
});
};
@@ -168,7 +141,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
"secretAccessKey",
"bucket",
"endpoint",
"additionalFlags",
]);
if (!result) {
@@ -207,8 +179,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region,
secretAccessKey: secretKey,
serverId,
additionalFlags:
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
})
.then(() => {
toast.success("Connection Success");
@@ -388,48 +358,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
</FormItem>
)}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FormLabel>Additional Flags (Optional)</FormLabel>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => append({ value: "" })}
>
<PlusIcon className="size-4" />
Add Flag
</Button>
</div>
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`additionalFlags.${index}.value`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormControl>
<Input
placeholder="--s3-sign-accept-encoding=false"
{...field}
/>
</FormControl>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
</form>
<DialogFooter

View File

@@ -12,7 +12,6 @@ import { toast } from "sonner";
import { z } from "zod";
import {
DiscordIcon,
MattermostIcon,
GotifyIcon,
LarkIcon,
NtfyIcon,
@@ -135,14 +134,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
priority: z.number().min(1).max(5).default(3),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("mattermost"),
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
channel: z.string().optional(),
username: z.string().optional(),
})
.merge(notificationBaseSchema),
z
.object({
type: z.literal("pushover"),
@@ -219,10 +210,6 @@ export const notificationsMap = {
icon: <NtfyIcon />,
label: "ntfy",
},
mattermost: {
icon: <MattermostIcon />,
label: "Mattermost",
},
pushover: {
icon: <PushoverIcon />,
label: "Pushover",
@@ -266,16 +253,14 @@ export const HandleNotifications = ({ notificationId }: Props) => {
api.notification.testGotifyConnection.useMutation();
const { mutateAsync: testNtfyConnection, isPending: isLoadingNtfy } =
api.notification.testNtfyConnection.useMutation();
const {
mutateAsync: testMattermostConnection,
isPending: isLoadingMattermost,
} = api.notification.testMattermostConnection.useMutation();
const { mutateAsync: testLarkConnection, isPending: isLoadingLark } =
api.notification.testLarkConnection.useMutation();
const { mutateAsync: testTeamsConnection, isPending: isLoadingTeams } =
api.notification.testTeamsConnection.useMutation();
const { mutateAsync: testCustomConnection, isPending: isLoadingCustom } =
api.notification.testCustomConnection.useMutation();
const { mutateAsync: testPushoverConnection, isPending: isLoadingPushover } =
api.notification.testPushoverConnection.useMutation();
@@ -303,9 +288,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const ntfyMutation = notificationId
? api.notification.updateNtfy.useMutation()
: api.notification.createNtfy.useMutation();
const mattermostMutation = notificationId
? api.notification.updateMattermost.useMutation()
: api.notification.createMattermost.useMutation();
const larkMutation = notificationId
? api.notification.updateLark.useMutation()
: api.notification.createLark.useMutation();
@@ -456,21 +438,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "mattermost") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
webhookUrl: notification.mattermost?.webhookUrl,
channel: notification.mattermost?.channel || "",
username: notification.mattermost?.username || "",
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "lark") {
form.reset({
appBuildError: notification.appBuildError,
@@ -549,7 +516,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
resend: resendMutation,
gotify: gotifyMutation,
ntfy: ntfyMutation,
mattermost: mattermostMutation,
lark: larkMutation,
teams: teamsMutation,
custom: customMutation,
@@ -680,22 +646,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "",
ntfyId: notification?.ntfyId || "",
});
} else if (data.type === "mattermost") {
promise = mattermostMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
channel: data.channel || undefined,
username: data.username || undefined,
name: data.name,
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
mattermostId: notification?.mattermostId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "lark") {
promise = larkMutation.mutateAsync({
appBuildError: appBuildError,
@@ -1456,62 +1406,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
/>
</>
)}
{type === "mattermost" && (
<>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://your-mattermost.com/hooks/xxx-generatedkey-xxx"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="channel"
render={({ field }) => (
<FormItem>
<FormLabel>Channel</FormLabel>
<FormControl>
<Input placeholder="deployments" {...field} />
</FormControl>
<FormDescription>
Optional. Channel to post to (without #).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Dokploy" {...field} />
</FormControl>
<FormDescription>
Optional. Display name for the webhook.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{type === "custom" && (
<div className="space-y-4">
<FormField
@@ -1598,7 +1492,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
</div>
</div>
)}
{type === "lark" && (
<>
<FormField
@@ -1959,7 +1852,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingResend ||
isLoadingGotify ||
isLoadingNtfy ||
isLoadingMattermost ||
isLoadingLark ||
isLoadingTeams ||
isLoadingCustom ||
@@ -2019,12 +1911,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
accessToken: data.accessToken || "",
priority: data.priority ?? 0,
});
} else if (data.type === "mattermost") {
await testMattermostConnection({
webhookUrl: data.webhookUrl,
channel: data.channel || undefined,
username: data.username || undefined,
});
} else if (data.type === "lark") {
await testLarkConnection({
webhookUrl: data.webhookUrl,

View File

@@ -4,7 +4,6 @@ import {
DiscordIcon,
GotifyIcon,
LarkIcon,
MattermostIcon,
NtfyIcon,
ResendIcon,
SlackIcon,
@@ -122,12 +121,6 @@ export const ShowNotifications = () => {
<TeamsIcon className="size-7 text-muted-foreground" />
</div>
)}
{notification.notificationType ===
"mattermost" && (
<div className="flex items-center justify-center rounded-lg">
<MattermostIcon className="size-7" />
</div>
)}
{notification.name}
</span>

View File

@@ -20,7 +20,6 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -410,10 +409,7 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
<FormControl>
<Input placeholder="root" {...field} />
</FormControl>
<FormDescription>
Use &quot;root&quot; or a non-root user with passwordless
sudo access.
</FormDescription>
<FormMessage />
</FormItem>
)}

View File

@@ -118,10 +118,9 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
</div>
) : (
<div id="hook-form-add-gitlab" className="grid w-full gap-4">
<AlertBlock type="info">
You can connect as root or as a non-root user with passwordless
sudo access. If using a non-root user, ensure passwordless sudo is
configured.
<AlertBlock type="warning">
Using a root user is required to ensure everything works as
expected.
</AlertBlock>
<Tabs defaultValue="ssh-keys">

View File

@@ -163,29 +163,6 @@ export const ValidateServer = ({ serverId }: Props) => {
: "Not Created"
}
/>
<StatusRow
label="Privilege Mode"
isEnabled={
data?.privilegeMode === "root" ||
data?.privilegeMode === "sudo"
}
description={
data?.privilegeMode === "root"
? "Running as root"
: data?.privilegeMode === "sudo"
? "Running with sudo"
: "No sudo access (required for non-root)"
}
/>
<StatusRow
label="Docker Group"
isEnabled={data?.dockerGroupMember}
description={
data?.dockerGroupMember
? "User is in docker group"
: "User is not in docker group"
}
/>
</div>
</div>
</div>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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) => {

View File

@@ -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");

View File

@@ -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} />

View File

@@ -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

View File

@@ -88,21 +88,6 @@ export const DiscordIcon = ({ className }: Props) => {
</svg>
);
};
export const MattermostIcon = ({ className }: Props) => {
return (
<svg
fill="#0061ff"
viewBox="0 0 501 501"
xmlns="http://www.w3.org/2000/svg"
className={cn("size-8", className)}
>
<path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z" />
<path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z" />
</svg>
);
};
export const TeamsIcon = ({ className }: Props) => {
return (
<svg

View File

@@ -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={

View File

@@ -143,10 +143,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 +379,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",
@@ -457,7 +447,6 @@ const ROLE_PRESETS: {
domain: ["read"],
destination: ["read"],
notification: ["read"],
tag: ["read"],
member: ["read"],
logs: ["read"],
monitoring: ["read"],
@@ -526,7 +515,6 @@ const ROLE_PRESETS: {
domain: ["read", "create", "delete"],
destination: ["read", "create", "delete"],
notification: ["read", "create", "delete"],
tag: ["read", "create", "update", "delete"],
logs: ["read"],
monitoring: ["read"],
auditLog: ["read"],

View File

@@ -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>
);
};

View File

@@ -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"

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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}

View File

@@ -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],

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,10 +0,0 @@
ALTER TYPE "public"."notificationType" ADD VALUE 'mattermost' BEFORE 'pushover';--> statement-breakpoint
CREATE TABLE "mattermost" (
"mattermostId" text PRIMARY KEY NOT NULL,
"webhookUrl" text NOT NULL,
"channel" text,
"username" text
);
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "mattermostId" text;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_mattermostId_mattermost_mattermostId_fk" FOREIGN KEY ("mattermostId") REFERENCES "public"."mattermost"("mattermostId") ON DELETE cascade ON UPDATE no action;

View File

@@ -1 +0,0 @@
ALTER TABLE "destination" ADD COLUMN "additionalFlags" text[];

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

View File

@@ -1065,34 +1065,6 @@
"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
},
{
"idx": 154,
"version": "7",
"when": 1774337356154,
"tag": "0154_careful_eternals",
"breakpoints": true
},
{
"idx": 155,
"version": "7",
"when": 1774794547865,
"tag": "0155_careless_clea",
"breakpoints": true
}
]
}

View File

@@ -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,
};
/**

View File

@@ -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",

View File

@@ -38,5 +38,3 @@ export const redirectWithError = (res: NextApiResponse, error: string) => {
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
);
};
export default findGitea;

View File

@@ -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>

View File

@@ -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} |{" "}

View File

@@ -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}

View File

@@ -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: "/",
},
};
}

View File

@@ -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>

View File

@@ -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} |{" "}

View File

@@ -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>

View File

@@ -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} |{" "}

View File

@@ -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} |{" "}

View File

@@ -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: {},
};
}
}

View File

@@ -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,
});

View File

@@ -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);

View File

@@ -47,15 +47,8 @@ export const destinationRouter = createTRPCRouter({
testConnection: withPermission("destination", "create")
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const {
secretAccessKey,
bucket,
region,
endpoint,
accessKey,
provider,
additionalFlags,
} = input;
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
input;
try {
const rcloneFlags = [
`--s3-access-key-id="${accessKey}"`,
@@ -72,9 +65,6 @@ export const destinationRouter = createTRPCRouter({
if (provider) {
rcloneFlags.unshift(`--s3-provider="${provider}"`);
}
if (additionalFlags?.length) {
rcloneFlags.push(...additionalFlags);
}
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
@@ -169,14 +159,7 @@ export const destinationRouter = createTRPCRouter({
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error?.message
: "Error connecting to bucket",
cause: error,
});
throw error;
}
}),
});

View File

@@ -1,5 +1,4 @@
import {
containerRemove,
containerRestart,
findServerById,
getConfig,
@@ -53,32 +52,6 @@ export const dockerRouter = createTRPCRouter({
return result;
}),
removeContainer: withPermission("docker", "read")
.input(
z.object({
containerId: z
.string()
.min(1)
.regex(containerIdRegex, "Invalid container id."),
serverId: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
if (server.organizationId !== ctx.session?.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
}
await containerRemove(input.containerId, input.serverId);
await audit(ctx, {
action: "delete",
resourceType: "docker",
resourceId: input.containerId,
resourceName: input.containerId,
});
}),
getConfig: withPermission("docker", "read")
.input(
z.object({

View File

@@ -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({

View File

@@ -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;
}),
});

View File

@@ -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,

View File

@@ -4,7 +4,6 @@ import {
createEmailNotification,
createGotifyNotification,
createLarkNotification,
createMattermostNotification,
createNtfyNotification,
createPushoverNotification,
createResendNotification,
@@ -20,7 +19,6 @@ import {
sendEmailNotification,
sendGotifyNotification,
sendLarkNotification,
sendMattermostNotification,
sendNtfyNotification,
sendPushoverNotification,
sendResendNotification,
@@ -33,7 +31,6 @@ import {
updateEmailNotification,
updateGotifyNotification,
updateLarkNotification,
updateMattermostNotification,
updateNtfyNotification,
updatePushoverNotification,
updateResendNotification,
@@ -57,7 +54,6 @@ import {
apiCreateEmail,
apiCreateGotify,
apiCreateLark,
apiCreateMattermost,
apiCreateNtfy,
apiCreatePushover,
apiCreateResend,
@@ -70,7 +66,6 @@ import {
apiTestEmailConnection,
apiTestGotifyConnection,
apiTestLarkConnection,
apiTestMattermostConnection,
apiTestNtfyConnection,
apiTestPushoverConnection,
apiTestResendConnection,
@@ -82,7 +77,6 @@ import {
apiUpdateEmail,
apiUpdateGotify,
apiUpdateLark,
apiUpdateMattermost,
apiUpdateNtfy,
apiUpdatePushover,
apiUpdateResend,
@@ -479,7 +473,6 @@ export const notificationRouter = createTRPCRouter({
resend: true,
gotify: true,
ntfy: true,
mattermost: true,
custom: true,
lark: true,
pushover: true,
@@ -682,74 +675,6 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
createMattermost: withPermission("notification", "create")
.input(apiCreateMattermost)
.mutation(async ({ input, ctx }) => {
try {
await createMattermostNotification(
input,
ctx.session.activeOrganizationId,
);
await audit(ctx, {
action: "create",
resourceType: "notification",
resourceName: input.name,
});
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the notification",
cause: error,
});
}
}),
updateMattermost: withPermission("notification", "update")
.input(apiUpdateMattermost)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (
IS_CLOUD &&
notification.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
const result = await updateMattermostNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
await audit(ctx, {
action: "update",
resourceType: "notification",
resourceId: input.notificationId,
resourceName: notification.name,
});
return result;
} catch (error) {
throw error;
}
}),
testMattermostConnection: withPermission("notification", "create")
.input(apiTestMattermostConnection)
.mutation(async ({ input }) => {
try {
await sendMattermostNotification(input, {
text: "Hi, From Dokploy 👋",
channel: input.channel,
username: input.username || "Dokploy Bot",
});
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
cause: error,
});
}
}),
createCustom: withPermission("notification", "create")
.input(apiCreateCustom)
.mutation(async ({ input, ctx }) => {

View File

@@ -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;
}
}

View File

@@ -252,8 +252,6 @@ export const serverRouter = createTRPCRouter({
isDokployNetworkInstalled: boolean;
isSwarmInstalled: boolean;
isMainDirectoryInstalled: boolean;
privilegeMode: string;
dockerGroupMember: boolean;
};
} catch (error) {
throw new TRPCError({

View File

@@ -648,7 +648,6 @@ export const settingsRouter = createTRPCRouter({
"postgres",
"redis",
"mongo",
"libsql",
"mariadb",
"sshRouter",
"gitProvider",
@@ -657,18 +656,6 @@ export const settingsRouter = createTRPCRouter({
"github",
"gitlab",
"gitea",
"tag",
"patch",
"server",
"volumeBackups",
"environment",
"auditLog",
"customRole",
"whitelabeling",
"sso",
"licenseKey",
"organization",
"previewDeployment",
],
});

View File

@@ -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,
});
}
}),
});

View File

@@ -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, {

View File

@@ -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";

Some files were not shown because too many files have changed in this diff Show More