Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7fd0031
Frontend: audit log
ciur Aug 22, 2025
29ce75a
Frontend: add basic components
ciur Aug 22, 2025
eaa3098
add endpoints, apislice, types
ciur Aug 22, 2025
f039968
data table component - first draft
ciur Aug 23, 2025
b9891da
usage of new data table
ciur Aug 23, 2025
05d8727
wip
ciur Aug 23, 2025
1ad6e39
split components in separate files
ciur Aug 23, 2025
1c79b88
OKish initial version of data table
ciur Aug 23, 2025
eb171e7
Filter component
ciur Aug 23, 2025
29b62ab
OKish UI
ciur Aug 23, 2025
300241e
fix theme dark/light mode change
ciur Aug 23, 2025
9e97095
OKish UI
ciur Aug 23, 2025
f1b395e
OKish UI
ciur Aug 23, 2025
95c4f7d
rebase and minor adjustment in package.json
ciur Aug 23, 2025
2566c09
filter works for operation and table_name filters
ciur Aug 24, 2025
f55c2c9
It works!
ciur Aug 24, 2025
13bf409
minor adjustments
ciur Aug 24, 2025
ca74c76
sorted table names
ciur Aug 24, 2025
e0011a3
add useDynamicHeight
ciur Aug 24, 2025
8cb19d1
Store global UI state
ciur Aug 24, 2025
ff6cb99
adding global state for the audit log UI
ciur Aug 24, 2025
bc3dbfd
sync filter selector with global state
ciur Aug 25, 2025
ebdc47f
save filters state into global redux store
ciur Aug 25, 2025
595a302
save operation filter state unto global redux store
ciur Aug 25, 2025
17e6bbb
make user_id/username not null
ciur Aug 26, 2025
5358efd
filters..., filters...
ciur Aug 26, 2025
f6b6aba
clear operation filter
ciur Aug 26, 2025
9761dc7
fix minor pagination issues
ciur Aug 26, 2025
0078cd3
fix minor issue
ciur Aug 26, 2025
b7741c8
add table_names filter
ciur Aug 26, 2025
37e5031
opti
ciur Aug 26, 2025
2b9bcf0
optimizations
ciur Aug 26, 2025
16053da
use useCallback
ciur Aug 26, 2025
569978c
wip
ciur Aug 27, 2025
e4c4338
minor adjustments
ciur Aug 27, 2025
522f6d8
refactoring auditLog
ciur Aug 28, 2025
ac8b0f4
operations filter and table names filter works pretty well
ciur Aug 29, 2025
9aae67c
3 filters work!
ciur Aug 29, 2025
8ba1fa4
3 filters work!
ciur Aug 29, 2025
5310362
OKish audit log
ciur Aug 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.6 - not yet released

- Audit log

## 3.5.3 - 2025-08-18

- Performance - async instead of sync: all REST API/DB operations are changed to async
Expand Down
3 changes: 3 additions & 0 deletions frontend/apps/kommon.dev/src/components/NavBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export default function Navbar() {
<NavLink to="/role-form-modal" end>
&lt;RoleFormModal /&gt;
</NavLink>
<NavLink to="/data-table" end>
&lt;DataTable /&gt;
</NavLink>
</Stack>
</nav>
)
Expand Down
4 changes: 3 additions & 1 deletion frontend/apps/kommon.dev/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {createRoot} from "react-dom/client"
import {BrowserRouter, Route, Routes} from "react-router"
import AppShell from "./app/AppShell"
import "./index.css"
import DataTablePage from "./pages/DataTable"
import EditNodeTitleModal from "./pages/EditNodeTitle"
import SubmitButton from "./pages/SubmitButton"
import RoleForm from "./pages/RoleForm"
import RoleFormModal from "./pages/RoleFormModal"
import SubmitButton from "./pages/SubmitButton"

