mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-04 17:56:38 +00:00
Restrict features
This commit is contained in:
@@ -1925,5 +1925,22 @@
|
|||||||
"sidebarLogsRequest": "Request Logs",
|
"sidebarLogsRequest": "Request Logs",
|
||||||
"sidebarLogsAccess": "Access Logs",
|
"sidebarLogsAccess": "Access Logs",
|
||||||
"sidebarLogsAction": "Action Logs",
|
"sidebarLogsAction": "Action Logs",
|
||||||
"requestLogsDescription": "View detailed pre-request logs for resources in this organization"
|
"logRetention": "Log Retention",
|
||||||
|
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||||
|
"requestLogsDescription": "View detailed request logs for resources in this organization",
|
||||||
|
"logRetentionRequestLabel": "Request Log Retention",
|
||||||
|
"logRetentionRequestDescription": "How long to retain request logs",
|
||||||
|
"logRetentionAccessLabel": "Access Log Retention",
|
||||||
|
"logRetentionAccessDescription": "How long to retain access logs",
|
||||||
|
"logRetentionActionLabel": "Action Log Retention",
|
||||||
|
"logRetentionActionDescription": "How long to retain action logs",
|
||||||
|
"logRetentionDisabled": "Disabled",
|
||||||
|
"logRetention3Days": "3 days",
|
||||||
|
"logRetention7Days": "7 days",
|
||||||
|
"logRetention14Days": "14 days",
|
||||||
|
"logRetention30Days": "30 days",
|
||||||
|
"logRetentionForever": "Forever",
|
||||||
|
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||||
|
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||||
|
"licenseRequiredToUse": "An Enterprise license is required to use this feature."
|
||||||
}
|
}
|
||||||
@@ -29,13 +29,13 @@ export const orgs = pgTable("orgs", {
|
|||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(15),
|
.default(7),
|
||||||
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(15),
|
.default(0),
|
||||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(15),
|
.default(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
createdAt: text("createdAt"),
|
createdAt: text("createdAt"),
|
||||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(15),
|
.default(7),
|
||||||
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(15),
|
.default(0),
|
||||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(15)
|
.default(0)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { runSetupFunctions } from "./setup";
|
|||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
import { createNextServer } from "./nextServer";
|
import { createNextServer } from "./nextServer";
|
||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
|
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
import {
|
import {
|
||||||
ApiKey,
|
ApiKey,
|
||||||
ApiKeyOrg,
|
ApiKeyOrg,
|
||||||
@@ -13,13 +14,13 @@ import {
|
|||||||
User,
|
User,
|
||||||
UserOrg
|
UserOrg
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { setHostMeta } from "@server/lib/hostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
import { initTelemetryClient } from "@server/lib/telemetry";
|
||||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||||
import { initCleanup } from "#dynamic/cleanup";
|
import { initCleanup } from "#dynamic/cleanup";
|
||||||
import license from "#dynamic/license/license";
|
import license from "#dynamic/license/license";
|
||||||
|
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
@@ -33,6 +34,8 @@ async function startServers() {
|
|||||||
|
|
||||||
initTelemetryClient();
|
initTelemetryClient();
|
||||||
|
|
||||||
|
initLogCleanupInterval();
|
||||||
|
|
||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
const internalServer = createInternalServer();
|
const internalServer = createInternalServer();
|
||||||
|
|||||||
62
server/lib/cleanupLogs.ts
Normal file
62
server/lib/cleanupLogs.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { db, orgs } from "@server/db";
|
||||||
|
import { cleanUpOldLogs as cleanUpOldAccessLogs } from "@server/private/lib/logAccessAudit";
|
||||||
|
import { cleanUpOldLogs as cleanUpOldActionLogs } from "@server/private/middlewares/logActionAudit";
|
||||||
|
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
|
||||||
|
import { gt, or } from "drizzle-orm";
|
||||||
|
|
||||||
|
export function initLogCleanupInterval() {
|
||||||
|
return setInterval(
|
||||||
|
async () => {
|
||||||
|
const orgsToClean = await db
|
||||||
|
.select({
|
||||||
|
orgId: orgs.orgId,
|
||||||
|
settingsLogRetentionDaysAction:
|
||||||
|
orgs.settingsLogRetentionDaysAction,
|
||||||
|
settingsLogRetentionDaysAccess:
|
||||||
|
orgs.settingsLogRetentionDaysAccess,
|
||||||
|
settingsLogRetentionDaysRequest:
|
||||||
|
orgs.settingsLogRetentionDaysRequest
|
||||||
|
})
|
||||||
|
.from(orgs)
|
||||||
|
.where(
|
||||||
|
or(
|
||||||
|
gt(orgs.settingsLogRetentionDaysAction, 0),
|
||||||
|
gt(orgs.settingsLogRetentionDaysAccess, 0),
|
||||||
|
gt(orgs.settingsLogRetentionDaysRequest, 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const org of orgsToClean) {
|
||||||
|
const {
|
||||||
|
orgId,
|
||||||
|
settingsLogRetentionDaysAction,
|
||||||
|
settingsLogRetentionDaysAccess,
|
||||||
|
settingsLogRetentionDaysRequest
|
||||||
|
} = org;
|
||||||
|
|
||||||
|
if (settingsLogRetentionDaysAction > 0) {
|
||||||
|
await cleanUpOldActionLogs(
|
||||||
|
orgId,
|
||||||
|
settingsLogRetentionDaysRequest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsLogRetentionDaysAccess > 0) {
|
||||||
|
await cleanUpOldAccessLogs(
|
||||||
|
orgId,
|
||||||
|
settingsLogRetentionDaysRequest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsLogRetentionDaysRequest > 0) {
|
||||||
|
await cleanUpOldRequestLogs(
|
||||||
|
orgId,
|
||||||
|
settingsLogRetentionDaysRequest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 3 * 60 * 60 * 1000
|
||||||
|
60 * 1000 // for testing
|
||||||
|
); // every 3 hours
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { accessAuditLog, db, orgs } from "@server/db";
|
import { accessAuditLog, db, orgs } from "@server/db";
|
||||||
import { getCountryCodeForIp } from "@server/lib/geoip";
|
import { getCountryCodeForIp } from "@server/lib/geoip";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq, lt } from "drizzle-orm";
|
||||||
|
import cache from "@server/lib/cache";
|
||||||
|
|
||||||
async function getAccessDays(orgId: string): Promise<number> {
|
async function getAccessDays(orgId: string): Promise<number> {
|
||||||
// check cache first
|
// check cache first
|
||||||
@@ -23,11 +24,38 @@ async function getAccessDays(orgId: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// store the result in cache
|
// store the result in cache
|
||||||
cache.set(`org_${orgId}_accessDays`, org.settingsLogRetentionDaysAction);
|
cache.set(
|
||||||
|
`org_${orgId}_accessDays`,
|
||||||
|
org.settingsLogRetentionDaysAction,
|
||||||
|
300
|
||||||
|
);
|
||||||
|
|
||||||
return org.settingsLogRetentionDaysAction;
|
return org.settingsLogRetentionDaysAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleteResult = await db
|
||||||
|
.delete(accessAuditLog)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
lt(accessAuditLog.timestamp, cutoffTimestamp),
|
||||||
|
eq(accessAuditLog.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Cleaned up ${deleteResult.changes} access audit logs older than ${retentionDays} days`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error cleaning up old action audit logs:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function logAccessAudit(data: {
|
export async function logAccessAudit(data: {
|
||||||
action: boolean;
|
action: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -40,6 +68,12 @@ export async function logAccessAudit(data: {
|
|||||||
requestIp?: string;
|
requestIp?: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
|
const retentionDays = await getAccessDays(data.orgId);
|
||||||
|
if (retentionDays === 0) {
|
||||||
|
// do not log
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let actorType: string | undefined;
|
let actorType: string | undefined;
|
||||||
let actor: string | undefined;
|
let actor: string | undefined;
|
||||||
let actorId: string | undefined;
|
let actorId: string | undefined;
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ import logger from "@server/logger";
|
|||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import NodeCache from "node-cache";
|
import { and, eq, lt } from "drizzle-orm";
|
||||||
import { eq } from "drizzle-orm";
|
import cache from "@server/lib/cache";
|
||||||
|
|
||||||
const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes
|
|
||||||
async function getActionDays(orgId: string): Promise<number> {
|
async function getActionDays(orgId: string): Promise<number> {
|
||||||
// check cache first
|
// check cache first
|
||||||
const cached = cache.get<number>(`org_${orgId}_actionDays`);
|
const cached = cache.get<number>(`org_${orgId}_actionDays`);
|
||||||
@@ -41,11 +40,34 @@ async function getActionDays(orgId: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// store the result in cache
|
// store the result in cache
|
||||||
cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction);
|
cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction, 300);
|
||||||
|
|
||||||
return org.settingsLogRetentionDaysAction;
|
return org.settingsLogRetentionDaysAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleteResult = await db
|
||||||
|
.delete(actionAuditLog)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
lt(actionAuditLog.timestamp, cutoffTimestamp),
|
||||||
|
eq(actionAuditLog.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Cleaned up ${deleteResult.changes} action audit logs older than ${retentionDays} days`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error cleaning up old action audit logs:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function logActionAudit(action: ActionsEnum) {
|
export function logActionAudit(action: ActionsEnum) {
|
||||||
return async function (
|
return async function (
|
||||||
req: Request,
|
req: Request,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { db, orgs, requestAuditLog } from "@server/db";
|
import { db, orgs, requestAuditLog } from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { eq } from "drizzle-orm";
|
import { and, eq, lt } from "drizzle-orm";
|
||||||
import NodeCache from "node-cache";
|
import cache from "@server/lib/cache";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
||||||
@@ -24,7 +24,6 @@ Reasons:
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes
|
|
||||||
async function getRetentionDays(orgId: string): Promise<number> {
|
async function getRetentionDays(orgId: string): Promise<number> {
|
||||||
// check cache first
|
// check cache first
|
||||||
const cached = cache.get<number>(`org_${orgId}_retentionDays`);
|
const cached = cache.get<number>(`org_${orgId}_retentionDays`);
|
||||||
@@ -34,7 +33,8 @@ async function getRetentionDays(orgId: string): Promise<number> {
|
|||||||
|
|
||||||
const [org] = await db
|
const [org] = await db
|
||||||
.select({
|
.select({
|
||||||
settingsLogRetentionDaysRequest: orgs.settingsLogRetentionDaysRequest
|
settingsLogRetentionDaysRequest:
|
||||||
|
orgs.settingsLogRetentionDaysRequest
|
||||||
})
|
})
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
.where(eq(orgs.orgId, orgId))
|
.where(eq(orgs.orgId, orgId))
|
||||||
@@ -45,11 +45,38 @@ async function getRetentionDays(orgId: string): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// store the result in cache
|
// store the result in cache
|
||||||
cache.set(`org_${orgId}_retentionDays`, org.settingsLogRetentionDaysRequest);
|
cache.set(
|
||||||
|
`org_${orgId}_retentionDays`,
|
||||||
|
org.settingsLogRetentionDaysRequest,
|
||||||
|
300
|
||||||
|
);
|
||||||
|
|
||||||
return org.settingsLogRetentionDaysRequest;
|
return org.settingsLogRetentionDaysRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const deleteResult = await db
|
||||||
|
.delete(requestAuditLog)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
lt(requestAuditLog.timestamp, cutoffTimestamp),
|
||||||
|
eq(requestAuditLog.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Cleaned up ${deleteResult.changes} request audit logs older than ${retentionDays} days`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error cleaning up old request audit logs:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function logRequestAudit(
|
export async function logRequestAudit(
|
||||||
data: {
|
data: {
|
||||||
action: boolean;
|
action: boolean;
|
||||||
@@ -76,7 +103,6 @@ export async function logRequestAudit(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (data.orgId) {
|
if (data.orgId) {
|
||||||
const retentionDays = await getRetentionDays(data.orgId);
|
const retentionDays = await getRetentionDays(data.orgId);
|
||||||
if (retentionDays === 0) {
|
if (retentionDays === 0) {
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ export async function getOrg(
|
|||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
const org = await db
|
const [org] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(orgs)
|
.from(orgs)
|
||||||
.where(eq(orgs.orgId, orgId))
|
.where(eq(orgs.orgId, orgId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (org.length === 0) {
|
if (!org) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
@@ -66,7 +66,7 @@ export async function getOrg(
|
|||||||
|
|
||||||
return response<GetOrgResponse>(res, {
|
return response<GetOrgResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
org: org[0]
|
org
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ const updateOrgParamsSchema = z
|
|||||||
|
|
||||||
const updateOrgBodySchema = z
|
const updateOrgBodySchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255).optional()
|
name: z.string().min(1).max(255).optional(),
|
||||||
|
settingsLogRetentionDaysRequest: z.number().min(-1).optional(),
|
||||||
|
settingsLogRetentionDaysAccess: z.number().min(-1).optional(),
|
||||||
|
settingsLogRetentionDaysAction: z.number().min(-1).optional()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
@@ -74,7 +77,7 @@ export async function updateOrg(
|
|||||||
const updatedOrg = await db
|
const updatedOrg = await db
|
||||||
.update(orgs)
|
.update(orgs)
|
||||||
.set({
|
.set({
|
||||||
name: parsedBody.data.name
|
...parsedBody.data
|
||||||
})
|
})
|
||||||
.where(eq(orgs.orgId, orgId))
|
.where(eq(orgs.orgId, orgId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -42,15 +42,36 @@ import {
|
|||||||
import { useUserContext } from "@app/hooks/useUserContext";
|
import { useUserContext } from "@app/hooks/useUserContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from "@app/components/ui/dropdown-menu";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||||
|
|
||||||
// Schema for general organization settings
|
// Schema for general organization settings
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
subnet: z.string().optional()
|
subnet: z.string().optional(),
|
||||||
|
settingsLogRetentionDaysRequest: z.number(),
|
||||||
|
settingsLogRetentionDaysAccess: z.number(),
|
||||||
|
settingsLogRetentionDaysAction: z.number()
|
||||||
});
|
});
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
|
||||||
|
const LOG_RETENTION_OPTIONS = [
|
||||||
|
{ label: "logRetentionDisabled", value: 0 },
|
||||||
|
{ label: "logRetention3Days", value: 3 },
|
||||||
|
{ label: "logRetention7Days", value: 7 },
|
||||||
|
{ label: "logRetention14Days", value: 14 },
|
||||||
|
{ label: "logRetention30Days", value: 30 },
|
||||||
|
{ label: "logRetentionForever", value: -1 }
|
||||||
|
];
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const { orgUser } = userOrgUserContext();
|
const { orgUser } = userOrgUserContext();
|
||||||
@@ -60,6 +81,8 @@ export default function GeneralPage() {
|
|||||||
const { user } = useUserContext();
|
const { user } = useUserContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
const subscription = useSubscriptionStatusContext();
|
||||||
|
|
||||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
const [loadingDelete, setLoadingDelete] = useState(false);
|
||||||
const [loadingSave, setLoadingSave] = useState(false);
|
const [loadingSave, setLoadingSave] = useState(false);
|
||||||
@@ -69,7 +92,13 @@ export default function GeneralPage() {
|
|||||||
resolver: zodResolver(GeneralFormSchema),
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: org?.org.name,
|
name: org?.org.name,
|
||||||
subnet: org?.org.subnet || "" // Add default value for subnet
|
subnet: org?.org.subnet || "", // Add default value for subnet
|
||||||
|
settingsLogRetentionDaysRequest:
|
||||||
|
org.org.settingsLogRetentionDaysRequest ?? 15,
|
||||||
|
settingsLogRetentionDaysAccess:
|
||||||
|
org.org.settingsLogRetentionDaysAccess ?? 15,
|
||||||
|
settingsLogRetentionDaysAction:
|
||||||
|
org.org.settingsLogRetentionDaysAction ?? 15
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
@@ -131,8 +160,14 @@ export default function GeneralPage() {
|
|||||||
try {
|
try {
|
||||||
// Update organization
|
// Update organization
|
||||||
await api.post(`/org/${org?.org.orgId}`, {
|
await api.post(`/org/${org?.org.orgId}`, {
|
||||||
name: data.name
|
name: data.name,
|
||||||
// subnet: data.subnet // Include subnet in the API request
|
// subnet: data.subnet // Include subnet in the API request
|
||||||
|
settingsLogRetentionDaysRequest:
|
||||||
|
data.settingsLogRetentionDaysRequest,
|
||||||
|
settingsLogRetentionDaysAccess:
|
||||||
|
data.settingsLogRetentionDaysAccess,
|
||||||
|
settingsLogRetentionDaysAction:
|
||||||
|
data.settingsLogRetentionDaysAction
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also save auth page settings if they have unsaved changes
|
// Also save auth page settings if they have unsaved changes
|
||||||
@@ -159,6 +194,11 @@ export default function GeneralPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getLabelForValue = (value: number) => {
|
||||||
|
const option = LOG_RETENTION_OPTIONS.find((opt) => opt.value === value);
|
||||||
|
return option ? t(option.label) : `${value} days`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<ConfirmDeleteDialog
|
<ConfirmDeleteDialog
|
||||||
@@ -168,9 +208,7 @@ export default function GeneralPage() {
|
|||||||
}}
|
}}
|
||||||
dialog={
|
dialog={
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>{t("orgQuestionRemove")}</p>
|
||||||
{t("orgQuestionRemove")}
|
|
||||||
</p>
|
|
||||||
<p>{t("orgMessageRemove")}</p>
|
<p>{t("orgMessageRemove")}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -179,23 +217,24 @@ export default function GeneralPage() {
|
|||||||
string={org?.org.name || ""}
|
string={org?.org.name || ""}
|
||||||
title={t("orgDelete")}
|
title={t("orgDelete")}
|
||||||
/>
|
/>
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
<Form {...form}>
|
||||||
<SettingsSectionTitle>
|
<form
|
||||||
{t("orgGeneralSettings")}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
</SettingsSectionTitle>
|
className="space-y-4"
|
||||||
<SettingsSectionDescription>
|
id="org-settings-form"
|
||||||
{t("orgGeneralSettingsDescription")}
|
>
|
||||||
</SettingsSectionDescription>
|
<SettingsSection>
|
||||||
</SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionTitle>
|
||||||
<SettingsSectionForm>
|
{t("orgGeneralSettings")}
|
||||||
<Form {...form}>
|
</SettingsSectionTitle>
|
||||||
<form
|
<SettingsSectionDescription>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
{t("orgGeneralSettingsDescription")}
|
||||||
className="space-y-4"
|
</SettingsSectionDescription>
|
||||||
id="org-settings-form"
|
</SettingsSectionHeader>
|
||||||
>
|
<SettingsSectionBody>
|
||||||
|
<SettingsSectionForm>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -235,15 +274,228 @@ export default function GeneralPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</form>
|
</SettingsSectionForm>
|
||||||
</Form>
|
</SettingsSectionBody>
|
||||||
</SettingsSectionForm>
|
</SettingsSection>
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
{(build === "saas") && (
|
<SettingsSection>
|
||||||
<AuthPageSettings ref={authPageSettingsRef} />
|
<SettingsSectionHeader>
|
||||||
)}
|
<SettingsSectionTitle>
|
||||||
|
{t("logRetention")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("logRetentionDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
{/* {build === "saas" && !subscription?.subscribed ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("orgAuthPageDisabled")}{" "}
|
||||||
|
{t("subscriptionRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null} */}
|
||||||
|
|
||||||
|
<SettingsSectionForm>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="settingsLogRetentionDaysRequest"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("logRetentionRequestLabel")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between"
|
||||||
|
>
|
||||||
|
{getLabelForValue(
|
||||||
|
field.value
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-full">
|
||||||
|
{LOG_RETENTION_OPTIONS.map(
|
||||||
|
(option) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={
|
||||||
|
option.value
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
field.onChange(
|
||||||
|
option.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
option.label
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"logRetentionRequestDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{build != "oss" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="settingsLogRetentionDaysAccess"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"logRetentionAccessLabel"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between"
|
||||||
|
disabled={
|
||||||
|
(build ==
|
||||||
|
"saas" &&
|
||||||
|
!subscription?.subscribed) ||
|
||||||
|
(build ==
|
||||||
|
"enterprise" &&
|
||||||
|
!isUnlocked())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getLabelForValue(
|
||||||
|
field.value
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-full">
|
||||||
|
{LOG_RETENTION_OPTIONS.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={
|
||||||
|
option.value
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
field.onChange(
|
||||||
|
option.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
option.label
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"logRetentionAccessDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="settingsLogRetentionDaysAction"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"logRetentionActionLabel"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between"
|
||||||
|
disabled={
|
||||||
|
(build ==
|
||||||
|
"saas" &&
|
||||||
|
!subscription?.subscribed) ||
|
||||||
|
(build ==
|
||||||
|
"enterprise" &&
|
||||||
|
!isUnlocked())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getLabelForValue(
|
||||||
|
field.value
|
||||||
|
)}
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-full">
|
||||||
|
{LOG_RETENTION_OPTIONS.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={
|
||||||
|
option.value
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
field.onChange(
|
||||||
|
option.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
option.label
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"logRetentionActionDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
||||||
|
|
||||||
{/* Save Button */}
|
{/* Save Button */}
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
|
|||||||
@@ -6,12 +6,21 @@ import { createApiClient } from "@app/lib/api";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable";
|
import {
|
||||||
|
getStoredPageSize,
|
||||||
|
LogDataTable,
|
||||||
|
setStoredPageSize
|
||||||
|
} from "@app/components/LogDataTable";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||||
import { ArrowUpRight, Key, User } from "lucide-react";
|
import { ArrowUpRight, Key, User } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||||
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
@@ -21,6 +30,8 @@ export default function GeneralPage() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const { orgId } = useParams();
|
const { orgId } = useParams();
|
||||||
|
const subscription = useSubscriptionStatusContext();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
const [rows, setRows] = useState<any[]>([]);
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
@@ -202,6 +213,16 @@ export default function GeneralPage() {
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||||
|
if (
|
||||||
|
(build == "saas" && !subscription?.subscribed) ||
|
||||||
|
(build == "enterprise" && !isUnlocked())
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"Access denied: subscription inactive or license locked"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -583,6 +604,27 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SettingsSectionTitle
|
||||||
|
title={t("accessLogs")}
|
||||||
|
description={t("accessLogsDescription")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{build == "saas" && !subscription?.subscribed ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("subscriptionRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{build == "enterprise" && !isUnlocked() ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("licenseRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<LogDataTable
|
<LogDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
@@ -610,6 +652,10 @@ export default function GeneralPage() {
|
|||||||
// Row expansion props
|
// Row expansion props
|
||||||
expandable={true}
|
expandable={true}
|
||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
|
disabled={
|
||||||
|
(build == "saas" && !subscription?.subscribed) ||
|
||||||
|
(build == "enterprise" && !isUnlocked())
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,11 +6,20 @@ import { createApiClient } from "@app/lib/api";
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable";
|
import {
|
||||||
|
getStoredPageSize,
|
||||||
|
LogDataTable,
|
||||||
|
setStoredPageSize
|
||||||
|
} from "@app/components/LogDataTable";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||||
import { Key, User } from "lucide-react";
|
import { Key, User } from "lucide-react";
|
||||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||||
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
@@ -20,6 +29,8 @@ export default function GeneralPage() {
|
|||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const { orgId } = useParams();
|
const { orgId } = useParams();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const subscription = useSubscriptionStatusContext();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
const [rows, setRows] = useState<any[]>([]);
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
@@ -187,6 +198,15 @@ export default function GeneralPage() {
|
|||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||||
|
if (
|
||||||
|
(build == "saas" && !subscription?.subscribed) ||
|
||||||
|
(build == "enterprise" && !isUnlocked())
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"Access denied: subscription inactive or license locked"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -435,6 +455,27 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<SettingsSectionTitle
|
||||||
|
title={t("actionLogs")}
|
||||||
|
description={t("actionLogsDescription")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{build == "saas" && !subscription?.subscribed ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("subscriptionRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{build == "enterprise" && !isUnlocked() ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("licenseRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<LogDataTable
|
<LogDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
@@ -464,6 +505,10 @@ export default function GeneralPage() {
|
|||||||
// Row expansion props
|
// Row expansion props
|
||||||
expandable={true}
|
expandable={true}
|
||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
|
disabled={
|
||||||
|
(build == "saas" && !subscription?.subscribed) ||
|
||||||
|
(build == "enterprise" && !isUnlocked())
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -123,16 +123,20 @@ export const orgNavSections = (
|
|||||||
href: "/{orgId}/settings/logs/request",
|
href: "/{orgId}/settings/logs/request",
|
||||||
icon: <SquareMousePointer className="h-4 w-4" />
|
icon: <SquareMousePointer className="h-4 w-4" />
|
||||||
},
|
},
|
||||||
{
|
...(build != "oss"
|
||||||
title: "sidebarLogsAccess",
|
? [
|
||||||
href: "/{orgId}/settings/logs/access",
|
{
|
||||||
icon: <ScanEye className="h-4 w-4" />
|
title: "sidebarLogsAccess",
|
||||||
},
|
href: "/{orgId}/settings/logs/access",
|
||||||
{
|
icon: <ScanEye className="h-4 w-4" />
|
||||||
title: "sidebarLogsAction",
|
},
|
||||||
href: "/{orgId}/settings/logs/action",
|
{
|
||||||
icon: <Logs className="h-4 w-4" />
|
title: "sidebarLogsAction",
|
||||||
},
|
href: "/{orgId}/settings/logs/action",
|
||||||
|
icon: <Logs className="h-4 w-4" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface DataTablePaginationProps<TData> {
|
|||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
isServerPagination?: boolean;
|
isServerPagination?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTablePagination<TData>({
|
export function DataTablePagination<TData>({
|
||||||
@@ -31,7 +32,8 @@ export function DataTablePagination<TData>({
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
totalCount,
|
totalCount,
|
||||||
isServerPagination = false,
|
isServerPagination = false,
|
||||||
isLoading = false
|
isLoading = false,
|
||||||
|
disabled = false
|
||||||
}: DataTablePaginationProps<TData>) {
|
}: DataTablePaginationProps<TData>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -96,8 +98,9 @@ export function DataTablePagination<TData>({
|
|||||||
<Select
|
<Select
|
||||||
value={`${table.getState().pagination.pageSize}`}
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
onValueChange={handlePageSizeChange}
|
onValueChange={handlePageSizeChange}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 w-[73px]">
|
<SelectTrigger className="h-8 w-[73px]" disabled={disabled}>
|
||||||
<SelectValue
|
<SelectValue
|
||||||
placeholder={table.getState().pagination.pageSize}
|
placeholder={table.getState().pagination.pageSize}
|
||||||
/>
|
/>
|
||||||
@@ -128,7 +131,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => handlePageNavigation('first')}
|
onClick={() => handlePageNavigation('first')}
|
||||||
disabled={!table.getCanPreviousPage() || isLoading}
|
disabled={!table.getCanPreviousPage() || isLoading || disabled}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t('paginatorToFirst')}</span>
|
<span className="sr-only">{t('paginatorToFirst')}</span>
|
||||||
<DoubleArrowLeftIcon className="h-4 w-4" />
|
<DoubleArrowLeftIcon className="h-4 w-4" />
|
||||||
@@ -137,7 +140,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
onClick={() => handlePageNavigation('previous')}
|
onClick={() => handlePageNavigation('previous')}
|
||||||
disabled={!table.getCanPreviousPage() || isLoading}
|
disabled={!table.getCanPreviousPage() || isLoading || disabled}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t('paginatorToPrevious')}</span>
|
<span className="sr-only">{t('paginatorToPrevious')}</span>
|
||||||
<ChevronLeftIcon className="h-4 w-4" />
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
@@ -146,7 +149,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-8 w-8 p-0"
|
className="h-8 w-8 p-0"
|
||||||
onClick={() => handlePageNavigation('next')}
|
onClick={() => handlePageNavigation('next')}
|
||||||
disabled={!table.getCanNextPage() || isLoading}
|
disabled={!table.getCanNextPage() || isLoading || disabled}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t('paginatorToNext')}</span>
|
<span className="sr-only">{t('paginatorToNext')}</span>
|
||||||
<ChevronRightIcon className="h-4 w-4" />
|
<ChevronRightIcon className="h-4 w-4" />
|
||||||
@@ -155,7 +158,7 @@ export function DataTablePagination<TData>({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden h-8 w-8 p-0 lg:flex"
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
onClick={() => handlePageNavigation('last')}
|
onClick={() => handlePageNavigation('last')}
|
||||||
disabled={!table.getCanNextPage() || isLoading}
|
disabled={!table.getCanNextPage() || isLoading || disabled}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{t('paginatorToLast')}</span>
|
<span className="sr-only">{t('paginatorToLast')}</span>
|
||||||
<DoubleArrowRightIcon className="h-4 w-4" />
|
<DoubleArrowRightIcon className="h-4 w-4" />
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ type DataTableProps<TData, TValue> = {
|
|||||||
};
|
};
|
||||||
tabs?: TabFilter[];
|
tabs?: TabFilter[];
|
||||||
defaultTab?: string;
|
defaultTab?: string;
|
||||||
|
disabled?: boolean;
|
||||||
onDateRangeChange?: (
|
onDateRangeChange?: (
|
||||||
startDate: DateTimeValue,
|
startDate: DateTimeValue,
|
||||||
endDate: DateTimeValue
|
endDate: DateTimeValue
|
||||||
@@ -144,6 +145,7 @@ export function LogDataTable<TData, TValue>({
|
|||||||
onPageSizeChange: onPageSizeChangeProp,
|
onPageSizeChange: onPageSizeChangeProp,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
expandable = false,
|
expandable = false,
|
||||||
|
disabled=false,
|
||||||
renderExpandedRow
|
renderExpandedRow
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -175,6 +177,11 @@ export function LogDataTable<TData, TValue>({
|
|||||||
|
|
||||||
// Apply tab filter to data
|
// Apply tab filter to data
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
|
// If disabled, return empty array to prevent data loading
|
||||||
|
if (disabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
if (!tabs || activeTab === "") {
|
if (!tabs || activeTab === "") {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -185,7 +192,7 @@ export function LogDataTable<TData, TValue>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return data.filter(activeTabFilter.filterFn);
|
return data.filter(activeTabFilter.filterFn);
|
||||||
}, [data, tabs, activeTab]);
|
}, [data, tabs, activeTab, disabled]);
|
||||||
|
|
||||||
// Toggle row expansion
|
// Toggle row expansion
|
||||||
const toggleRowExpansion = (rowId: string) => {
|
const toggleRowExpansion = (rowId: string) => {
|
||||||
@@ -219,9 +226,12 @@ export function LogDataTable<TData, TValue>({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-6 p-0"
|
className="h-6 w-6 p-0"
|
||||||
|
disabled={disabled}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
toggleRowExpansion(row.id);
|
if (!disabled) {
|
||||||
e.stopPropagation();
|
toggleRowExpansion(row.id);
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
@@ -236,7 +246,7 @@ export function LogDataTable<TData, TValue>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return [expansionColumn, ...columns];
|
return [expansionColumn, ...columns];
|
||||||
}, [columns, expandable, expandedRows, toggleRowExpansion]);
|
}, [columns, expandable, expandedRows, toggleRowExpansion, disabled]);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredData,
|
data: filteredData,
|
||||||
@@ -298,6 +308,8 @@ export function LogDataTable<TData, TValue>({
|
|||||||
}, [currentPage, table, isServerPagination]);
|
}, [currentPage, table, isServerPagination]);
|
||||||
|
|
||||||
const handleTabChange = (value: string) => {
|
const handleTabChange = (value: string) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
setActiveTab(value);
|
setActiveTab(value);
|
||||||
// Reset to first page when changing tabs
|
// Reset to first page when changing tabs
|
||||||
table.setPageIndex(0);
|
table.setPageIndex(0);
|
||||||
@@ -305,6 +317,8 @@ export function LogDataTable<TData, TValue>({
|
|||||||
|
|
||||||
// Enhanced pagination component that updates our local state
|
// Enhanced pagination component that updates our local state
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
// setPageSize(newPageSize);
|
// setPageSize(newPageSize);
|
||||||
table.setPageSize(newPageSize);
|
table.setPageSize(newPageSize);
|
||||||
|
|
||||||
@@ -321,6 +335,8 @@ export function LogDataTable<TData, TValue>({
|
|||||||
|
|
||||||
// Handle page changes for server pagination
|
// Handle page changes for server pagination
|
||||||
const handlePageChange = (newPageIndex: number) => {
|
const handlePageChange = (newPageIndex: number) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
if (isServerPagination && onPageChange) {
|
if (isServerPagination && onPageChange) {
|
||||||
onPageChange(newPageIndex);
|
onPageChange(newPageIndex);
|
||||||
}
|
}
|
||||||
@@ -330,6 +346,8 @@ export function LogDataTable<TData, TValue>({
|
|||||||
start: DateTimeValue,
|
start: DateTimeValue,
|
||||||
end: DateTimeValue
|
end: DateTimeValue
|
||||||
) => {
|
) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
setStartDate(start);
|
setStartDate(start);
|
||||||
setEndDate(end);
|
setEndDate(end);
|
||||||
onDateRangeChange?.(start, end);
|
onDateRangeChange?.(start, end);
|
||||||
@@ -358,14 +376,15 @@ export function LogDataTable<TData, TValue>({
|
|||||||
endValue={endDate}
|
endValue={endDate}
|
||||||
onRangeChange={handleDateRangeChange}
|
onRangeChange={handleDateRangeChange}
|
||||||
className="flex-wrap gap-2"
|
className="flex-wrap gap-2"
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2 sm:justify-end">
|
<div className="flex items-start gap-2 sm:justify-end">
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onRefresh}
|
onClick={() => !disabled && onRefresh()}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing || disabled}
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||||
@@ -374,7 +393,7 @@ export function LogDataTable<TData, TValue>({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{onExport && (
|
{onExport && (
|
||||||
<Button onClick={onExport} disabled={isExporting}>
|
<Button onClick={() => !disabled && onExport()} disabled={isExporting || disabled}>
|
||||||
<Download
|
<Download
|
||||||
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`}
|
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`}
|
||||||
/>
|
/>
|
||||||
@@ -415,7 +434,7 @@ export function LogDataTable<TData, TValue>({
|
|||||||
"selected"
|
"selected"
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
expandable
|
expandable && !disabled
|
||||||
? toggleRowExpansion(
|
? toggleRowExpansion(
|
||||||
row.id
|
row.id
|
||||||
)
|
)
|
||||||
@@ -500,6 +519,7 @@ export function LogDataTable<TData, TValue>({
|
|||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
isServerPagination={isServerPagination}
|
isServerPagination={isServerPagination}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user