diff --git a/ui/src/lib/components/k8s/Drawer/component.svelte b/ui/src/lib/components/k8s/Drawer/component.svelte
index 699e9766..a0faca25 100644
--- a/ui/src/lib/components/k8s/Drawer/component.svelte
+++ b/ui/src/lib/components/k8s/Drawer/component.svelte
@@ -3,7 +3,7 @@
+
+
+ Details
+
+
diff --git a/ui/src/routes/monitor/pepr/[[stream]]/(details)/denied-details/comonent.test.ts b/ui/src/routes/monitor/pepr/[[stream]]/(details)/denied-details/comonent.test.ts
new file mode 100644
index 00000000..f2fbe300
--- /dev/null
+++ b/ui/src/routes/monitor/pepr/[[stream]]/(details)/denied-details/comonent.test.ts
@@ -0,0 +1,25 @@
+import { render } from '@testing-library/svelte'
+
+import type { SvelteComponent } from 'svelte'
+import DeniedDetails from './DeniedDetails.svelte'
+
+// Mock the carbon-icons-svelte module
+
+const comp = vi.fn().mockImplementation(() => ({
+ $$: {
+ on_mount: [],
+ on_destroy: [],
+ before_update: [],
+ after_update: [],
+ },
+})) as unknown as SvelteComponent
+
+describe('Denied Details', () => {
+ test('renders denied messages', () => {
+ const peprDetails = { component: comp, messages: ['Authorized: test', 'Found: test'] }
+ const { getByText } = render(DeniedDetails, { props: { details: peprDetails } })
+ expect(getByText('Details')).toBeInTheDocument()
+ expect(getByText(peprDetails.messages[0])).toBeInTheDocument()
+ expect(getByText(peprDetails.messages[1])).toBeInTheDocument()
+ })
+})
diff --git a/ui/src/routes/monitor/pepr/[[stream]]/(details)/mutated-details/MutatedDetails.svelte b/ui/src/routes/monitor/pepr/[[stream]]/(details)/mutated-details/MutatedDetails.svelte
new file mode 100644
index 00000000..9f7a906b
--- /dev/null
+++ b/ui/src/routes/monitor/pepr/[[stream]]/(details)/mutated-details/MutatedDetails.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
diff --git a/ui/src/routes/monitor/pepr/[[stream]]/(details)/mutated-details/comonent.test.ts b/ui/src/routes/monitor/pepr/[[stream]]/(details)/mutated-details/comonent.test.ts
new file mode 100644
index 00000000..88785667
--- /dev/null
+++ b/ui/src/routes/monitor/pepr/[[stream]]/(details)/mutated-details/comonent.test.ts
@@ -0,0 +1,32 @@
+import { render, screen } from '@testing-library/svelte'
+
+import type { SvelteComponent } from 'svelte'
+import MutatedDetails from './MutatedDetails.svelte'
+
+// Mock the carbon-icons-svelte module
+
+const comp = vi.fn().mockImplementation(() => ({
+ $$: {
+ on_mount: [],
+ on_destroy: [],
+ before_update: [],
+ after_update: [],
+ },
+})) as unknown as SvelteComponent
+
+describe('Denied Details', () => {
+ test('renders exemption title', () => {
+ const peprDetails = {
+ component: comp,
+ operations: {
+ ADDED: [{ op: 'add', path: '/path', value: 'value' }],
+ REPLACED: [{ op: 'add', path: '/path', value: 'value' }],
+ REMOVED: [{ op: 'add', path: '/path', value: '' }],
+ },
+ }
+ render(MutatedDetails, { props: { details: peprDetails } })
+
+ expect(screen.getByText('Details')).toBeInTheDocument()
+ //todo: figure out assertions for split up text
+ })
+})
diff --git a/ui/src/routes/monitor/pepr/[[stream]]/+page.svelte b/ui/src/routes/monitor/pepr/[[stream]]/+page.svelte
index 213c1a86..98e35d06 100644
--- a/ui/src/routes/monitor/pepr/[[stream]]/+page.svelte
+++ b/ui/src/routes/monitor/pepr/[[stream]]/+page.svelte
@@ -10,6 +10,7 @@
import { page } from '$app/stores'
import { type PeprEvent } from '$lib/types'
import './page.postcss'
+ import { getDetails } from './helpers'
let loaded = false
let streamFilter = ''
@@ -45,9 +46,9 @@
eventSource.onmessage = (e) => {
try {
const payload: PeprEvent = JSON.parse(e.data)
-
// The event type is the first word in the header
payload.event = payload.header.split(' ')[0]
+ payload.details = getDetails(payload)
// If this is a repeated event, update the count
if (payload.repeated) {
@@ -141,6 +142,7 @@
Event |
Resource |
+ Details |
Count |
Timestamp |
@@ -158,6 +160,13 @@
{item.event}
{item._name} |
+
+ {#if item.details}
+
+ {:else}
+ -
+ {/if}
+ |
{item.count || 1} |
{item.ts} |
diff --git a/ui/src/routes/monitor/pepr/[[stream]]/helpers.ts b/ui/src/routes/monitor/pepr/[[stream]]/helpers.ts
new file mode 100644
index 00000000..cb71de90
--- /dev/null
+++ b/ui/src/routes/monitor/pepr/[[stream]]/helpers.ts
@@ -0,0 +1,62 @@
+import type { PatchOperation, PeprDetails, PeprEvent } from '$lib/types'
+import type { SvelteComponent } from 'svelte'
+import DeniedDetails from './(details)/denied-details/DeniedDetails.svelte'
+import MutatedDetails from './(details)/mutated-details/MutatedDetails.svelte'
+
+// Utility function to decode base64
+function decodeBase64(base64String: string) {
+ try {
+ return atob(base64String)
+ } catch (e) {
+ console.error('Failed to decode base64 string:', e)
+ return ''
+ }
+}
+
+export function getDetails(payload: PeprEvent): PeprDetails | undefined {
+ if (!payload.res) {
+ return undefined
+ }
+
+ if (payload.event === 'DENIED') {
+ if (payload.res) {
+ const status = payload.res.status as Record
+ const split = status.message.split(' Authorized: ')
+
+ // No "Authorized" or "Found" in the message
+ if (split.length !== 2) {
+ return { component: DeniedDetails as unknown as SvelteComponent, messages: split }
+ }
+
+ const authorized = `Authorized: ${split[1].split(' Found: ')[0]}`
+ const found = `Found: ${split[1].split(' Found:')[1]}`
+ return { component: DeniedDetails as unknown as SvelteComponent, messages: [authorized, found] }
+ }
+ }
+
+ if (payload.event === 'MUTATED') {
+ if (payload.res.patch) {
+ const decodedPatch = decodeBase64(payload.res.patch as string)
+ const parsedPatch = JSON.parse(decodedPatch)
+
+ const opMap: { [key: string]: string } = {
+ add: 'ADDED',
+ remove: 'REMOVED',
+ replace: 'REPLACED',
+ }
+
+ // Group by operation type
+ const groups: { [key: string]: PatchOperation[] } = {}
+ for (const op of parsedPatch) {
+ if (!groups[opMap[op.op]]) {
+ groups[opMap[op.op]] = []
+ }
+ groups[opMap[op.op]].push(op)
+ }
+
+ return { component: MutatedDetails as unknown as SvelteComponent, operations: groups }
+ }
+ }
+
+ return undefined
+}