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"> <nav className="flex items-center gap-1.5 md:gap-2">
{breadcrumbs && breadcrumbs.length > 0 ? ( {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) => ( {breadcrumbs.map((crumb, index) => (
<div key={index} className="flex items-center gap-1.5 md:gap-2"> <div key={index} className="flex items-center gap-1.5 md:gap-2">
{crumb.path ? ( {crumb.path ? (
@ -138,7 +149,7 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
) : ( ) : (
<> <>
<button <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" className="text-xs md:text-[13px] font-normal text-[#6b7280] hover:text-[#0f1724] transition-colors cursor-pointer"
> >
QAssure QAssure

View File

@ -1,12 +1,12 @@
import type { ReactElement, ReactNode } from 'react'; import type { ReactElement, ReactNode } from "react";
import { Fragment } from 'react'; import { Fragment } from "react";
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from "lucide-react";
export interface Column<T> { export interface Column<T> {
key: string; key: string;
label: string; label: string;
render?: (item: T) => ReactNode; render?: (item: T) => ReactNode;
align?: 'left' | 'right' | 'center'; align?: "left" | "right" | "center";
mobileLabel?: string; mobileLabel?: string;
} }
@ -32,7 +32,7 @@ export const DataTable = <T,>({
columns, columns,
keyExtractor, keyExtractor,
mobileCardRenderer, mobileCardRenderer,
emptyMessage = 'No data found', emptyMessage = "No data found",
isLoading = false, isLoading = false,
error = null, error = null,
expandableRows = false, expandableRows = false,
@ -43,8 +43,13 @@ export const DataTable = <T,>({
showExpandColumn = true, showExpandColumn = true,
expandedColSpan, expandedColSpan,
}: DataTableProps<T>): ReactElement => { }: DataTableProps<T>): ReactElement => {
const canExpand = expandableRows && !!onRowExpandToggle && !!isRowExpanded && !!renderExpandedRow; const canExpand =
const desktopColSpan = expandedColSpan || columns.length + (canExpand && showExpandColumn ? 1 : 0); expandableRows &&
!!onRowExpandToggle &&
!!isRowExpanded &&
!!renderExpandedRow;
const desktopColSpan =
expandedColSpan || columns.length + (canExpand && showExpandColumn ? 1 : 0);
// Loading State // Loading State
if (isLoading) { if (isLoading) {
@ -75,15 +80,18 @@ export const DataTable = <T,>({
<thead> <thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> <tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
{canExpand && showExpandColumn && ( {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) => { {columns.map((column) => {
const alignClass = const alignClass =
column.align === 'right' column.align === "right"
? 'text-right' ? "text-right"
: column.align === 'center' : column.align === "center"
? 'text-center' ? "text-center"
: 'text-left'; : "text-left";
return ( return (
<th <th
key={column.key} key={column.key}
@ -97,7 +105,10 @@ export const DataTable = <T,>({
</thead> </thead>
<tbody> <tbody>
<tr> <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} {emptyMessage}
</td> </td>
</tr> </tr>
@ -122,23 +133,26 @@ export const DataTable = <T,>({
<thead> <thead>
<tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]"> <tr className="bg-[#f5f7fa] border-b border-[rgba(0,0,0,0.08)]">
{canExpand && showExpandColumn && ( {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) => { {columns.map((column) => {
const alignClass = const alignClass =
column.align === 'right' column.align === "right"
? 'text-right' ? "text-right"
: column.align === 'center' : column.align === "center"
? 'text-center' ? "text-center"
: 'text-left'; : "text-left";
return ( return (
<th <th
key={column.key} 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`} 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} {column.label}
</th> </th>
); );
})} })}
</tr> </tr>
</thead> </thead>
@ -163,28 +177,37 @@ export const DataTable = <T,>({
onRowExpandToggle(item); 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> </button>
</td> </td>
)} )}
{columns.map((column) => { {columns.map((column) => {
const alignClass = const alignClass =
column.align === 'right' column.align === "right"
? 'text-right' ? "text-right"
: column.align === 'center' : column.align === "center"
? 'text-center' ? "text-center"
: 'text-left'; : "text-left";
return ( 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]`}> <td
{column.render ? column.render(item) : String((item as any)[column.key])} 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> </td>
); );
})} })}
</tr> </tr>
{canExpand && expanded && ( {canExpand && expanded && (
<tr className="border-b border-[rgba(0,0,0,0.08)] bg-white"> <tr className="border-t border-[rgba(0,0,0,0.08)] bg-[#F9F9F9]">
<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"> <td colSpan={desktopColSpan}>
{renderExpandedRow(item)} <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> </td>
</tr> </tr>
)} )}
@ -199,7 +222,9 @@ export const DataTable = <T,>({
{/* Mobile Card View */} {/* Mobile Card View */}
<div className="md:hidden divide-y divide-[rgba(0,0,0,0.08)]"> <div className="md:hidden divide-y divide-[rgba(0,0,0,0.08)]">
{mobileCardRenderer {mobileCardRenderer
? data.map((item) => <div key={keyExtractor(item)}>{mobileCardRenderer(item)}</div>) ? data.map((item) => (
<div key={keyExtractor(item)}>{mobileCardRenderer(item)}</div>
))
: data.map((item) => { : data.map((item) => {
const expanded = canExpand ? !!isRowExpanded(item) : false; const expanded = canExpand ? !!isRowExpanded(item) : false;
return ( return (
@ -212,7 +237,9 @@ export const DataTable = <T,>({
aria-expanded={expanded} aria-expanded={expanded}
onClick={() => onRowExpandToggle(item)} 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> </button>
</div> </div>
)} )}
@ -222,14 +249,14 @@ export const DataTable = <T,>({
{column.mobileLabel || column.label}: {column.mobileLabel || column.label}:
</span> </span>
<div className="text-xs sm:text-sm text-[#0f1724]"> <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>
</div> </div>
))} ))}
{canExpand && expanded && renderExpandedRow && ( {canExpand && expanded && renderExpandedRow && (
<div className="mt-3"> <div className="mt-3">{renderExpandedRow(item)}</div>
{renderExpandedRow(item)}
</div>
)} )}
</div> </div>
); );

View File

@ -133,7 +133,7 @@ export const PromptTestCaseResultsListModal = ({
showExpandColumn={true} showExpandColumn={true}
expandedColSpan={columns.length + 1} expandedColSpan={columns.length + 1}
renderExpandedRow={(item) => ( 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"> <span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none">
Output Output
</span> </span>
@ -142,7 +142,7 @@ export const PromptTestCaseResultsListModal = ({
) : ( ) : (
"No output generated." "No output generated."
)} )}
</div> </>
)} )}
/> />
</div> </div>

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import { useEffect, useState, type ReactElement, type ReactNode } from "react"; import { useEffect, useState, type ReactElement, type ReactNode } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft } from "lucide-react";
import { Layout } from "@/components/layout/Layout"; 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 { aiService } from "@/services/ai-service";
import type { AICompletion } from "@/types/ai"; import type { AICompletion } from "@/types/ai";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
@ -57,16 +56,14 @@ const CompletionDetail = (): ReactElement => {
return ( return (
<Layout <Layout
currentPage="Completion History" currentPage="Completion Details"
breadcrumbs={[
{ label: "Completions", path: "/tenant/ai/completions" },
{ label: "Completion details" },
]}
pageHeader={{ pageHeader={{
title: "Completion details", title: "Completion details",
description: "Full request, response, and usage for this completion.", 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"> <div className="space-y-5">
@ -165,15 +162,6 @@ const CompletionDetail = (): ReactElement => {
</pre> </pre>
</section> </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> </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 { useNavigate } from "react-router-dom";
import { ChevronDown, SlidersHorizontal } from "lucide-react"; import { ChevronDown, SlidersHorizontal } from "lucide-react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
@ -12,7 +18,10 @@ import {
} from "@/components/shared"; } from "@/components/shared";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import { moduleService } from "@/services/module-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 { AICompletion, AIProviderInfo } from "@/types/ai";
import type { MyModule } from "@/types/module"; import type { MyModule } from "@/types/module";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
@ -37,7 +46,9 @@ const CompletionHistory = (): ReactElement => {
const [isMetaLoading, setIsMetaLoading] = useState<boolean>(true); const [isMetaLoading, setIsMetaLoading] = useState<boolean>(true);
const [providers, setProviders] = useState<AIProviderInfo[]>([]); 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 [modules, setModules] = useState<MyModule[]>([]);
const [tenantUsers, setTenantUsers] = useState<TenantUserDropdownItem[]>([]); const [tenantUsers, setTenantUsers] = useState<TenantUserDropdownItem[]>([]);
@ -67,7 +78,8 @@ const CompletionHistory = (): ReactElement => {
}); });
const providerOptions = useMemo( 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], [providers],
); );
@ -107,20 +119,21 @@ const CompletionHistory = (): ReactElement => {
const loadMeta = useCallback(async (): Promise<void> => { const loadMeta = useCallback(async (): Promise<void> => {
setIsMetaLoading(true); setIsMetaLoading(true);
try { try {
const [providerData, modelData, modulesRes, usersData] = await Promise.all([ const [providerData, modelData, modulesRes, usersData] =
aiService.getProviders(), await Promise.all([
aiService.getModels(), aiService.getProviders(),
moduleService.getMyModules(), aiService.getModels(),
tenantService.getCurrentTenantUsersDropdown(), moduleService.getMyModules(),
]); tenantService.getCurrentTenantUsersDropdown(),
]);
setProviders(providerData); setProviders(providerData);
setModels(modelData); setModels(modelData);
setModules(modulesRes.data || []); setModules(modulesRes.data || []);
setTenantUsers(usersData); setTenantUsers(usersData);
} catch (err: unknown) { } catch (err: unknown) {
const msg = const msg =
(err as { response?: { data?: { error?: { message?: string } } } })?.response?.data?.error (err as { response?: { data?: { error?: { message?: string } } } })
?.message || "Failed to load filter options"; ?.response?.data?.error?.message || "Failed to load filter options";
showToast.error(msg); showToast.error(msg);
} finally { } finally {
setIsMetaLoading(false); setIsMetaLoading(false);
@ -152,8 +165,9 @@ const CompletionHistory = (): ReactElement => {
}); });
} catch (err: unknown) { } catch (err: unknown) {
const msg = const msg =
(err as { response?: { data?: { error?: { message?: string } } } })?.response?.data?.error (err as { response?: { data?: { error?: { message?: string } } } })
?.message || "Failed to load completion history"; ?.response?.data?.error?.message ||
"Failed to load completion history";
setError(msg); setError(msg);
showToast.error(msg); showToast.error(msg);
} finally { } finally {
@ -199,10 +213,13 @@ const CompletionHistory = (): ReactElement => {
const renderExpanded = (row: AICompletion): ReactElement => { const renderExpanded = (row: AICompletion): ReactElement => {
const total = 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); const preview = (row.response || row.content || "").slice(0, 800);
return ( 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> {/* <p>
<span className="font-semibold text-[#475569]">IDs </span> <span className="font-semibold text-[#475569]">IDs </span>
Module: <code className="text-[11px]">{row.module_id || "—"}</code> Module: <code className="text-[11px]">{row.module_id || "—"}</code>
@ -214,7 +231,9 @@ const CompletionHistory = (): ReactElement => {
{row.correlation_id || "—"} {row.correlation_id || "—"}
</p> */} </p> */}
<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`} {`${total} tokens · USD ${Number(row.cost ?? 0).toFixed(6)} · ${row.latency_ms ?? 0} ms`}
</p> </p>
{row.use_case && ( {row.use_case && (
@ -230,8 +249,12 @@ const CompletionHistory = (): ReactElement => {
</p> </p>
)} )}
<div> <div>
<span className="font-semibold text-[#475569] block mb-1">Response preview</span> <span className="font-semibold text-[#475569] block mb-1">
<p className="whitespace-pre-wrap break-words leading-relaxed">{preview || "—"}</p> Response preview
</span>
<p className="whitespace-pre-wrap break-words leading-relaxed">
{preview || "—"}
</p>
</div> </div>
<button <button
type="button" type="button"
@ -241,7 +264,8 @@ const CompletionHistory = (): ReactElement => {
> >
Open full detail view Open full detail view
</button> </button>
</div> </>
// </div>
); );
}; };
@ -249,19 +273,31 @@ const CompletionHistory = (): ReactElement => {
{ {
key: "date", key: "date",
label: "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", key: "module_name",
label: "Module", 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", key: "user_name",
label: "User", 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", key: "model",
label: "Model", label: "Model",
@ -271,7 +307,9 @@ const CompletionHistory = (): ReactElement => {
key: "status", key: "status",
label: "Status", label: "Status",
render: (row) => ( render: (row) => (
<StatusBadge variant={row.status === "completed" ? "success" : "failure"}> <StatusBadge
variant={row.status === "completed" ? "success" : "failure"}
>
{row.status || "unknown"} {row.status || "unknown"}
</StatusBadge> </StatusBadge>
), ),
@ -304,7 +342,9 @@ const CompletionHistory = (): ReactElement => {
description: description:
"Track provider/model/token usage for persisted completions.", "Track provider/model/token usage for persisted completions.",
action: ( action: (
<PrimaryButton onClick={() => navigate("/tenant/ai/completions/create")}> <PrimaryButton
onClick={() => navigate("/tenant/ai/completions/create")}
>
Create Completion Create Completion
</PrimaryButton> </PrimaryButton>
), ),
@ -327,7 +367,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All providers" placeholder="All providers"
onChange={(value) => { onChange={(value) => {
setPage(1); setPage(1);
setFilters((prev) => ({ ...prev, provider: value as string | null })); setFilters((prev) => ({
...prev,
provider: value as string | null,
}));
}} }}
/> />
<FilterDropdown <FilterDropdown
@ -337,7 +380,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All models" placeholder="All models"
onChange={(value) => { onChange={(value) => {
setPage(1); setPage(1);
setFilters((prev) => ({ ...prev, model: value as string | null })); setFilters((prev) => ({
...prev,
model: value as string | null,
}));
}} }}
/> />
<FilterDropdown <FilterDropdown
@ -351,7 +397,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All statuses" placeholder="All statuses"
onChange={(value) => { onChange={(value) => {
setPage(1); 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={ style={
showMoreFilters || hasExtraFilters showMoreFilters || hasExtraFilters
? { borderColor: `${primaryColor}55`, color: primaryColor } ? {
borderColor: `${primaryColor}55`,
color: primaryColor,
}
: undefined : undefined
} }
> >
@ -400,7 +452,10 @@ const CompletionHistory = (): ReactElement => {
placeholder="All modules" placeholder="All modules"
onChange={(value) => { onChange={(value) => {
setPage(1); setPage(1);
setFilters((prev) => ({ ...prev, moduleId: value as string | null })); setFilters((prev) => ({
...prev,
moduleId: value as string | null,
}));
}} }}
/> />
<FilterDropdown <FilterDropdown
@ -411,31 +466,44 @@ const CompletionHistory = (): ReactElement => {
isSearchable isSearchable
onChange={(value) => { onChange={(value) => {
setPage(1); setPage(1);
setFilters((prev) => ({ ...prev, userId: value as string | null })); setFilters((prev) => ({
...prev,
userId: value as string | null,
}));
}} }}
/> />
</div> </div>
<div className="flex flex-wrap items-center gap-4 md:gap-6"> <div className="flex flex-wrap items-center gap-4 md:gap-6">
<div className="flex items-center gap-2"> <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 <input
type="date" type="date"
value={filters.startDate} value={filters.startDate}
onChange={(e) => { onChange={(e) => {
setPage(1); 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" 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>
<div className="flex items-center gap-2"> <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 <input
type="date" type="date"
value={filters.endDate} value={filters.endDate}
onChange={(e) => { onChange={(e) => {
setPage(1); 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" 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> </div>
{isMetaLoading && ( {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> </div>

View File

@ -565,14 +565,14 @@ const DocumentCategories = (): ReactElement => {
/> />
</div> </div>
</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 <button
onClick={() => setIsViewModalOpen(false)} onClick={() => setIsViewModalOpen(false)}
className="px-6 py-2 bg-[#0f1724] rounded-md text-sm font-bold text-white hover:bg-black transition-colors" className="px-6 py-2 bg-[#0f1724] rounded-md text-sm font-bold text-white hover:bg-black transition-colors"
> >
Close Close
</button> </button>
</div> </div> */}
</div> </div>
</Modal> </Modal>

View File

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

View File

@ -10,7 +10,7 @@ import {
FormSlider, FormSlider,
FormTagInput FormTagInput
} from "@/components/shared"; } from "@/components/shared";
import { Plus, Trash2, ArrowLeft } from "lucide-react"; import { Plus, Trash2 } from "lucide-react";
import { aiService } from "@/services/ai-service"; import { aiService } from "@/services/ai-service";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { useForm, useFieldArray, Controller } from "react-hook-form"; import { useForm, useFieldArray, Controller } from "react-hook-form";
@ -201,6 +201,10 @@ const PromptEdit = (): ReactElement => {
return ( return (
<Layout <Layout
currentPage="Edit Prompt" currentPage="Edit Prompt"
breadcrumbs={[
{ label: "AI Prompts", path: "/tenant/ai/prompts" },
{ label: "Edit Prompt" },
]}
pageHeader={{ pageHeader={{
title: "Edit Prompt", title: "Edit Prompt",
description: "Modify prompt template and configuration settings.", 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 <form
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
className="flex items-start gap-6" className="flex items-start gap-6"

View File

@ -229,7 +229,7 @@ export const PromptTestCaseCreate = (): ReactElement => {
</div> </div>
{/* Input Variables Section */} {/* 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"> <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"> <h2 className="text-base font-semibold text-slate-800 select-none">
Input Variables Input Variables
@ -248,7 +248,7 @@ export const PromptTestCaseCreate = (): ReactElement => {
{fields.map((field, index) => ( {fields.map((field, index) => (
<div <div
key={field.id} 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"> <div className="flex-1 max-w-[32%] h-11 flex items-center">
<span className="text-sm font-semibold text-slate-700"> <span className="text-sm font-semibold text-slate-700">

View File

@ -3,7 +3,6 @@ import { useNavigate, useSearchParams, useParams } from "react-router-dom";
import { import {
Plus, Plus,
Play, Play,
ArrowLeft,
Eye, Eye,
Loader2, Loader2,
} from "lucide-react"; } from "lucide-react";
@ -228,6 +227,10 @@ const PromptTestCases = (): ReactElement => {
return ( return (
<Layout <Layout
currentPage="Prompt Management" currentPage="Prompt Management"
breadcrumbs={[
{ label: "AI Prompts", path: "/tenant/ai/prompts" },
{ label: "Prompt Test Cases" },
]}
pageHeader={{ pageHeader={{
title: "Prompt Test Cases", title: "Prompt Test Cases",
description: "Manage and execute test cases for your prompt templates.", description: "Manage and execute test cases for your prompt templates.",
@ -243,16 +246,6 @@ const PromptTestCases = (): ReactElement => {
}} }}
> >
<div className="flex flex-col gap-5"> <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"> <div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden select-none">
<DataTable <DataTable
@ -268,7 +261,7 @@ const PromptTestCases = (): ReactElement => {
showExpandColumn={true} showExpandColumn={true}
expandedColSpan={columns.length + 1} expandedColSpan={columns.length + 1}
renderExpandedRow={(item: any) => ( 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> <div>
<span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none"> <span className="text-xs font-bold text-slate-600 uppercase tracking-wide mb-2 block select-none">
Input Variables Input Variables

View File

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