refactor: rework audit log to full-width table with filter bar
All checks were successful
CI / build (push) Successful in 1m12s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 54s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 36s
CI / build (pull_request) Successful in 1m10s
CI / cleanup-branch (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
All checks were successful
CI / build (push) Successful in 1m12s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 54s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 36s
CI / build (pull_request) Successful in 1m10s
CI / cleanup-branch (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
Replace split-pane layout with a table-based design: horizontal filter bar, full-width data table with sticky headers, expandable detail rows showing IP/user-agent/JSON payload, and bottom pagination. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,40 +1,24 @@
|
|||||||
/* ─── Filter Toggle ─── */
|
/* ─── Filter Bar ─── */
|
||||||
.filterToggle {
|
.filterBar {
|
||||||
width: 100%;
|
|
||||||
padding: 8px 20px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-muted);
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
|
||||||
font-family: var(--font-body);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filterToggle:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: var(--bg-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Filters ─── */
|
|
||||||
.filters {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: flex-end;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
flex-shrink: 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
.filtersCollapsed {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterGroup {
|
.filterGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterGroupGrow {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterLabel {
|
.filterLabel {
|
||||||
@@ -49,12 +33,12 @@
|
|||||||
background: var(--bg-base);
|
background: var(--bg-base);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 5px 8px;
|
padding: 6px 10px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterInput:focus {
|
.filterInput:focus {
|
||||||
@@ -69,41 +53,98 @@
|
|||||||
background: var(--bg-base);
|
background: var(--bg-base);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
padding: 5px 8px;
|
padding: 6px 10px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
|
font-family: var(--font-body);
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-body);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Event Row Styles ─── */
|
/* ─── Table Area ─── */
|
||||||
.eventTimestamp {
|
.tableArea {
|
||||||
font-family: var(--font-mono);
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
text-align: left;
|
||||||
|
padding: 10px 14px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventAction {
|
.thTimestamp {
|
||||||
color: var(--text-muted);
|
width: 170px;
|
||||||
font-size: 11px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventCompact {
|
.thResult {
|
||||||
display: flex;
|
width: 90px;
|
||||||
align-items: center;
|
}
|
||||||
gap: 6px;
|
|
||||||
font-size: 12px;
|
.table td {
|
||||||
|
padding: 8px 14px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-top: 1px;
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Event Rows ─── */
|
||||||
|
.eventRow {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventRow:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eventRowExpanded {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellTimestamp {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellUser {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellTarget {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
max-width: 220px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Badges ─── */
|
/* ─── Badges ─── */
|
||||||
.categoryBadge {
|
.categoryBadge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 99px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -116,23 +157,64 @@
|
|||||||
.resultBadge {
|
.resultBadge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 99px;
|
border-radius: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultSuccess {
|
.resultSuccess {
|
||||||
background: rgba(34, 197, 94, 0.1);
|
background: rgba(16, 185, 129, 0.12);
|
||||||
color: #22c55e;
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.resultFailure {
|
.resultFailure {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(244, 63, 94, 0.12);
|
||||||
color: #ef4444;
|
color: var(--rose);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Expanded Detail Row ─── */
|
||||||
|
.detailRow td {
|
||||||
|
padding: 0 14px 14px;
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailMeta {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailField {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailLabel {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailValue {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Detail JSON ─── */
|
|
||||||
.detailJson {
|
.detailJson {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -147,9 +229,64 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Mono ─── */
|
/* ─── Pagination ─── */
|
||||||
.mono {
|
.pagination {
|
||||||
font-family: var(--font-mono);
|
display: flex;
|
||||||
font-size: 11px;
|
align-items: center;
|
||||||
white-space: nowrap;
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn {
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-raised);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-body);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn:hover:not(:disabled) {
|
||||||
|
border-color: var(--amber-dim);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageBtn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageInfo {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Empty State ─── */
|
||||||
|
.emptyState {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.filterBar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterGroupGrow {
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellTarget {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ function AuditLogContent() {
|
|||||||
const [category, setCategory] = useState('');
|
const [category, setCategory] = useState('');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [selectedEventId, setSelectedEventId] = useState<number | null>(null);
|
const [expandedRow, setExpandedRow] = useState<number | null>(null);
|
||||||
const [filtersVisible, setFiltersVisible] = useState(true);
|
|
||||||
const pageSize = 25;
|
const pageSize = 25;
|
||||||
|
|
||||||
const params: AuditLogParams = {
|
const params: AuditLogParams = {
|
||||||
@@ -57,36 +56,22 @@ function AuditLogContent() {
|
|||||||
const showingFrom = data && data.totalCount > 0 ? page * pageSize + 1 : 0;
|
const showingFrom = data && data.totalCount > 0 ? page * pageSize + 1 : 0;
|
||||||
const showingTo = data ? Math.min((page + 1) * pageSize, data.totalCount) : 0;
|
const showingTo = data ? Math.min((page + 1) * pageSize, data.totalCount) : 0;
|
||||||
|
|
||||||
const selectedEvent = data?.items.find((e) => e.id === selectedEventId) ?? null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={layout.page}>
|
<div className={layout.page}>
|
||||||
|
{/* Header */}
|
||||||
<div className={layout.panelHeader}>
|
<div className={layout.panelHeader}>
|
||||||
<div>
|
<div>
|
||||||
<div className={layout.panelTitle}>Audit Log</div>
|
<div className={layout.panelTitle}>Audit Log</div>
|
||||||
{data && (
|
|
||||||
<div className={layout.panelSubtitle}>
|
<div className={layout.panelSubtitle}>
|
||||||
{data.totalCount.toLocaleString()} events
|
{data
|
||||||
|
? `${data.totalCount.toLocaleString()} events`
|
||||||
|
: 'Loading...'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={layout.split}>
|
{/* Filter bar */}
|
||||||
{/* Left pane — event list */}
|
<div className={styles.filterBar}>
|
||||||
<div className={layout.listPane}>
|
|
||||||
{/* Collapsible filter bar */}
|
|
||||||
<div style={{ flexShrink: 0 }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.filterToggle}
|
|
||||||
onClick={() => setFiltersVisible((v) => !v)}
|
|
||||||
>
|
|
||||||
{filtersVisible ? 'Hide Filters' : 'Show Filters'}
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
className={`${styles.filters} ${!filtersVisible ? styles.filtersCollapsed : ''}`}
|
|
||||||
>
|
|
||||||
<div className={styles.filterGroup}>
|
<div className={styles.filterGroup}>
|
||||||
<label className={styles.filterLabel}>From</label>
|
<label className={styles.filterLabel}>From</label>
|
||||||
<input
|
<input
|
||||||
@@ -129,76 +114,71 @@ function AuditLogContent() {
|
|||||||
<option value="CONFIG">CONFIG</option>
|
<option value="CONFIG">CONFIG</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.filterGroup}>
|
<div className={`${styles.filterGroup} ${styles.filterGroupGrow}`}>
|
||||||
<label className={styles.filterLabel}>Search</label>
|
<label className={styles.filterLabel}>Search</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={styles.filterInput}
|
className={styles.filterInput}
|
||||||
placeholder="Search..."
|
placeholder="Search actions, targets..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Event list */}
|
{/* Table area */}
|
||||||
<div className={layout.entityList}>
|
<div className={styles.tableArea}>
|
||||||
{audit.isLoading ? (
|
{audit.isLoading ? (
|
||||||
<div className={layout.loading}>Loading...</div>
|
<div className={layout.loading}>Loading...</div>
|
||||||
) : !data || data.items.length === 0 ? (
|
) : !data || data.items.length === 0 ? (
|
||||||
<div className={layout.loading}>No events found.</div>
|
<div className={styles.emptyState}>
|
||||||
|
No audit events found for the selected filters.
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.items.map((event) => (
|
<table className={styles.table}>
|
||||||
<div
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className={styles.thTimestamp}>Timestamp</th>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Category</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th className={styles.thResult}>Result</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.items.map((event) => (
|
||||||
|
<EventRow
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className={`${layout.entityItem} ${
|
event={event}
|
||||||
selectedEventId === event.id ? layout.entityItemSelected : ''
|
isExpanded={expandedRow === event.id}
|
||||||
}`}
|
onToggle={() =>
|
||||||
onClick={() => setSelectedEventId(event.id)}
|
setExpandedRow((prev) => (prev === event.id ? null : event.id))
|
||||||
>
|
}
|
||||||
<div className={layout.entityInfo}>
|
/>
|
||||||
<div className={styles.eventTimestamp}>
|
))}
|
||||||
{formatTimestamp(event.timestamp)}
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
<div className={styles.eventCompact}>
|
|
||||||
<span>{event.username}</span>
|
|
||||||
<span className={styles.eventAction}>{event.action}</span>
|
|
||||||
</div>
|
|
||||||
<div className={styles.eventCompact}>
|
|
||||||
<span
|
|
||||||
className={`${styles.resultBadge} ${
|
|
||||||
event.result === 'SUCCESS'
|
|
||||||
? styles.resultSuccess
|
|
||||||
: styles.resultFailure
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{event.result}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{data && data.totalCount > 0 && (
|
{data && data.totalCount > 0 && (
|
||||||
<div className={layout.pagination}>
|
<div className={styles.pagination}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={layout.pageBtn}
|
className={styles.pageBtn}
|
||||||
disabled={page === 0}
|
disabled={page === 0}
|
||||||
onClick={() => setPage((p) => p - 1)}
|
onClick={() => setPage((p) => p - 1)}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
<span className={layout.pageInfo}>
|
<span className={styles.pageInfo}>
|
||||||
{showingFrom}-{showingTo} of {data.totalCount.toLocaleString()}
|
{showingFrom}--{showingTo} of {data.totalCount.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={layout.pageBtn}
|
className={styles.pageBtn}
|
||||||
disabled={page >= totalPages - 1}
|
disabled={page >= totalPages - 1}
|
||||||
onClick={() => setPage((p) => p + 1)}
|
onClick={() => setPage((p) => p + 1)}
|
||||||
>
|
>
|
||||||
@@ -207,80 +187,76 @@ function AuditLogContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
{/* Right pane — detail view */}
|
function EventRow({
|
||||||
<div className={layout.detailPane}>
|
event,
|
||||||
{!selectedEvent ? (
|
isExpanded,
|
||||||
<div className={layout.detailEmpty}>
|
onToggle,
|
||||||
Select an event to view details
|
}: {
|
||||||
</div>
|
event: {
|
||||||
) : (
|
id: number;
|
||||||
|
timestamp: string;
|
||||||
|
username: string;
|
||||||
|
category: string;
|
||||||
|
action: string;
|
||||||
|
target: string;
|
||||||
|
result: string;
|
||||||
|
detail: Record<string, unknown>;
|
||||||
|
ipAddress: string;
|
||||||
|
userAgent: string;
|
||||||
|
};
|
||||||
|
isExpanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={layout.detailSection}>
|
<tr
|
||||||
<div className={layout.detailSectionTitle}>Event Info</div>
|
className={`${styles.eventRow} ${isExpanded ? styles.eventRowExpanded : ''}`}
|
||||||
<div className={layout.fieldRow}>
|
onClick={onToggle}
|
||||||
<span className={layout.fieldLabel}>Timestamp</span>
|
>
|
||||||
<span className={`${layout.fieldVal} ${styles.mono}`}>
|
<td className={styles.cellTimestamp}>{formatTimestamp(event.timestamp)}</td>
|
||||||
{formatTimestamp(selectedEvent.timestamp)}
|
<td className={styles.cellUser}>{event.username}</td>
|
||||||
</span>
|
<td>
|
||||||
</div>
|
<span className={styles.categoryBadge}>{event.category}</span>
|
||||||
<div className={layout.fieldRow}>
|
</td>
|
||||||
<span className={layout.fieldLabel}>User</span>
|
<td>{event.action}</td>
|
||||||
<span className={layout.fieldVal}>{selectedEvent.username}</span>
|
<td className={styles.cellTarget}>{event.target}</td>
|
||||||
</div>
|
<td>
|
||||||
<div className={layout.fieldRow}>
|
|
||||||
<span className={layout.fieldLabel}>Category</span>
|
|
||||||
<span className={layout.fieldVal}>
|
|
||||||
<span className={styles.categoryBadge}>{selectedEvent.category}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={layout.fieldRow}>
|
|
||||||
<span className={layout.fieldLabel}>Action</span>
|
|
||||||
<span className={layout.fieldVal}>{selectedEvent.action}</span>
|
|
||||||
</div>
|
|
||||||
<div className={layout.fieldRow}>
|
|
||||||
<span className={layout.fieldLabel}>Target</span>
|
|
||||||
<span className={`${layout.fieldVal} ${styles.mono}`}>
|
|
||||||
{selectedEvent.target}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={layout.fieldRow}>
|
|
||||||
<span className={layout.fieldLabel}>Result</span>
|
|
||||||
<span className={layout.fieldVal}>
|
|
||||||
<span
|
<span
|
||||||
className={`${styles.resultBadge} ${
|
className={`${styles.resultBadge} ${
|
||||||
selectedEvent.result === 'SUCCESS'
|
event.result === 'SUCCESS' ? styles.resultSuccess : styles.resultFailure
|
||||||
? styles.resultSuccess
|
|
||||||
: styles.resultFailure
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{selectedEvent.result}
|
{event.result}
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{isExpanded && (
|
||||||
|
<tr className={styles.detailRow}>
|
||||||
|
<td colSpan={6}>
|
||||||
|
<div className={styles.detailContent}>
|
||||||
|
<div className={styles.detailMeta}>
|
||||||
|
<div className={styles.detailField}>
|
||||||
|
<span className={styles.detailLabel}>IP Address</span>
|
||||||
|
<span className={styles.detailValue}>{event.ipAddress}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={layout.fieldRow}>
|
<div className={styles.detailField}>
|
||||||
<span className={layout.fieldLabel}>IP Address</span>
|
<span className={styles.detailLabel}>User Agent</span>
|
||||||
<span className={`${layout.fieldVal} ${styles.mono}`}>
|
<span className={styles.detailValue}>{event.userAgent}</span>
|
||||||
{selectedEvent.ipAddress}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={layout.fieldRow}>
|
|
||||||
<span className={layout.fieldLabel}>User Agent</span>
|
|
||||||
<span className={layout.fieldVal}>{selectedEvent.userAgent}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{event.detail && Object.keys(event.detail).length > 0 && (
|
||||||
<div className={layout.detailSection}>
|
|
||||||
<div className={layout.detailSectionTitle}>Detail Payload</div>
|
|
||||||
<pre className={styles.detailJson}>
|
<pre className={styles.detailJson}>
|
||||||
{JSON.stringify(selectedEvent.detail, null, 2)}
|
{JSON.stringify(event.detail, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user