Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
ALTER TABLE "tenant_export_jobs"
|
||||||
|
ADD COLUMN IF NOT EXISTS "operation" text DEFAULT 'export' NOT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS "previous_tenant_locked" "locked_tenant";
|
||||||
|
|
||||||
|
ALTER TABLE "tenants"
|
||||||
|
ADD COLUMN IF NOT EXISTS "locked_by_export_job_id" uuid;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.table_constraints
|
||||||
|
WHERE constraint_schema = 'public'
|
||||||
|
AND constraint_name = 'tenants_locked_by_export_job_id_tenant_export_jobs_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "tenants"
|
||||||
|
ADD CONSTRAINT "tenants_locked_by_export_job_id_tenant_export_jobs_id_fk"
|
||||||
|
FOREIGN KEY ("locked_by_export_job_id")
|
||||||
|
REFERENCES "tenant_export_jobs"("id")
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -365,6 +365,13 @@
|
|||||||
"when": 1783247083934,
|
"when": 1783247083934,
|
||||||
"tag": "0054_tenant_export_jobs",
|
"tag": "0054_tenant_export_jobs",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 52,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1784021669805,
|
||||||
|
"tag": "0055_tenant_export_maintenance_lock",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
|
|
||||||
import { tenants } from "./tenants"
|
import { tenants } from "./tenants"
|
||||||
import { authUsers } from "./auth_users"
|
import { authUsers } from "./auth_users"
|
||||||
|
import { lockedTenantEnum } from "./enums"
|
||||||
|
|
||||||
export const tenantExportJobs = pgTable("tenant_export_jobs", {
|
export const tenantExportJobs = pgTable("tenant_export_jobs", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
@@ -26,6 +27,8 @@ export const tenantExportJobs = pgTable("tenant_export_jobs", {
|
|||||||
|
|
||||||
createdBy: uuid("created_by").references(() => authUsers.id),
|
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||||
|
|
||||||
|
operation: text("operation").notNull().default("export"),
|
||||||
|
previousTenantLocked: lockedTenantEnum("previous_tenant_locked"),
|
||||||
status: text("status").notNull().default("queued"),
|
status: text("status").notNull().default("queued"),
|
||||||
filename: text("filename").notNull(),
|
filename: text("filename").notNull(),
|
||||||
storagePath: text("storage_path"),
|
storagePath: text("storage_path"),
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export const tenants = pgTable(
|
|||||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
|
||||||
locked: lockedTenantEnum("locked"),
|
locked: lockedTenantEnum("locked"),
|
||||||
|
lockedByExportJobId: uuid("locked_by_export_job_id"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
authRolePermissions,
|
authRolePermissions,
|
||||||
authUsers,
|
authUsers,
|
||||||
m2mApiKeys,
|
m2mApiKeys,
|
||||||
|
tenants,
|
||||||
} from "../../db/schema"
|
} from "../../db/schema"
|
||||||
|
|
||||||
import { eq, and, inArray } from "drizzle-orm"
|
import { eq, and, inArray } from "drizzle-orm"
|
||||||
@@ -23,6 +24,19 @@ export default fp(async (server: FastifyInstance) => {
|
|||||||
url === "/api/mcp" ||
|
url === "/api/mcp" ||
|
||||||
url.startsWith("/api/mcp/")
|
url.startsWith("/api/mcp/")
|
||||||
|
|
||||||
|
const isWriteMethod = (method: string) =>
|
||||||
|
["POST", "PUT", "PATCH", "DELETE"].includes(method)
|
||||||
|
|
||||||
|
const isTenantLockAllowedRoute = (urlPath: string) =>
|
||||||
|
urlPath === "/api/auth/me" ||
|
||||||
|
urlPath === "/auth/me" ||
|
||||||
|
urlPath === "/api/tenant/switch" ||
|
||||||
|
urlPath === "/tenant/switch" ||
|
||||||
|
urlPath.startsWith("/api/admin/") ||
|
||||||
|
urlPath.startsWith("/admin/") ||
|
||||||
|
urlPath.startsWith("/api/auth/") ||
|
||||||
|
urlPath.startsWith("/auth/")
|
||||||
|
|
||||||
const authenticateMcpApiKey = async (apiKey: string) => {
|
const authenticateMcpApiKey = async (apiKey: string) => {
|
||||||
if (!apiKey.startsWith("fedeo_mcp_")) return false
|
if (!apiKey.startsWith("fedeo_mcp_")) return false
|
||||||
|
|
||||||
@@ -150,6 +164,25 @@ export default fp(async (server: FastifyInstance) => {
|
|||||||
const tenantId = req.user.tenant_id
|
const tenantId = req.user.tenant_id
|
||||||
const userId = req.user.user_id
|
const userId = req.user.user_id
|
||||||
|
|
||||||
|
if (isWriteMethod(req.method) && !isTenantLockAllowedRoute(urlPath)) {
|
||||||
|
const [tenant] = await server.db
|
||||||
|
.select({
|
||||||
|
locked: tenants.locked,
|
||||||
|
lockedByExportJobId: tenants.lockedByExportJobId,
|
||||||
|
})
|
||||||
|
.from(tenants)
|
||||||
|
.where(eq(tenants.id, tenantId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (tenant?.locked === "maintenance_tenant") {
|
||||||
|
return reply.code(423).send({
|
||||||
|
error: "Tenant is locked for maintenance",
|
||||||
|
locked: tenant.locked,
|
||||||
|
lockedByExportJobId: tenant.lockedByExportJobId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// 3️⃣ Rollen des Nutzers im Tenant holen
|
// 3️⃣ Rollen des Nutzers im Tenant holen
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|||||||
@@ -267,8 +267,83 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
return `fedeo-tenant-${safeTenantName || tenantId}-${new Date().toISOString().slice(0, 10)}.fedeo-export.zip`;
|
return `fedeo-tenant-${safeTenantName || tenantId}-${new Date().toISOString().slice(0, 10)}.fedeo-export.zip`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lockTenantForJob = async (tenantId: number, jobId: string) => {
|
||||||
|
const [tenant] = await server.db
|
||||||
|
.select({
|
||||||
|
locked: tenants.locked,
|
||||||
|
lockedByExportJobId: tenants.lockedByExportJobId,
|
||||||
|
})
|
||||||
|
.from(tenants)
|
||||||
|
.where(eq(tenants.id, tenantId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!tenant) throw new Error("Tenant not found");
|
||||||
|
if (tenant.lockedByExportJobId && tenant.lockedByExportJobId !== jobId) {
|
||||||
|
throw new Error("Tenant ist bereits durch einen anderen Export oder Import gesperrt");
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.db
|
||||||
|
.update(tenantExportJobs)
|
||||||
|
.set({
|
||||||
|
previousTenantLocked: tenant.locked,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(tenantExportJobs.id, jobId));
|
||||||
|
|
||||||
|
await server.db
|
||||||
|
.update(tenants)
|
||||||
|
.set({
|
||||||
|
locked: "maintenance_tenant",
|
||||||
|
lockedByExportJobId: jobId,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(tenants.id, tenantId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const unlockTenantForJob = async (tenantId: number, jobId: string) => {
|
||||||
|
const [job] = await server.db
|
||||||
|
.select({
|
||||||
|
previousTenantLocked: tenantExportJobs.previousTenantLocked,
|
||||||
|
})
|
||||||
|
.from(tenantExportJobs)
|
||||||
|
.where(eq(tenantExportJobs.id, jobId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
await server.db
|
||||||
|
.update(tenants)
|
||||||
|
.set({
|
||||||
|
locked: job?.previousTenantLocked || null,
|
||||||
|
lockedByExportJobId: null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(and(
|
||||||
|
eq(tenants.id, tenantId),
|
||||||
|
eq(tenants.lockedByExportJobId, jobId)
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTenantImportJob = async (tenantId: number, currentUserId: string, filename: string) => {
|
||||||
|
const [job] = await server.db
|
||||||
|
.insert(tenantExportJobs)
|
||||||
|
.values({
|
||||||
|
tenantId,
|
||||||
|
createdBy: currentUserId,
|
||||||
|
operation: "import",
|
||||||
|
status: "running",
|
||||||
|
filename,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
await lockTenantForJob(tenantId, job.id);
|
||||||
|
|
||||||
|
return job;
|
||||||
|
};
|
||||||
|
|
||||||
const startTenantExportJob = async (jobId: string, tenantId: number, filename: string) => {
|
const startTenantExportJob = async (jobId: string, tenantId: number, filename: string) => {
|
||||||
try {
|
try {
|
||||||
|
await lockTenantForJob(tenantId, jobId);
|
||||||
|
|
||||||
await server.db
|
await server.db
|
||||||
.update(tenantExportJobs)
|
.update(tenantExportJobs)
|
||||||
.set({
|
.set({
|
||||||
@@ -317,6 +392,8 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(tenantExportJobs.id, jobId));
|
.where(eq(tenantExportJobs.id, jobId));
|
||||||
|
} finally {
|
||||||
|
await unlockTenantForJob(tenantId, jobId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -339,6 +416,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
.values({
|
.values({
|
||||||
tenantId,
|
tenantId,
|
||||||
createdBy: currentUserId,
|
createdBy: currentUserId,
|
||||||
|
operation: "export",
|
||||||
status: "queued",
|
status: "queued",
|
||||||
filename,
|
filename,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -417,6 +495,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
short: tenants.short,
|
short: tenants.short,
|
||||||
createdAt: tenants.createdAt,
|
createdAt: tenants.createdAt,
|
||||||
locked: tenants.locked,
|
locked: tenants.locked,
|
||||||
|
lockedByExportJobId: tenants.lockedByExportJobId,
|
||||||
})
|
})
|
||||||
.from(tenants),
|
.from(tenants),
|
||||||
server.db
|
server.db
|
||||||
@@ -931,6 +1010,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
const body = req.body as {
|
const body = req.body as {
|
||||||
name?: string;
|
name?: string;
|
||||||
short?: string;
|
short?: string;
|
||||||
|
locked?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const name = body.name?.trim();
|
const name = body.name?.trim();
|
||||||
@@ -954,6 +1034,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
short: tenants.short,
|
short: tenants.short,
|
||||||
createdAt: tenants.createdAt,
|
createdAt: tenants.createdAt,
|
||||||
locked: tenants.locked,
|
locked: tenants.locked,
|
||||||
|
lockedByExportJobId: tenants.lockedByExportJobId,
|
||||||
});
|
});
|
||||||
|
|
||||||
await createTenantSeeds(createdTenant.id, currentUser.id);
|
await createTenantSeeds(createdTenant.id, currentUser.id);
|
||||||
@@ -1027,6 +1108,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
const body = req.body as {
|
const body = req.body as {
|
||||||
name?: string;
|
name?: string;
|
||||||
short?: string;
|
short?: string;
|
||||||
|
locked?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateData: Record<string, any> = {
|
const updateData: Record<string, any> = {
|
||||||
@@ -1036,6 +1118,16 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
|
|
||||||
if (typeof body.name === "string") updateData.name = body.name.trim();
|
if (typeof body.name === "string") updateData.name = body.name.trim();
|
||||||
if (typeof body.short === "string") updateData.short = body.short.trim();
|
if (typeof body.short === "string") updateData.short = body.short.trim();
|
||||||
|
if (Object.prototype.hasOwnProperty.call(body, "locked")) {
|
||||||
|
const allowedLockedValues = new Set([null, "maintenance_tenant", "maintenance", "general", "no_subscription"]);
|
||||||
|
const lockedValue = body.locked || null;
|
||||||
|
if (!allowedLockedValues.has(lockedValue)) {
|
||||||
|
return reply.code(400).send({ error: "Invalid locked value" });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData.locked = lockedValue;
|
||||||
|
updateData.lockedByExportJobId = null;
|
||||||
|
}
|
||||||
|
|
||||||
const [updatedTenant] = await server.db
|
const [updatedTenant] = await server.db
|
||||||
.update(tenants)
|
.update(tenants)
|
||||||
@@ -1047,6 +1139,7 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
short: tenants.short,
|
short: tenants.short,
|
||||||
createdAt: tenants.createdAt,
|
createdAt: tenants.createdAt,
|
||||||
locked: tenants.locked,
|
locked: tenants.locked,
|
||||||
|
lockedByExportJobId: tenants.lockedByExportJobId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!updatedTenant) {
|
if (!updatedTenant) {
|
||||||
@@ -1200,6 +1293,9 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
// POST /admin/tenant-imports
|
// POST /admin/tenant-imports
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
server.post("/admin/tenant-imports", { bodyLimit: 1024 * 1024 * 1024 }, async (req, reply) => {
|
server.post("/admin/tenant-imports", { bodyLimit: 1024 * 1024 * 1024 }, async (req, reply) => {
|
||||||
|
let importJob: any = null;
|
||||||
|
let targetTenantId: number | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const currentUser = await requireAdmin(req, reply);
|
const currentUser = await requireAdmin(req, reply);
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
@@ -1212,19 +1308,71 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
if (!data?.file) return reply.code(400).send({ error: "export file required" });
|
if (!data?.file) return reply.code(400).send({ error: "export file required" });
|
||||||
|
|
||||||
const archiveBuffer = await data.toBuffer();
|
const archiveBuffer = await data.toBuffer();
|
||||||
const targetTenantId = Number(data.fields?.targetTenantId?.value || 0) || null;
|
targetTenantId = Number(data.fields?.targetTenantId?.value || 0) || null;
|
||||||
result = await importTenantFullExportArchive(server, archiveBuffer, { targetTenantId });
|
|
||||||
|
if (targetTenantId) {
|
||||||
|
importJob = await createTenantImportJob(targetTenantId, currentUser.id, data.filename || "tenant-import.zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await importTenantFullExportArchive(server, archiveBuffer, { targetTenantId });
|
||||||
|
} catch (err: any) {
|
||||||
|
if (importJob) {
|
||||||
|
await server.db
|
||||||
|
.update(tenantExportJobs)
|
||||||
|
.set({
|
||||||
|
status: "failed",
|
||||||
|
error: err?.message || String(err),
|
||||||
|
completedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(tenantExportJobs.id, importJob.id));
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const body = req.body as TenantFullExport | { exportData?: TenantFullExport; targetTenantId?: number };
|
const body = req.body as TenantFullExport | { exportData?: TenantFullExport; targetTenantId?: number };
|
||||||
const exportData = "format" in body ? body : body.exportData;
|
const exportData = "format" in body ? body : body.exportData;
|
||||||
const targetTenantId = "format" in body ? null : Number(body.targetTenantId || 0) || null;
|
targetTenantId = "format" in body ? null : Number(body.targetTenantId || 0) || null;
|
||||||
|
|
||||||
if (!exportData) {
|
if (!exportData) {
|
||||||
return reply.code(400).send({ error: "exportData required" });
|
return reply.code(400).send({ error: "exportData required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await importTenantFullExport(server, exportData, { targetTenantId });
|
if (targetTenantId) {
|
||||||
|
importJob = await createTenantImportJob(targetTenantId, currentUser.id, "tenant-import.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await importTenantFullExport(server, exportData, { targetTenantId });
|
||||||
|
} catch (err: any) {
|
||||||
|
if (importJob) {
|
||||||
|
await server.db
|
||||||
|
.update(tenantExportJobs)
|
||||||
|
.set({
|
||||||
|
status: "failed",
|
||||||
|
error: err?.message || String(err),
|
||||||
|
completedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(tenantExportJobs.id, importJob.id));
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (importJob) {
|
||||||
|
await server.db
|
||||||
|
.update(tenantExportJobs)
|
||||||
|
.set({
|
||||||
|
status: "ready",
|
||||||
|
completedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
.where(eq(tenantExportJobs.id, importJob.id));
|
||||||
|
}
|
||||||
|
|
||||||
const fallbackName = deriveNameFromEmail(currentUser.email);
|
const fallbackName = deriveNameFromEmail(currentUser.email);
|
||||||
|
|
||||||
await server.db
|
await server.db
|
||||||
@@ -1284,6 +1432,14 @@ export default async function adminRoutes(server: FastifyInstance) {
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("ERROR /admin/tenant-imports:", err);
|
console.error("ERROR /admin/tenant-imports:", err);
|
||||||
return reply.code(500).send({ error: err?.message || "Internal Server Error" });
|
return reply.code(500).send({ error: err?.message || "Internal Server Error" });
|
||||||
|
} finally {
|
||||||
|
if (importJob && targetTenantId) {
|
||||||
|
try {
|
||||||
|
await unlockTenantForJob(targetTenantId, importJob.id);
|
||||||
|
} catch (unlockErr) {
|
||||||
|
req.log.error({ err: unlockErr, importJobId: importJob.id, targetTenantId }, "Tenant konnte nach Import nicht entsperrt werden");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type AdminTenant = {
|
|||||||
short: string
|
short: string
|
||||||
user_count: number
|
user_count: number
|
||||||
locked?: string | null
|
locked?: string | null
|
||||||
|
lockedByExportJobId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminUserProfile = {
|
export type AdminUserProfile = {
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ const lastImportResult = ref<null | {
|
|||||||
restoredFiles: number
|
restoredFiles: number
|
||||||
skippedFiles: number
|
skippedFiles: number
|
||||||
}>(null)
|
}>(null)
|
||||||
|
const lockedOptions = [
|
||||||
|
{ label: "Aktiv", value: null },
|
||||||
|
{ label: "Tenant-Wartung", value: "maintenance_tenant" },
|
||||||
|
{ label: "Globale Wartung", value: "maintenance" },
|
||||||
|
{ label: "Allgemeine Sperre", value: "general" },
|
||||||
|
{ label: "Kein Abonnement", value: "no_subscription" },
|
||||||
|
]
|
||||||
|
|
||||||
const tenantForm = ref<AdminTenant | null>(null)
|
const tenantForm = ref<AdminTenant | null>(null)
|
||||||
const assignedUsers = ref<AdminUser[]>([])
|
const assignedUsers = ref<AdminUser[]>([])
|
||||||
@@ -77,6 +84,7 @@ const saveTenant = async () => {
|
|||||||
await admin.updateTenant(tenantForm.value.id, {
|
await admin.updateTenant(tenantForm.value.id, {
|
||||||
name: tenantForm.value.name,
|
name: tenantForm.value.name,
|
||||||
short: tenantForm.value.short,
|
short: tenantForm.value.short,
|
||||||
|
locked: tenantForm.value.locked || null,
|
||||||
})
|
})
|
||||||
|
|
||||||
await fetchTenant()
|
await fetchTenant()
|
||||||
@@ -316,7 +324,25 @@ onMounted(async () => {
|
|||||||
<UFormField label="Kürzel">
|
<UFormField label="Kürzel">
|
||||||
<UInput v-model="tenantForm.short" />
|
<UInput v-model="tenantForm.short" />
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField label="Locked Mode">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="tenantForm.locked"
|
||||||
|
:items="lockedOptions"
|
||||||
|
value-key="value"
|
||||||
|
label-key="label"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
</UForm>
|
</UForm>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="tenantForm.lockedByExportJobId"
|
||||||
|
class="mt-4"
|
||||||
|
title="Automatische Sperre aktiv"
|
||||||
|
:description="`Dieser Tenant wird gerade durch Export/Import ${tenantForm.lockedByExportJobId} pausiert.`"
|
||||||
|
color="warning"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
</UCard>
|
</UCard>
|
||||||
|
|
||||||
<UCard v-if="!loading && tenantForm" class="mt-3">
|
<UCard v-if="!loading && tenantForm" class="mt-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user