refactor: implement breadcrumb navigation and modernize layout components across tenant pages

This commit is contained in:
Yashwin 2026-05-12 15:42:04 +05:30
parent af7e83c59c
commit 4024711100
13 changed files with 495 additions and 344 deletions

View File

@ -115,6 +115,17 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
<nav className="flex items-center gap-1.5 md:gap-2">
{breadcrumbs && breadcrumbs.length > 0 ? (
<>
{breadcrumbs[0].label !== 'QAssure' && (
<>
<button
onClick={() => navigate(roles.includes('super_admin') ? '/dashboard' : '/tenant')}
className="text-xs md:text-[13px] font-normal text-[#6b7280] hover:text-[#0f1724] transition-colors cursor-pointer"
>
QAssure
</button>
<ChevronRight className="w-3 h-3 md:w-3.5 md:h-3.5 text-[#6b7280]" />
</>
)}
{breadcrumbs.map((crumb, index) => (
<div key={index} className="flex items-center gap-1.5 md:gap-2">
{crumb.path ? (
@ -138,7 +149,7 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
) : (
<>
<button
onClick={() => navigate('/dashboard')}
onClick={() => navigate(roles.includes('super_admin') ? '/dashboard' : '/tenant')}
className="text-xs md:text-[13px] font-normal text-[#6b7280] hover:text-[#0f1724] transition-colors cursor-pointer"
>
QAssure

View File

@ -1,12 +1,12 @@
import type { ReactElement, ReactNode } from 'react';
import { Fragment } from 'react';
import { ChevronDown } from 'lucide-react';
import type { ReactElement, ReactNode } from "react";
import { Fragment } from "react";
import { ChevronDown } from "lucide-react";
export interface Column<T> {
key: string;
label: string;
render?: (item: T) => ReactNode;
align?: 'left' | 'right' | 'center';
align?: "left" | "right" | "center";
mobileLabel?: string;
}
@ -32,7 +32,7 @@ export const DataTable = <T,>({
columns,
keyExtractor,
mobileCardRenderer,
emptyMessage = 'No data found',
emptyMessage = "No data found",
isLoading = false,
error = null,
expandableRows = false,
@ -43,8 +43,13 @@ export const DataTable = <T,>({
showExpandColumn = true,
expandedColSpan,
}: DataTableProps<T>): ReactElement => {
const canExpand = expandableRows && !!onRowExpandToggle && !!isRowExpanded && !!renderExpandedRow;
const desktopColSpan = expandedColSpan || columns.length + (canExpand && showExpandColumn ? 1 : 0);
const canExpand =
expandableRows &&
!!onRowExpandToggle &&
!!isRowExpanded &&
!!renderExpandedRow;
const desktopColSpan =
expandedColSpan || columns.length + (canExpand && showExpandColumn ? 1 : 0);
// Loading State
if (isLoading) {
@ -75,15 +80,18 @@ export const DataTable = <T,>({
<thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
{canExpand && showExpandColumn && (
<th className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left" aria-label="Expand" />
<th
className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
aria-label="Expand"
/>
)}
{columns.map((column) => {
const alignClass =
column.align === 'right'
? 'text-right'
: column.align === 'center'
? 'text-center'
: 'text-left';
column.align === "right"
? "text-right"
: column.align === "center"
? "text-center"
: "text-left";
return (
<th
key={column.key}
@ -97,7 +105,10 @@ export const DataTable = <T,>({
</thead>
<tbody>
<tr>
<td colSpan={desktopColSpan} className="px-3 md:px-2 lg:px-4 xl:px-5 py-6 md:py-3 lg:py-7 xl:py-8 text-center text-xs md:text-xs lg:text-[13px] text-[#6b7280]">
<td
colSpan={desktopColSpan}
className="px-3 md:px-2 lg:px-4 xl:px-5 py-6 md:py-3 lg:py-7 xl:py-8 text-center text-xs md:text-xs lg:text-[13px] text-[#6b7280]"
>
{emptyMessage}
</td>
</tr>
@ -122,23 +133,26 @@ export const DataTable = <T,>({
<thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
{canExpand && showExpandColumn && (
<th className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left" aria-label="Expand" />
<th
className="w-10 px-2 py-2 md:py-1 lg:py-2.5 xl:py-3 text-left"
aria-label="Expand"
/>
)}
{columns.map((column) => {
const alignClass =
column.align === 'right'
? 'text-right'
: column.align === 'center'
? 'text-center'
: 'text-left';
return (
<th
key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
>
{column.label}
</th>
);
column.align === "right"
? "text-right"
: column.align === "center"
? "text-center"
: "text-left";
return (
<th
key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2 md:py-1 lg:py-2.5 xl:py-3 ${alignClass} text-[10px] md:text-[8px] lg:text-[13px] font-medium text-[#9aa6b2] uppercase`}
>
{column.label}
</th>
);
})}
</tr>
</thead>
@ -163,28 +177,37 @@ export const DataTable = <T,>({
onRowExpandToggle(item);
}}
>
<ChevronDown className={`w-4 h-4 transition-transform ${expanded ? 'rotate-180' : ''}`} />
<ChevronDown
className={`w-4 h-4 transition-transform ${expanded ? "rotate-180" : ""}`}
/>
</button>
</td>
)}
{columns.map((column) => {
const alignClass =
column.align === 'right'
? 'text-right'
: column.align === 'center'
? 'text-center'
: 'text-left';
column.align === "right"
? "text-right"
: column.align === "center"
? "text-center"
: "text-left";
return (
<td key={column.key} className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-[13px]`}>
{column.render ? column.render(item) : String((item as any)[column.key])}
<td
key={column.key}
className={`px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4 ${alignClass} text-xs md:text-[10px] lg:text-[13px]`}
>
{column.render
? column.render(item)
: String((item as any)[column.key])}
</td>
);
})}
</tr>
{canExpand && expanded && (
<tr className="border-b border-[rgba(0,0,0,0.08)] bg-white">
<td colSpan={desktopColSpan} className="px-3 md:px-2 lg:px-4 xl:px-5 py-2.5 md:py-1.5 lg:py-3 xl:py-4">
{renderExpandedRow(item)}
<tr className="border-t border-[rgba(0,0,0,0.08)] bg-[#F9F9F9]">
<td colSpan={desktopColSpan}>
<div className="flex flex-col items-start w-full bg-[#FFF] border border-gray-300 rounded-md p-4 text-xs text-gray-700 m-4">
{renderExpandedRow(item)}
</div>
</td>
</tr>
)}
@ -199,7 +222,9 @@ export const DataTable = <T,>({
{/* Mobile Card View */}
<div className="md:hidden divide-y divide-[rgba(0,0,0,0.08)]">
{mobileCardRenderer
? data.map((item) => <div key={keyExtractor(item)}>{mobileCardRenderer(item)}</div>)
? data.map((item) => (
<div key={keyExtractor(item)}>{mobileCardRenderer(item)}</div>
))
: data.map((item) => {
const expanded = canExpand ? !!isRowExpanded(item) : false;
return (
@ -212,7 +237,9 @@ export const DataTable = <T,>({
aria-expanded={expanded}
onClick={() => onRowExpandToggle(item)}
>
<ChevronDown className={`w-4 h-4 transition-transform ${expanded ? 'rotate-180' : ''}`} />
<ChevronDown
className={`w-4 h-4 transition-transform ${expanded ? "rotate-180" : ""}`}
/>
</button>
</div>
)}
@ -222,14 +249,14 @@ export const DataTable = <T,>({
{column.mobileLabel || column.label}:
</span>
<div className="text-xs sm:text-sm text-[#0f1724]">
{column.render ? column.render(item) : String((item as any)[column.key])}
{column.render
? column.render(item)
: String((item as any)[column.key])}
</div>
</div>
))}
{canExpand && expanded && renderExpandedRow && (
<div className="mt-3">
{renderExpandedRow(item)}
</div>
<div className="mt-3">{renderExpandedRow(item)}</div>
)}
</div>
);

View File

@ -133,7 +133,7 @@ export const PromptTestCaseResultsListModal = ({
showExpandColumn={true}
expandedColSpan={columns.length + 1}
renderExpandedRow={(item) => (
<div className="bg-slate-50/70 border border-slate-200/50 p-4 rounded-xl text-xs text-slate-800 select-all">
<>
<span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none">
Output
</span>
@ -142,7 +142,7 @@ export const PromptTestCaseResultsListModal = ({
) : (
"No output generated."
)}
</div>
</>
)}
/>
</div>

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import type { ReactElement } from 'react';
import { Layout } from '@/components/layout/Layout';
import { useState, useEffect } from "react";
import type { ReactElement } from "react";
import { Layout } from "@/components/layout/Layout";
import {
ViewAuditLogModal,
DataTable,
@ -9,14 +9,14 @@ import {
StatusBadge,
SearchBox,
type Column,
} from '@/components/shared';
import { Download, ArrowUpDown } from 'lucide-react';
import { auditLogService } from '@/services/audit-log-service';
import { moduleService } from '@/services/module-service';
import type { AuditLog } from '@/types/audit-log';
import { useAppTheme } from '@/hooks/useAppTheme';
import { PrimaryButton } from '@/components/shared';
import { useAppSelector } from '@/hooks/redux-hooks';
} from "@/components/shared";
import { Download, ArrowUpDown } from "lucide-react";
import { auditLogService } from "@/services/audit-log-service";
import { moduleService } from "@/services/module-service";
import type { AuditLog } from "@/types/audit-log";
import { useAppTheme } from "@/hooks/useAppTheme";
import { PrimaryButton } from "@/components/shared";
import { useAppSelector } from "@/hooks/redux-hooks";
export interface AuditLogsProps {
customTenantId?: string;
@ -26,55 +26,79 @@ export interface AuditLogsProps {
// Helper function to format date
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
// Helper function to get action badge variant
const getActionVariant = (action: string): 'success' | 'failure' | 'info' | 'process' => {
const getActionVariant = (
action: string,
): "success" | "failure" | "info" | "process" => {
const lowerAction = action.toLowerCase();
if (lowerAction.includes('create') || lowerAction.includes('register')) return 'success';
if (lowerAction.includes('update') || lowerAction.includes('version_update') || lowerAction.includes('login')) return 'info';
if (lowerAction.includes('delete') || lowerAction.includes('deregister')) return 'failure';
if (lowerAction.includes('read') || lowerAction.includes('get') || lowerAction.includes('status_change')) return 'process';
return 'info';
if (lowerAction.includes("create") || lowerAction.includes("register"))
return "success";
if (
lowerAction.includes("update") ||
lowerAction.includes("version_update") ||
lowerAction.includes("login")
)
return "info";
if (lowerAction.includes("delete") || lowerAction.includes("deregister"))
return "failure";
if (
lowerAction.includes("read") ||
lowerAction.includes("get") ||
lowerAction.includes("status_change")
)
return "process";
return "info";
};
// Helper function to get method badge variant
const getMethodVariant = (method: string | null): 'success' | 'failure' | 'info' | 'process' => {
if (!method) return 'info';
const getMethodVariant = (
method: string | null,
): "success" | "failure" | "info" | "process" => {
if (!method) return "info";
const upperMethod = method.toUpperCase();
if (upperMethod === 'GET') return 'success';
if (upperMethod === 'POST') return 'info';
if (upperMethod === 'PUT' || upperMethod === 'PATCH') return 'process';
if (upperMethod === 'DELETE') return 'failure';
return 'info';
if (upperMethod === "GET") return "success";
if (upperMethod === "POST") return "info";
if (upperMethod === "PUT" || upperMethod === "PATCH") return "process";
if (upperMethod === "DELETE") return "failure";
return "info";
};
// Helper function to get status badge color based on response status
const getStatusColor = (status: number | null): string => {
if (!status) return 'text-[#6b7280]';
if (status >= 200 && status < 300) return 'text-[#10b981]';
if (status >= 300 && status < 400) return 'text-[#f59e0b]';
if (status >= 400) return 'text-[#ef4444]';
return 'text-[#6b7280]';
if (!status) return "text-[#6b7280]";
if (status >= 200 && status < 300) return "text-[#10b981]";
if (status >= 300 && status < 400) return "text-[#f59e0b]";
if (status >= 400) return "text-[#ef4444]";
return "text-[#6b7280]";
};
const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}): ReactElement => {
const AuditLogs = ({
customTenantId,
hideLayout = false,
}: AuditLogsProps = {}): ReactElement => {
const { primaryColor } = useAppTheme();
const roles = useAppSelector((state) => state.auth.roles);
const authTenantId = useAppSelector((state) => state.auth.tenantId);
const tenantId = customTenantId || authTenantId;
const isTenantAdmin = customTenantId ? true : roles?.includes('tenant_admin');
const isTenantAdmin = customTenantId ? true : roles?.includes("tenant_admin");
const [expandedId, setExpandedId] = useState<string | null>(null);
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
const [resourceTypes, setResourceTypes] = useState<{ value: string; label: string }[]>([]);
const [modules, setModules] = useState<{ value: string; label: string }[]>([]);
const [resourceTypes, setResourceTypes] = useState<
{ value: string; label: string }[]
>([]);
const [modules, setModules] = useState<{ value: string; label: string }[]>(
[],
);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
@ -98,45 +122,53 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
// Filter state
const [methodFilter, setMethodFilter] = useState<string | null>(null);
const [actionFilter, setActionFilter] = useState<string | null>(null);
const [resourceTypeFilter, setResourceTypeFilter] = useState<string | null>(null);
const [resourceTypeFilter, setResourceTypeFilter] = useState<string | null>(
null,
);
const [moduleIdFilter, setModuleIdFilter] = useState<string | null>(null); // New module_id filter
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [startDate, setStartDate] = useState<string>("");
const [endDate, setEndDate] = useState<string>("");
const [orderBy, setOrderBy] = useState<string[] | null>(null);
const [search, setSearch] = useState<string>('');
const [debouncedSearch, setDebouncedSearch] = useState<string>('');
const [search, setSearch] = useState<string>("");
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
// View modal
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(null);
const [selectedAuditLogId, setSelectedAuditLogId] = useState<string | null>(
null,
);
const fetchResourceTypes = async (): Promise<void> => {
try {
const response = await auditLogService.getResourceTypesDropdown(tenantId as string | undefined);
const response = await auditLogService.getResourceTypesDropdown(
tenantId as string | undefined,
);
if (response.success) {
const options = response.data.map((rt: any) => ({
value: rt.value,
label: rt.label
label: rt.label,
}));
setResourceTypes(options);
}
} catch (err) {
console.error('Failed to load resource types', err);
console.error("Failed to load resource types", err);
}
};
const fetchModules = async (): Promise<void> => {
try {
const response = await moduleService.getMyModules(tenantId as string | undefined);
const response = await moduleService.getMyModules(
tenantId as string | undefined,
);
if (response.success) {
const options = response.data.map((m: any) => ({
value: m.id,
label: m.name
label: m.name,
}));
setModules(options);
}
} catch (err) {
console.error('Failed to load modules', err);
console.error("Failed to load modules", err);
}
};
@ -144,12 +176,13 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
try {
setIsLoading(true);
setError(null);
setExpandedId(null);
let response;
if (isTenantAdmin) {
response = await auditLogService.getAll(
currentPage,
limit,
currentPage,
limit,
{
method: methodFilter,
action: actionFilter,
@ -158,22 +191,29 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
startDate: startDate || null,
endDate: endDate || null,
tenant_id: tenantId as string | null,
search: debouncedSearch || null
},
orderBy
search: debouncedSearch || null,
},
orderBy,
);
} else {
response = await auditLogService.getMyLogs(currentPage, limit, moduleIdFilter, debouncedSearch || null);
response = await auditLogService.getMyLogs(
currentPage,
limit,
moduleIdFilter,
debouncedSearch || null,
);
}
if (response.success) {
setAuditLogs(response.data);
setPagination(response.pagination);
} else {
setError('Failed to load audit logs');
setError("Failed to load audit logs");
}
} catch (err: any) {
setError(err?.response?.data?.error?.message || 'Failed to load audit logs');
setError(
err?.response?.data?.error?.message || "Failed to load audit logs",
);
} finally {
setIsLoading(false);
}
@ -197,7 +237,19 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
// Fetch audit logs on mount and when pagination/filters change
useEffect(() => {
fetchAuditLogs();
}, [currentPage, limit, methodFilter, actionFilter, resourceTypeFilter, moduleIdFilter, startDate, endDate, orderBy, tenantId, debouncedSearch]);
}, [
currentPage,
limit,
methodFilter,
actionFilter,
resourceTypeFilter,
moduleIdFilter,
startDate,
endDate,
orderBy,
tenantId,
debouncedSearch,
]);
// View audit log handler
const handleViewAuditLog = (auditLogId: string): void => {
@ -209,27 +261,30 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
const handleExport = async (): Promise<void> => {
try {
const response = await auditLogService.export({
format: 'json',
format: "json",
startDate: startDate || undefined,
endDate: endDate || undefined,
tenantId: tenantId || undefined
tenantId: tenantId || undefined,
});
if (response.success) {
// In a real app, we'd trigger a file download here.
// In a real app, we'd trigger a file download here.
// For now, we'll just log and show a message since the response is JSON.
console.log('Export data:', response.data.records);
const blob = new Blob([JSON.stringify(response.data.records, null, 2)], { type: 'application/json' });
console.log("Export data:", response.data.records);
const blob = new Blob(
[JSON.stringify(response.data.records, null, 2)],
{ type: "application/json" },
);
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.download = `audit-logs-${new Date().toISOString().split('T')[0]}.json`;
link.download = `audit-logs-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
} catch (err: any) {
alert('Failed to export audit logs: ' + (err.message || 'Unknown error'));
alert("Failed to export audit logs: " + (err.message || "Unknown error"));
}
};
@ -239,81 +294,107 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
return response.data;
};
const toggleExpand = (id: string): void => {
setExpandedId((prev) => (prev === id ? null : id));
};
const renderExpanded = (row: AuditLog): ReactElement => {
return (
<div className="w-full overflow-auto font-mono leading-6">
<pre className="whitespace-pre-wrap break-words">
{JSON.stringify(row.metadata, null, 2)}
</pre>
</div>
);
};
// Define table columns
const columns: Column<AuditLog>[] = [
{
key: 'created_at',
label: 'Timestamp',
key: "created_at",
label: "Timestamp",
render: (log) => (
<span className="text-sm font-normal text-[#0f1724] whitespace-nowrap">{formatDate(log.created_at)}</span>
<span className="text-sm font-normal text-[#0f1724] whitespace-nowrap">
{formatDate(log.created_at)}
</span>
),
mobileLabel: 'Time',
mobileLabel: "Time",
},
{
key: 'resource_type',
label: 'Resource Type',
key: "resource_type",
label: "Resource Type",
render: (log) => (
<span className="text-sm font-normal text-[#0f1724]">{log.resource_type}</span>
),
},
{
key: 'module',
label: 'Module',
render: (log) => (
<span className="text-sm font-normal text-[#475569]">
{log.module?.name || 'Platform'}
<span className="text-sm font-normal text-[#0f1724]">
{log.resource_type}
</span>
),
},
{
key: 'action',
label: 'Action',
key: "module",
label: "Module",
render: (log) => (
<span className="text-sm font-normal text-[#475569]">
{log.module?.name || "Platform"}
</span>
),
},
{
key: "action",
label: "Action",
render: (log) => (
<StatusBadge variant={getActionVariant(log.action)}>
{log.action}
</StatusBadge>
),
},
...(isTenantAdmin ? [{
key: 'user',
label: 'User',
render: (log: AuditLog) => (
<span className="text-sm font-normal text-[#0f1724]">
{log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')}
</span>
),
}] : []),
...(isTenantAdmin
? [
{
key: "user",
label: "User",
render: (log: AuditLog) => (
<span className="text-sm font-normal text-[#0f1724]">
{log.user
? `${log.user.first_name} ${log.user.last_name}`
: log.user_email || "N/A"}
</span>
),
},
]
: []),
{
key: 'request_method',
label: 'Method',
key: "request_method",
label: "Method",
render: (log) => (
<StatusBadge variant={getMethodVariant(log.request_method)}>
{log.request_method ? log.request_method.toUpperCase() : 'N/A'}
{log.request_method ? log.request_method.toUpperCase() : "N/A"}
</StatusBadge>
),
},
{
key: 'response_status',
label: 'Status',
key: "response_status",
label: "Status",
render: (log) => (
<span className={`text-sm font-normal ${getStatusColor(log.response_status)}`}>
{log.response_status || 'N/A'}
<span
className={`text-sm font-normal ${getStatusColor(log.response_status)}`}
>
{log.response_status || "N/A"}
</span>
),
},
{
key: 'ip_address',
label: 'IP Address',
key: "ip_address",
label: "IP Address",
render: (log) => (
<span className="text-sm font-normal text-[#0f1724] font-mono">
{log.ip_address || 'N/A'}
{log.ip_address || "N/A"}
</span>
),
},
{
key: 'actions',
label: 'Actions',
align: 'right',
key: "actions",
label: "Actions",
align: "right",
render: (log) => (
<div className="flex justify-end">
<button
@ -334,7 +415,9 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
<div className="p-4">
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-[#0f1724] truncate">{log.resource_type}</h3>
<h3 className="text-sm font-medium text-[#0f1724] truncate">
{log.resource_type}
</h3>
<div className="mt-1">
<StatusBadge variant={getActionVariant(log.action)}>
{log.action}
@ -353,30 +436,38 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<span className="text-[#9aa6b2]">Timestamp:</span>
<p className="text-[#0f1724] font-normal mt-1">{formatDate(log.created_at)}</p>
<p className="text-[#0f1724] font-normal mt-1">
{formatDate(log.created_at)}
</p>
</div>
<div>
<span className="text-[#9aa6b2]">Module:</span>
<p className="text-[#0f1724] font-normal mt-1">{log.module?.name || 'Platform'}</p>
<p className="text-[#0f1724] font-normal mt-1">
{log.module?.name || "Platform"}
</p>
</div>
<div>
<span className="text-[#9aa6b2]">User:</span>
<p className="text-[#0f1724] font-normal mt-1">
{log.user ? `${log.user.first_name} ${log.user.last_name}` : (log.user_email || 'N/A')}
{log.user
? `${log.user.first_name} ${log.user.last_name}`
: log.user_email || "N/A"}
</p>
</div>
<div>
<span className="text-[#9aa6b2]">Method:</span>
<div className="mt-1">
<StatusBadge variant={getMethodVariant(log.request_method)}>
{log.request_method ? log.request_method.toUpperCase() : 'N/A'}
{log.request_method ? log.request_method.toUpperCase() : "N/A"}
</StatusBadge>
</div>
</div>
<div>
<span className="text-[#9aa6b2]">Status:</span>
<p className={`font-normal mt-1 ${getStatusColor(log.response_status)}`}>
{log.response_status || 'N/A'}
<p
className={`font-normal mt-1 ${getStatusColor(log.response_status)}`}
>
{log.response_status || "N/A"}
</p>
</div>
</div>
@ -393,7 +484,7 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
{/* Search and Filters */}
<div className="flex flex-wrap items-center gap-3">
{/* Global Search */}
<SearchBox
<SearchBox
value={search}
onChange={setSearch}
placeholder="Search logs & metadata..."
@ -406,14 +497,14 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
<FilterDropdown
label="Action"
options={[
{ value: 'LOGIN', label: 'LOGIN' },
{ value: 'LOGOUT', label: 'LOGOUT' },
{ value: 'CREATE', label: 'CREATE' },
{ value: 'UPDATE', label: 'UPDATE' },
{ value: 'DELETE', label: 'DELETE' },
{ value: 'SUBMIT', label: 'SUBMIT' },
{ value: 'APPROVE', label: 'APPROVE' },
{ value: 'REJECT', label: 'REJECT' },
{ value: "LOGIN", label: "LOGIN" },
{ value: "LOGOUT", label: "LOGOUT" },
{ value: "CREATE", label: "CREATE" },
{ value: "UPDATE", label: "UPDATE" },
{ value: "DELETE", label: "DELETE" },
{ value: "SUBMIT", label: "SUBMIT" },
{ value: "APPROVE", label: "APPROVE" },
{ value: "REJECT", label: "REJECT" },
// {value: 'PUBLISH', label: 'PUBLISH'},
// {value: 'ARCHIVE', label: 'ARCHIVE'},
// {value: 'CHECKOUT', label: 'CHECKOUT'},
@ -467,10 +558,13 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
<FilterDropdown
label="Sort by"
options={[
{ value: ['created_at', 'desc'], label: 'Newest First' },
{ value: ['created_at', 'asc'], label: 'Oldest First' },
{ value: ['action', 'asc'], label: 'Action (A-Z)' },
{ value: ['resource_type', 'asc'], label: 'Resource Type (A-Z)' },
{ value: ["created_at", "desc"], label: "Newest First" },
{ value: ["created_at", "asc"], label: "Oldest First" },
{ value: ["action", "asc"], label: "Action (A-Z)" },
{
value: ["resource_type", "asc"],
label: "Resource Type (A-Z)",
},
]}
value={orderBy}
onChange={(value) => {
@ -502,8 +596,8 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
<div className="flex flex-wrap items-center gap-3 md:gap-6 pt-1">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">From:</span>
<input
type="date"
<input
type="date"
value={startDate}
onChange={(e) => {
setStartDate(e.target.value);
@ -514,7 +608,7 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">To:</span>
<input
<input
type="date"
value={endDate}
onChange={(e) => {
@ -524,16 +618,22 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
/>
</div>
{(startDate || endDate || actionFilter || resourceTypeFilter || methodFilter || moduleIdFilter || search) && (
<button
{(startDate ||
endDate ||
actionFilter ||
resourceTypeFilter ||
methodFilter ||
moduleIdFilter ||
search) && (
<button
onClick={() => {
setStartDate('');
setEndDate('');
setStartDate("");
setEndDate("");
setActionFilter(null);
setResourceTypeFilter(null);
setMethodFilter(null);
setModuleIdFilter(null);
setSearch('');
setSearch("");
setCurrentPage(1);
}}
className="text-xs hover:underline decoration-offset-2"
@ -553,7 +653,16 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
isLoading={isLoading}
error={error}
mobileCardRenderer={mobileCardRenderer}
emptyMessage={isTenantAdmin ? "No audit logs found matching your criteria" : "No activity recorded for your account yet"}
emptyMessage={
isTenantAdmin
? "No audit logs found matching your criteria"
: "No activity recorded for your account yet"
}
expandableRows
isRowExpanded={(row) => expandedId === row.id}
onRowExpandToggle={(row) => toggleExpand(row.id)}
renderExpandedRow={renderExpanded}
onRowClick={(row) => toggleExpand(row.id)}
/>
{/* Pagination */}
@ -595,10 +704,10 @@ const AuditLogs = ({ customTenantId, hideLayout = false }: AuditLogsProps = {}):
<Layout
currentPage="Audit Logs"
pageHeader={{
title: isTenantAdmin ? 'System Audit Logs' : 'My Activity',
description: isTenantAdmin
? 'Monitor all activities and changes across the quality platform.'
: 'View a chronological history of your own actions on the platform.',
title: isTenantAdmin ? "System Audit Logs" : "My Activity",
description: isTenantAdmin
? "Monitor all activities and changes across the quality platform."
: "View a chronological history of your own actions on the platform.",
}}
>
{content}

View File

@ -181,6 +181,10 @@ const CompletionCreate = (): ReactElement => {
return (
<Layout
currentPage="Create Completion"
breadcrumbs={[
{ label: "Completions", path: "/tenant/ai/completions" },
{ label: "Create Completion" },
]}
pageHeader={{
title: "Completions Playground",
description:

View File

@ -1,8 +1,7 @@
import { useEffect, useState, type ReactElement, type ReactNode } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import { Layout } from "@/components/layout/Layout";
import { PrimaryButton, SecondaryButton, StatusBadge } from "@/components/shared";
import { PrimaryButton, StatusBadge } from "@/components/shared";
import { aiService } from "@/services/ai-service";
import type { AICompletion } from "@/types/ai";
import { showToast } from "@/utils/toast";
@ -57,16 +56,14 @@ const CompletionDetail = (): ReactElement => {
return (
<Layout
currentPage="Completion History"
currentPage="Completion Details"
breadcrumbs={[
{ label: "Completions", path: "/tenant/ai/completions" },
{ label: "Completion details" },
]}
pageHeader={{
title: "Completion details",
description: "Full request, response, and usage for this completion.",
action: (
<SecondaryButton type="button" onClick={() => navigate("/tenant/ai/completions")}>
<ArrowLeft className="w-4 h-4 mr-1.5 inline" />
Back to list
</SecondaryButton>
),
}}
>
<div className="space-y-5">
@ -165,15 +162,6 @@ const CompletionDetail = (): ReactElement => {
</pre>
</section>
)} */}
<div className="flex gap-2">
<PrimaryButton type="button" onClick={() => navigate("/tenant/ai/completions")}>
Back to list
</PrimaryButton>
<SecondaryButton type="button" onClick={() => navigate("/tenant/ai/completions/create")}>
New completion
</SecondaryButton>
</div>
</>
)}
</div>

View File

@ -1,4 +1,10 @@
import { useCallback, useEffect, useMemo, useState, type ReactElement } from "react";
import {
useCallback,
useEffect,
useMemo,
useState,
type ReactElement,
} from "react";
import { useNavigate } from "react-router-dom";
import { ChevronDown, SlidersHorizontal } from "lucide-react";
import { Layout } from "@/components/layout/Layout";
@ -12,7 +18,10 @@ import {
} from "@/components/shared";
import { aiService } from "@/services/ai-service";
import { moduleService } from "@/services/module-service";
import { tenantService, type TenantUserDropdownItem } from "@/services/tenant-service";
import {
tenantService,
type TenantUserDropdownItem,
} from "@/services/tenant-service";
import type { AICompletion, AIProviderInfo } from "@/types/ai";
import type { MyModule } from "@/types/module";
import { showToast } from "@/utils/toast";
@ -37,7 +46,9 @@ const CompletionHistory = (): ReactElement => {
const [isMetaLoading, setIsMetaLoading] = useState<boolean>(true);
const [providers, setProviders] = useState<AIProviderInfo[]>([]);
const [models, setModels] = useState<Array<{ id: string; provider: string; isDefault?: boolean }>>([]);
const [models, setModels] = useState<
Array<{ id: string; provider: string; isDefault?: boolean }>
>([]);
const [modules, setModules] = useState<MyModule[]>([]);
const [tenantUsers, setTenantUsers] = useState<TenantUserDropdownItem[]>([]);
@ -67,7 +78,8 @@ const CompletionHistory = (): ReactElement => {
});
const providerOptions = useMemo(
() => providers.map((p) => ({ value: p.name, label: p.displayName || p.name })),
() =>
providers.map((p) => ({ value: p.name, label: p.displayName || p.name })),
[providers],
);
@ -107,20 +119,21 @@ const CompletionHistory = (): ReactElement => {
const loadMeta = useCallback(async (): Promise<void> => {
setIsMetaLoading(true);
try {
const [providerData, modelData, modulesRes, usersData] = await Promise.all([
aiService.getProviders(),
aiService.getModels(),
moduleService.getMyModules(),
tenantService.getCurrentTenantUsersDropdown(),
]);
const [providerData, modelData, modulesRes, usersData] =
await Promise.all([
aiService.getProviders(),
aiService.getModels(),
moduleService.getMyModules(),
tenantService.getCurrentTenantUsersDropdown(),
]);
setProviders(providerData);
setModels(modelData);
setModules(modulesRes.data || []);
setTenantUsers(usersData);
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { error?: { message?: string } } } })?.response?.data?.error
?.message || "Failed to load filter options";
(err as { response?: { data?: { error?: { message?: string } } } })
?.response?.data?.error?.message || "Failed to load filter options";
showToast.error(msg);
} finally {
setIsMetaLoading(false);
@ -152,8 +165,9 @@ const CompletionHistory = (): ReactElement => {
});
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { error?: { message?: string } } } })?.response?.data?.error
?.message || "Failed to load completion history";
(err as { response?: { data?: { error?: { message?: string } } } })
?.response?.data?.error?.message ||
"Failed to load completion history";
setError(msg);
showToast.error(msg);
} finally {
@ -199,10 +213,13 @@ const CompletionHistory = (): ReactElement => {
const renderExpanded = (row: AICompletion): ReactElement => {
const total =
row.usage?.total_tokens ?? row.total_tokens ?? (row.prompt_tokens ?? 0) + (row.completion_tokens ?? 0);
row.usage?.total_tokens ??
row.total_tokens ??
(row.prompt_tokens ?? 0) + (row.completion_tokens ?? 0);
const preview = (row.response || row.content || "").slice(0, 800);
return (
<div className="bg-[#f8fafc] border border-[rgba(0,0,0,0.06)] rounded-md p-3 md:p-4 text-xs text-[#334155] space-y-2">
// <div className="bg-[#f8fafc] border border-[rgba(0,0,0,0.06)] rounded-md p-3 md:p-4 text-xs text-[#334155] space-y-2">
<>
{/* <p>
<span className="font-semibold text-[#475569]">IDs </span>
Module: <code className="text-[11px]">{row.module_id || "—"}</code>
@ -214,7 +231,9 @@ const CompletionHistory = (): ReactElement => {
{row.correlation_id || "—"}
</p> */}
<p>
<span className="font-semibold text-[#475569]">Tokens / cost / latency </span>
<span className="font-semibold text-[#475569]">
Tokens / cost / latency {" "}
</span>
{`${total} tokens · USD ${Number(row.cost ?? 0).toFixed(6)} · ${row.latency_ms ?? 0} ms`}
</p>
{row.use_case && (
@ -230,8 +249,12 @@ const CompletionHistory = (): ReactElement => {
</p>
)}
<div>
<span className="font-semibold text-[#475569] block mb-1">Response preview</span>
<p className="whitespace-pre-wrap break-words leading-relaxed">{preview || "—"}</p>
<span className="font-semibold text-[#475569] block mb-1">
Response preview
</span>
<p className="whitespace-pre-wrap break-words leading-relaxed">
{preview || "—"}
</p>
</div>
<button
type="button"
@ -241,7 +264,8 @@ const CompletionHistory = (): ReactElement => {
>
Open full detail view
</button>
</div>
</>
// </div>
);
};
@ -249,19 +273,31 @@ const CompletionHistory = (): ReactElement => {
{
key: "date",
label: "Date",
render: (row) => <span className="whitespace-nowrap">{formatListDate(row.created_at)}</span>,
render: (row) => (
<span className="whitespace-nowrap">
{formatListDate(row.created_at)}
</span>
),
},
{
key: "module_name",
label: "Module",
render: (row) => <span className="line-clamp-2">{row.module_name || "Platform"}</span>,
render: (row) => (
<span className="line-clamp-2">{row.module_name || "Platform"}</span>
),
},
{
key: "user_name",
label: "User",
render: (row) => <span className="line-clamp-2">{row.user_name || "—"}</span>,
render: (row) => (
<span className="line-clamp-2">{row.user_name || "—"}</span>
),
},
{
key: "provider",
label: "Provider",
render: (row) => row.provider || "—",
},
{ key: "provider", label: "Provider", render: (row) => row.provider || "—" },
{
key: "model",
label: "Model",
@ -271,7 +307,9 @@ const CompletionHistory = (): ReactElement => {
key: "status",
label: "Status",
render: (row) => (
<StatusBadge variant={row.status === "completed" ? "success" : "failure"}>
<StatusBadge
variant={row.status === "completed" ? "success" : "failure"}
>
{row.status || "unknown"}
</StatusBadge>
),
@ -304,7 +342,9 @@ const CompletionHistory = (): ReactElement => {
description:
"Track provider/model/token usage for persisted completions.",
action: (
<PrimaryButton onClick={() => navigate("/tenant/ai/completions/create")}>
<PrimaryButton
onClick={() => navigate("/tenant/ai/completions/create")}
>
Create Completion
</PrimaryButton>
),
@ -327,7 +367,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All providers"
onChange={(value) => {
setPage(1);
setFilters((prev) => ({ ...prev, provider: value as string | null }));
setFilters((prev) => ({
...prev,
provider: value as string | null,
}));
}}
/>
<FilterDropdown
@ -337,7 +380,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All models"
onChange={(value) => {
setPage(1);
setFilters((prev) => ({ ...prev, model: value as string | null }));
setFilters((prev) => ({
...prev,
model: value as string | null,
}));
}}
/>
<FilterDropdown
@ -351,7 +397,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All statuses"
onChange={(value) => {
setPage(1);
setFilters((prev) => ({ ...prev, status: value as string | null }));
setFilters((prev) => ({
...prev,
status: value as string | null,
}));
}}
/>
@ -366,7 +415,10 @@ const CompletionHistory = (): ReactElement => {
)}
style={
showMoreFilters || hasExtraFilters
? { borderColor: `${primaryColor}55`, color: primaryColor }
? {
borderColor: `${primaryColor}55`,
color: primaryColor,
}
: undefined
}
>
@ -400,7 +452,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All modules"
onChange={(value) => {
setPage(1);
setFilters((prev) => ({ ...prev, moduleId: value as string | null }));
setFilters((prev) => ({
...prev,
moduleId: value as string | null,
}));
}}
/>
<FilterDropdown
@ -411,31 +466,44 @@ const CompletionHistory = (): ReactElement => {
isSearchable
onChange={(value) => {
setPage(1);
setFilters((prev) => ({ ...prev, userId: value as string | null }));
setFilters((prev) => ({
...prev,
userId: value as string | null,
}));
}}
/>
</div>
<div className="flex flex-wrap items-center gap-4 md:gap-6">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">From:</span>
<span className="text-xs font-medium text-[#475569]">
From:
</span>
<input
type="date"
value={filters.startDate}
onChange={(e) => {
setPage(1);
setFilters((prev) => ({ ...prev, startDate: e.target.value }));
setFilters((prev) => ({
...prev,
startDate: e.target.value,
}));
}}
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
/>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#475569]">To:</span>
<span className="text-xs font-medium text-[#475569]">
To:
</span>
<input
type="date"
value={filters.endDate}
onChange={(e) => {
setPage(1);
setFilters((prev) => ({ ...prev, endDate: e.target.value }));
setFilters((prev) => ({
...prev,
endDate: e.target.value,
}));
}}
className="text-xs px-2 py-1.5 border border-[rgba(0,0,0,0.08)] rounded focus:outline-none focus:ring-1 focus:ring-[#112868]/20"
/>
@ -446,7 +514,9 @@ const CompletionHistory = (): ReactElement => {
</div>
{isMetaLoading && (
<p className="text-xs text-[#64748b] mt-2">Loading filter options</p>
<p className="text-xs text-[#64748b] mt-2">
Loading filter options
</p>
)}
</div>

View File

@ -565,14 +565,14 @@ const DocumentCategories = (): ReactElement => {
/>
</div>
</div>
<div className="flex justify-end pt-4 border-t border-gray-100">
{/* <div className="flex justify-end pt-4 border-t border-gray-100">
<button
onClick={() => setIsViewModalOpen(false)}
className="px-6 py-2 bg-[#0f1724] rounded-md text-sm font-bold text-white hover:bg-black transition-colors"
>
Close
</button>
</div>
</div> */}
</div>
</Modal>

View File

@ -160,6 +160,10 @@ const PromptCreate = (): ReactElement => {
return (
<Layout
currentPage="Create Prompt"
breadcrumbs={[
{ label: "AI Prompts", path: "/tenant/ai/prompts" },
{ label: "Create Prompt" },
]}
pageHeader={{
title: (
<div className="flex items-center gap-3">

View File

@ -10,7 +10,7 @@ import {
FormSlider,
FormTagInput
} from "@/components/shared";
import { Plus, Trash2, ArrowLeft } from "lucide-react";
import { Plus, Trash2 } from "lucide-react";
import { aiService } from "@/services/ai-service";
import { showToast } from "@/utils/toast";
import { useForm, useFieldArray, Controller } from "react-hook-form";
@ -201,6 +201,10 @@ const PromptEdit = (): ReactElement => {
return (
<Layout
currentPage="Edit Prompt"
breadcrumbs={[
{ label: "AI Prompts", path: "/tenant/ai/prompts" },
{ label: "Edit Prompt" },
]}
pageHeader={{
title: "Edit Prompt",
description: "Modify prompt template and configuration settings.",
@ -216,16 +220,6 @@ const PromptEdit = (): ReactElement => {
),
}}
>
<div className="mb-4">
<button
onClick={() => navigate("/tenant/ai/prompts")}
className="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-900 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to list
</button>
</div>
<form
onSubmit={handleSubmit(onFormSubmit)}
className="flex items-start gap-6"

View File

@ -229,7 +229,7 @@ export const PromptTestCaseCreate = (): ReactElement => {
</div>
{/* Input Variables Section */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm p-5 overflow-hidden select-none">
<div className="p-4 bg-white flex items-center justify-between border-b border-slate-100">
<h2 className="text-base font-semibold text-slate-800 select-none">
Input Variables
@ -248,7 +248,7 @@ export const PromptTestCaseCreate = (): ReactElement => {
{fields.map((field, index) => (
<div
key={field.id}
className="flex items-start gap-4 pb-1 last:pb-0"
className="flex items-start gap-4 pb-1 last:pb-0 border-b"
>
<div className="flex-1 max-w-[32%] h-11 flex items-center">
<span className="text-sm font-semibold text-slate-700">

View File

@ -3,7 +3,6 @@ import { useNavigate, useSearchParams, useParams } from "react-router-dom";
import {
Plus,
Play,
ArrowLeft,
Eye,
Loader2,
} from "lucide-react";
@ -228,6 +227,10 @@ const PromptTestCases = (): ReactElement => {
return (
<Layout
currentPage="Prompt Management"
breadcrumbs={[
{ label: "AI Prompts", path: "/tenant/ai/prompts" },
{ label: "Prompt Test Cases" },
]}
pageHeader={{
title: "Prompt Test Cases",
description: "Manage and execute test cases for your prompt templates.",
@ -243,16 +246,6 @@ const PromptTestCases = (): ReactElement => {
}}
>
<div className="flex flex-col gap-5">
{/* Back navigation link */}
<div className="flex items-center">
<button
onClick={() => navigate("/tenant/ai/prompts")}
className="flex items-center gap-1 text-xs text-[#64748b] hover:text-[#112868] transition-colors cursor-pointer font-medium select-none"
>
<ArrowLeft className="w-3.5 h-3.5" />
Back to Prompts
</button>
</div>
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
<DataTable
@ -268,7 +261,7 @@ const PromptTestCases = (): ReactElement => {
showExpandColumn={true}
expandedColSpan={columns.length + 1}
renderExpandedRow={(item: any) => (
<div className="bg-slate-50/70 border border-slate-200/50 p-4 rounded-xl text-xs text-slate-800 select-all flex flex-col gap-4">
<div className="flex flex-col gap-4 w-full">
<div>
<span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none">
Input Variables

View File

@ -24,6 +24,7 @@ import {
Modal,
FormField,
PrimaryButton,
GradientStatCard,
// SecondaryButton,
} from "@/components/shared";
import { useAppTheme } from "@/hooks/useAppTheme";
@ -199,7 +200,6 @@ const StorageDashboard = (): ReactElement => {
return (
<Layout
currentPage="Storage Dashboard"
// breadcrumbs={[{ label: "File Attachments" }, { label: "Storage Dashboard" }]}
pageHeader={{
title: "Storage Dashboard",
description:
@ -247,76 +247,27 @@ const StorageDashboard = (): ReactElement => {
<div className="space-y-8 animate-in fade-in duration-300">
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white p-5 rounded-xl border border-[rgba(0,0,0,0.08)] shadow-sm">
<div className="flex items-center gap-3 mb-2">
<div
className="p-2 rounded-lg"
style={{ backgroundColor: `${primaryColor}10` }}
>
<HardDrive
className="w-4 h-4"
style={{ color: primaryColor }}
/>
</div>
<span className="text-xs font-bold text-[#9aa6b2] uppercase">
Usage
</span>
</div>
<p className="text-xl font-black text-[#0e1b2a]">
{stats.quota.usage_percent}%{" "}
<span className="text-[10px] text-[#9aa6b2] font-medium uppercase">
capacity
</span>
</p>
<div className="mt-2 w-full h-1.5 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full"
style={{
width: `${stats.quota.usage_percent}%`,
backgroundColor: primaryColor,
}}
/>
</div>
</div>
<div className="bg-white p-5 rounded-xl border border-[rgba(0,0,0,0.08)] shadow-sm">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-emerald-50 rounded-lg">
<Files className="w-4 h-4 text-emerald-600" />
</div>
<span className="text-xs font-bold text-[#9aa6b2] uppercase">
Total Files
</span>
</div>
<p className="text-xl font-black text-[#0e1b2a]">
{stats.files.total}
</p>
</div>
<div className="bg-white p-5 rounded-xl border border-[rgba(0,0,0,0.08)] shadow-sm">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-orange-50 rounded-lg">
<ImageIcon className="w-4 h-4 text-orange-500" />
</div>
<span className="text-xs font-bold text-[#9aa6b2] uppercase">
Images
</span>
</div>
<p className="text-xl font-black text-[#0e1b2a]">
{stats.files.images}
</p>
</div>
<div className="bg-white p-5 rounded-xl border border-[rgba(0,0,0,0.08)] shadow-sm">
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-red-50 rounded-lg">
<FileText className="w-4 h-4 text-red-500" />
</div>
<span className="text-xs font-bold text-[#9aa6b2] uppercase">
DOCs / PDFs
</span>
</div>
<p className="text-xl font-black text-[#0e1b2a]">
{stats.files.pdfs + stats.files.documents}
</p>
</div>
<GradientStatCard
icon={HardDrive}
value={`${stats.quota.usage_percent}%`}
label="Storage Usage"
badge={{ text: "Capacity", variant: "info" }}
/>
<GradientStatCard
icon={Files}
value={stats.files.total}
label="Total Files"
/>
<GradientStatCard
icon={ImageIcon}
value={stats.files.images}
label="Images"
/>
<GradientStatCard
icon={FileText}
value={stats.files.pdfs + stats.files.documents}
label="DOCs / PDFs"
/>
</div>
{/* Tables Grid */}