fix(ui): improve select component behavior and styling across various providers

- Added a check to prevent empty values from being processed in the onValueChange handler for Bitbucket, Gitea, GitHub, and GitLab providers.
- Removed unnecessary defaultValue prop from Select components to streamline the code.
- Updated button styles to remove background color for better consistency across the UI.
- Enhanced volume backup selection to display a message when no volumes are found.

This update enhances user experience by ensuring that empty selections are handled gracefully and improves the overall visual consistency of the UI components.
This commit is contained in:
Mauricio Siu
2026-07-06 03:27:52 -06:00
parent 2440e8f803
commit 8db1250487
29 changed files with 451 additions and 124 deletions

View File

@@ -1,6 +1,7 @@
import {
createBackup,
findBackupById,
findBackupsByDbId,
findComposeByBackupId,
findComposeById,
findLibsqlByBackupId,
@@ -55,6 +56,7 @@ import {
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { assertDatabaseBackupLimit } from "@/server/api/utils/plan-limits";
import {
apiCreateBackup,
apiFindOneBackup,
@@ -94,6 +96,22 @@ export const backupRouter = createTRPCRouter({
});
}
if (IS_CLOUD) {
const dbType = (
["postgres", "mysql", "mariadb", "mongo", "libsql"] as const
).find((type) => input[`${type}Id`]);
if (dbType) {
const existingBackups = await findBackupsByDbId(
input[`${dbType}Id`]!,
dbType,
);
await assertDatabaseBackupLimit(
ctx.session.activeOrganizationId,
existingBackups.length,
);
}
}
const newBackup = await createBackup(input);
const backup = await findBackupById(newBackup.backupId);

View File

@@ -2,8 +2,10 @@ import {
createEnvironment,
deleteEnvironment,
duplicateEnvironment,
filterEnvironmentServices,
findEnvironmentById,
findEnvironmentsByProjectId,
IS_CLOUD,
updateEnvironmentById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
@@ -20,6 +22,7 @@ import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { assertEnvironmentLimit } from "@/server/api/utils/plan-limits";
import {
apiCreateEnvironment,
apiDuplicateEnvironment,
@@ -30,43 +33,18 @@ import {
projects,
} from "@/server/db/schema";
const filterEnvironmentServices = (
environment: any,
accessedServices: string[],
) => ({
...environment,
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),
),
mongo: environment.mongo.filter((db: any) =>
accessedServices.includes(db.mongoId),
),
mysql: environment.mysql.filter((db: any) =>
accessedServices.includes(db.mysqlId),
),
postgres: environment.postgres.filter((db: any) =>
accessedServices.includes(db.postgresId),
),
redis: environment.redis.filter((db: any) =>
accessedServices.includes(db.redisId),
),
});
export const environmentRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateEnvironment)
.mutation(async ({ input, ctx }) => {
try {
await checkEnvironmentCreationPermission(ctx, input.projectId);
if (IS_CLOUD) {
await assertEnvironmentLimit(
ctx.session.activeOrganizationId,
input.projectId,
);
}
if (input.name === "production") {
throw new TRPCError({

View File

@@ -5,6 +5,10 @@ import { and, desc, eq, exists } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
assertMemberLimit,
assertOrganizationLimit,
} from "@/server/api/utils/plan-limits";
import {
invitation,
member,
@@ -28,6 +32,11 @@ export const organizationRouter = createTRPCRouter({
message: "Only the organization owner can create an organization",
});
}
if (IS_CLOUD) {
await assertOrganizationLimit(ctx.user.id);
}
const result = await db
.insert(organization)
.values({
@@ -258,6 +267,10 @@ export const organizationRouter = createTRPCRouter({
const orgId = ctx.session.activeOrganizationId;
const email = input.email.toLowerCase();
if (IS_CLOUD) {
await assertMemberLimit(orgId);
}
// Check if user is already a member
const existingUser = await db.query.user.findFirst({
where: eq(user.email, email),

View File

@@ -23,6 +23,7 @@ import { TRPCError } from "@trpc/server";
import { asc, desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { assertScheduledJobLimit } from "@/server/api/utils/plan-limits";
import { removeJob, schedule } from "@/server/utils/backup";
import { createTRPCRouter, protectedProcedure } from "../trpc";
@@ -35,6 +36,13 @@ export const scheduleRouter = createTRPCRouter({
await checkServicePermissionAndAccess(ctx, serviceId, {
schedule: ["create"],
});
if (IS_CLOUD) {
await assertScheduledJobLimit(
ctx.session.activeOrganizationId,
input.applicationId ? "application" : "compose",
serviceId,
);
}
} else {
if (input.scheduleType === "dokploy-server" && IS_CLOUD) {
throw new TRPCError({
@@ -73,6 +81,14 @@ export const scheduleRouter = createTRPCRouter({
message: "You don't have access to this server.",
});
}
if (IS_CLOUD) {
await assertScheduledJobLimit(
ctx.session.activeOrganizationId,
"server",
input.serverId,
);
}
}
}
const newSchedule = await createSchedule({

View File

@@ -7,6 +7,7 @@ import {
import { TRPCError } from "@trpc/server";
import Stripe from "stripe";
import { z } from "zod";
import { getCurrentPlan as getCurrentPlanForOrganization } from "@/server/utils/billing";
import {
type BillingTier,
getStripeItems,
@@ -31,44 +32,7 @@ import {
export const stripeRouter = createTRPCRouter({
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
if (!IS_CLOUD) return null;
const owner = await findUserById(ctx.user.ownerId);
if (!owner?.stripeCustomerId) return null;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
const subscriptions = await stripe.subscriptions.list({
customer: owner.stripeCustomerId,
status: "active",
expand: ["data.items.data.price"],
});
const activeSub = subscriptions.data[0];
if (!activeSub) return null;
const priceIds = activeSub.items.data.map(
(item) => (item.price as Stripe.Price).id,
);
if (
priceIds.some(
(id) =>
id === STARTUP_BASE_PRICE_MONTHLY_ID ||
id === STARTUP_BASE_PRICE_ANNUAL_ID,
)
) {
return "startup" as const;
}
if (
priceIds.some(
(id) => id === HOBBY_PRICE_MONTHLY_ID || id === HOBBY_PRICE_ANNUAL_ID,
)
) {
return "hobby" as const;
}
if (priceIds.some((id) => LEGACY_PRICE_IDS.includes(id))) {
return "legacy" as const;
}
return null;
return getCurrentPlanForOrganization(ctx.session.activeOrganizationId);
}),
getProducts: adminProcedure.query(async ({ ctx }) => {

View File

@@ -27,6 +27,7 @@ import { observable } from "@trpc/server/observable";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { assertVolumeBackupLimit } from "@/server/api/utils/plan-limits";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { createTRPCRouter, protectedProcedure, withPermission } from "../trpc";
@@ -69,20 +70,33 @@ export const volumeBackupsRouter = createTRPCRouter({
create: protectedProcedure
.input(createVolumeBackupSchema)
.mutation(async ({ input, ctx }) => {
const serviceId =
input.applicationId ||
input.postgresId ||
input.mysqlId ||
input.mariadbId ||
input.mongoId ||
input.redisId ||
input.libsqlId ||
input.composeId;
const serviceType = (
[
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"libsql",
"compose",
] as const
).find((type) => input[`${type}Id`]);
const serviceId = serviceType ? input[`${serviceType}Id`] : undefined;
if (serviceId) {
await checkServicePermissionAndAccess(ctx, serviceId, {
volumeBackup: ["create"],
});
}
if (IS_CLOUD && serviceType && serviceId) {
const existingVolumeBackups = await db.query.volumeBackups.findMany({
where: eq(volumeBackups[`${serviceType}Id`], serviceId),
});
await assertVolumeBackupLimit(
ctx.session.activeOrganizationId,
existingVolumeBackups.length,
);
}
const newVolumeBackup = await createVolumeBackup(input);
if (newVolumeBackup?.enabled) {