refactor: implement breadcrumb navigation and modernize layout components across tenant pages
This commit is contained in:
parent
af7e83c59c
commit
4024711100
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user