createRoot(document.getElementById("root")!).render(
<StrictMode>
Expand All @@ -19,6 +20,7 @@ createRoot(document.getElementById("root")!).render(
<Route path="edit-node-modal" element={<EditNodeTitleModal />} />
<Route path="role-form" element={<RoleForm />} />
<Route path="role-form-modal" element={<RoleFormModal />} />
<Route path="data-table" element={<DataTablePage />} />
</Route>
</Routes>
</BrowserRouter>
Expand Down
250 changes: 250 additions & 0 deletions frontend/apps/kommon.dev/src/pages/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import {
Badge,
Checkbox,
Container,
Group,
Stack,
Text,
Title
} from "@mantine/core"
import {IconClock, IconDatabase, IconUser} from "@tabler/icons-react"
import type {ColumnConfig, PaginatedResponse} from "kommon"
import {
ColumnSelector,
DataTable,
TableFilters,
TablePagination,
useTableData
} from "kommon"
import {useState} from "react"

interface AuditLogItem {
id: string
table_name: string
record_id: string
operation: "INSERT" | "UPDATE" | "DELETE"
timestamp: string
user_id: string
username: string
}

export default function DataTablePage() {
const [inProgress, setInProgress] = useState<boolean>(false)
const {state, actions, updateData, visibleColumns, totalItems} =
useTableData<AuditLogItem>({
initialData: sampleData,
initialColumns: auditLogColumns
})

const toggleIsLoading = () => {
setInProgress(!inProgress)
}

return (
<Stack>
<Group>
<Checkbox label="Is Loading" onClick={toggleIsLoading} />
</Group>
<Container size="xl" py="md">
<Stack gap="lg">
{/* Page Header */}
<div>
<Title order={2}>Audit Log</Title>
<Text c="dimmed" size="sm">
Track all database operations and changes
</Text>
</div>

{/* Filters and Column Selector */}
<Group justify="space-between" align="flex-start">
<div style={{flex: 1}}>
<TableFilters
columns={state.columns}
filters={state.filters}
onFiltersChange={actions.setFilters}
/>
</div>

<ColumnSelector
columns={state.columns}
onColumnsChange={actions.setColumns}
onToggleColumn={actions.toggleColumnVisibility}
/>
</Group>

{/* Data Table */}
<DataTable
data={sampleData.items}
columns={visibleColumns}
sorting={state.sorting}
onSortChange={actions.setSorting}
columnWidths={state.columnWidths}
onColumnResize={actions.setColumnWidth}
loading={false}
emptyMessage="No audit logs found"
/>

{/* Pagination */}
<TablePagination
currentPage={1}
totalPages={5}
pageSize={10}
onPageChange={actions.setPage}
onPageSizeChange={actions.setPageSize}
totalItems={100}
showPageSizeSelector
/>
</Stack>
</Container>
);
</Stack>
)
}

const sampleData: PaginatedResponse<AuditLogItem> = {
page_size: 15,
page_number: 1,
num_pages: 3,
items: [
{
id: "319483c6-91a7-4f0d-8b3a-827f6079d9e4",
table_name: "nodes",
record_id: "add7799c-a39d-4c16-bf92-2b19d4792ce5",
operation: "INSERT",
timestamp: "2025-08-20T06:35:10.535760Z",
user_id: "49e78737-7c6e-410f-ae27-315b04bdec69",
username: "admin"
},
{
id: "ef31f5c5-c141-40ca-8194-4671a7953ae5",
table_name: "nodes",
record_id: "add7799c-a39d-4c16-bf92-2b19d4792ce5",
operation: "UPDATE",
timestamp: "2025-08-20T06:40:08.056195Z",
user_id: "49e78737-7c6e-410f-ae27-315b04bdec69",
username: "admin"
},
{
id: "e314b451-0347-46fd-8d70-b6e2d4709202",
table_name: "nodes",
record_id: "add7799c-a39d-4c16-bf92-2b19d4792ce5",
operation: "DELETE",
timestamp: "2025-08-21T03:55:49.877671Z",
user_id: "49e78737-7c6e-410f-ae27-315b04bdec69",
username: "admin"
}
]
}

const auditLogColumns: ColumnConfig<AuditLogItem>[] = [
{
key: "timestamp",
label: "Timestamp",
sortable: true,
filterable: false,
width: 180,
render: value => {
const date = new Date(value as string)
return (
<Group gap="xs">
<IconClock size={14} style={{opacity: 0.6}} />
<div>
<Text size="xs">{date.toLocaleDateString()}</Text>
<Text size="xs" c="dimmed">
{date.toLocaleTimeString()}
</Text>
</div>
</Group>
)
}
},
{
key: "operation",
label: "Operation",
sortable: true,
filterable: true,
width: 100,
render: value => {
const colors: Record<string, string> = {
INSERT: "green",
UPDATE: "blue",
DELETE: "red"
}
return (
<Badge
color={colors[value as string] || "gray"}
variant="light"
size="sm"
>
{value as string}
</Badge>
)
}
},
{
key: "table_name",
label: "Table",
sortable: true,
filterable: true,
width: 150,
render: value => (
<Group gap="xs">
<IconDatabase size={14} style={{opacity: 0.6}} />
<Text size="sm" ff="monospace">
{value as string}
</Text>
</Group>
)
},
{
key: "record_id",
label: "Record ID",
sortable: false,
filterable: true,
width: 200,
render: value => (
<Text size="xs" ff="monospace" title={value as string}>
{(value as string).substring(0, 8)}...
</Text>
)
},
{
key: "username",
label: "User",
sortable: true,
filterable: true,
width: 120,
render: value => (
<Group gap="xs">
<IconUser size={14} style={{opacity: 0.6}} />
<Text size="sm">{value as string}</Text>
</Group>
)
},
{
key: "user_id",
label: "User ID",
sortable: false,
filterable: true,
visible: false, // Hidden by default
width: 200,
render: value => (
<Text size="xs" ff="monospace" title={value as string}>
{(value as string).substring(0, 8)}...
</Text>
)
},
{
key: "id",
label: "Log ID",
sortable: false,
filterable: false,
visible: false, // Hidden by default
width: 200,
render: value => (
<Text size="xs" ff="monospace" title={value as string}>
{(value as string).substring(0, 8)}...
</Text>
)
}
]
2 changes: 1 addition & 1 deletion frontend/apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"js-cookie": "^3.0.5",
"kommon": "workspace:*",
"pdfjs-dist": "^5.3.93",
"react": "^19.1.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-i18next": "^15.7.1",
"react-redux": "^9.2.0",
Expand Down
4 changes: 3 additions & 1 deletion frontend/apps/ui/public/localization/de/_default.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,5 +265,7 @@
"notifications.common.error": "Fehler",
"notifications.role.updated.success": "Rolle wurde aktualisiert",
"notifications.role.created.success": "Rolle wurde erstellt",
"notifications.role.deleted.success": "Rolle wurde gelöscht"
"notifications.role.deleted.success": "Rolle wurde gelöscht",
"audit_log.name": "Audit-Protokolle",
"documents": "Dokumente"
}
4 changes: 3 additions & 1 deletion frontend/apps/ui/public/localization/en/_default.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,5 +265,7 @@
"notifications.common.error": "Error",
"notifications.role.updated.success": "Role successfully updated",
"notifications.role.created.success": "Role successfully created",
"notifications.role.deleted.success": "Role successfully deleted"
"notifications.role.deleted.success": "Role successfully deleted",
"audit_log.name": "Audit Logs",
"documents": "Documents"
}
2 changes: 1 addition & 1 deletion frontend/apps/ui/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import logoURL from "/logo_transparent_bg.svg"
import {ColorSchemeToggle} from "@/components/ColorSchemeToggle/ColorSchemeToggle"
import classes from "./Header.module.css"

import LanguageMenu from "./LanguageMenu"
import Search from "./Search"
import SidebarToggle from "./SidebarToggle"
import UserMenu from "./UserMenu"
import LanguageMenu from "./LanguageMenu"

function Header() {
const theme = useMantineTheme()
Expand Down
16 changes: 15 additions & 1 deletion frontend/apps/ui/src/components/Header/SidebarToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,21 @@ export default function SidebarToggle() {
dispatch(toggleNavBar())
}
return (
<UnstyledButton onClick={() => onClick()}>
<UnstyledButton
onClick={() => onClick()}
style={{
outline: "none",
boxShadow: "none",
"&:focus": {
outline: "none",
boxShadow: "none"
},
"&:active": {
outline: "none",
boxShadow: "none"
}
}}
>
<IconMenu2 />
</UnstyledButton>
)
Expand Down
Loading