Skip to content

Commit

Permalink
Created history UI and DB auditing (#391)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlhaufe authored Oct 10, 2024
1 parent 2b9ae16 commit b1f2d6f
Show file tree
Hide file tree
Showing 23 changed files with 287 additions and 90 deletions.
81 changes: 77 additions & 4 deletions components/XDataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type Dialog from 'primevue/dialog'
import type DataTable from 'primevue/datatable'
import { FilterMatchMode } from 'primevue/api';
import camelCaseToTitle from '~/utils/camelCaseToTitle.js';
import { AuditLog } from '~/server/domain';
export type ViewFieldType = 'text' | 'textarea' | 'number' | 'date' | 'boolean' | 'hidden' | 'object'
Expand All @@ -15,6 +16,8 @@ const props = defineProps<{
createModel: { [K in keyof RowType]?: FormFieldType },
editModel: { [K in keyof RowType]?: FormFieldType },
loading: boolean,
showHistory: boolean,
organizationSlug: string,
onCreate: (data: RowType) => Promise<void>,
onDelete: (id: string) => Promise<void>,
onUpdate: (data: RowType) => Promise<void>
Expand All @@ -35,7 +38,12 @@ const dataTable = ref<DataTable>(),
createDialogItem = ref<RowType>(Object.create(null)),
editDialog = ref<Dialog>(),
editDialogVisible = ref(false),
editDialogItem = ref<RowType>(Object.create(null))
editDialogItem = ref<RowType>(Object.create(null)),
historyDialog = ref<Dialog>(),
historyDialogVisible = ref(false),
historyItems = ref<{ date: string, entity: Record<string, any> }[]>([]),
selectedHistoryItem = ref<{ date: string, entity: Record<string, any> }>({ date: '', entity: {} }),
historyDialogLoading = ref(false)
const filters = ref<Record<string, { value: any, matchMode: string }>>({
'global': { value: null, matchMode: FilterMatchMode.CONTAINS }
Expand All @@ -49,6 +57,34 @@ const openEditDialog = (item: RowType) => {
editDialogItem.value = { ...item }
}
const openHistoryDialog = async (item: RowType) => {
historyDialogLoading.value = true
historyDialogVisible.value = true
const auditLog = await $fetch<AuditLog[]>(`/api/audit-log`, {
method: 'GET',
query: {
entityId: item.id,
organizationSlug: props.organizationSlug
}
})
historyItems.value = [
{ date: 'Current', entity: item },
...auditLog.map(log => ({
date: new Date(log.createdAt).toLocaleString(),
entity: JSON.parse(log.entity)
}))
]
selectedHistoryItem.value = historyItems.value[0]
historyDialogLoading.value = false
}
const onHistoryChange = (e: Event) => {
const select = e.target as HTMLSelectElement
selectedHistoryItem.value = historyItems.value[select.selectedIndex]
}
const onDelete = (item: RowType) => new Promise<void>((resolve, _reject) => {
confirm.require({
message: `Are you sure you want to delete ${item.name}?`,
Expand Down Expand Up @@ -151,8 +187,10 @@ const onEditDialogCancel = () => {
</Column>
<Column frozen align-frozen="right">
<template #body="{ data }">
<Button icon="pi pi-pencil" text rounded @click="openEditDialog(data)" />
<Button icon="pi pi-trash" text rounded severity="danger" @click="onDelete(data)" />
<Button icon="pi pi-pencil" text rounded @click="openEditDialog(data)" title="Edit" />
<Button v-if="props.showHistory" icon="pi pi-clock" text rounded @click="openHistoryDialog(data)"
title="History" />
<Button icon="pi pi-trash" text rounded severity="danger" @click="onDelete(data)" title="Delete" />
</template>
</Column>
<template #empty>No data found</template>
Expand Down Expand Up @@ -262,4 +300,39 @@ const onEditDialogCancel = () => {
<Button label="Cancel" type="reset" form="editDialogForm" icon="pi pi-times" class="p-button-text" />
</template>
</Dialog>
</template>

<Dialog ref="historyDialog" v-model:visible="historyDialogVisible" :modal="true" class="p-fluid">
<template #header>
<div class="flex flex-row gap-5 w-full">
<span class="flex align-items-center justify-content-center">History</span>
<select class="p-component p-inputtext flex align-items-center justify-content-center w-14rem"
@change="onHistoryChange">
<option v-for="item in historyItems" :key="item.date">{{ item.date }}</option>
</select>
<ProgressSpinner v-if="historyDialogLoading"
class="flex align-items-center justify-content-center w-2rem" style="width: 50px; height: 50px" />
</div>
</template>
<section>
<div class="field grid" v-for="key of Object.keys(selectedHistoryItem.entity)" :key="key">
<label :for="key" class="col-4">{{ camelCaseToTitle(key) }}:</label>
<span class="col-8" v-if="selectedHistoryItem.entity[key] instanceof Date">
{{ selectedHistoryItem.entity[key].toLocaleString() }}
</span>
<span class="col-8" v-else-if="typeof selectedHistoryItem.entity[key] === 'object'">
{{ selectedHistoryItem.entity[key]?.name ?? JSON.stringify(selectedHistoryItem.entity[key]) }}
</span>
<span class="col-8" v-else>{{ selectedHistoryItem.entity[key] }}</span>
</div>
</section>
<template #footer>
<Button label="Close" icon="pi pi-times" class="p-button-text" @click="historyDialogVisible = false" />
</template>
</Dialog>
</template>

<style scoped>
select {
appearance: auto;
}
</style>
161 changes: 96 additions & 65 deletions migrations/.snapshot-cathedral.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,71 +83,6 @@
"foreignKeys": {},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "uuid",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "uuid"
},
"type": {
"name": "type",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"create",
"update",
"delete",
"update_early",
"delete_early"
],
"mappedType": "enum"
},
"entity": {
"name": "entity",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
}
},
"name": "audit_log",
"schema": "public",
"indexes": [
{
"keyName": "audit_log_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {},
"nativeEnums": {}
},
{
"columns": {
"id": {
Expand Down Expand Up @@ -2937,6 +2872,102 @@
},
"nativeEnums": {}
},
{
"columns": {
"id": {
"name": "id",
"type": "uuid",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "uuid"
},
"solution_id": {
"name": "solution_id",
"type": "uuid",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "uuid"
},
"entity_id": {
"name": "entity_id",
"type": "uuid",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "uuid"
},
"type": {
"name": "type",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"enumItems": [
"create",
"update",
"delete",
"update_early",
"delete_early"
],
"mappedType": "enum"
},
"entity": {
"name": "entity",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"mappedType": "datetime"
}
},
"name": "audit_log",
"schema": "public",
"indexes": [
{
"keyName": "audit_log_pkey",
"columnNames": [
"id"
],
"composite": false,
"constraint": true,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {
"audit_log_solution_id_foreign": {
"constraintName": "audit_log_solution_id_foreign",
"columnNames": [
"solution_id"
],
"localTableName": "public.audit_log",
"referencedColumnNames": [
"id"
],
"referencedTableName": "public.solution",
"updateRule": "cascade"
}
},
"nativeEnums": {}
},
{
"columns": {
"id": {
Expand Down
16 changes: 16 additions & 0 deletions migrations/Migration20241009234603.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Migration } from '@mikro-orm/migrations';

export class Migration20241009234603 extends Migration {

override async up(): Promise<void> {
this.addSql(`alter table "audit_log" add column "solution_id" uuid not null, add column "entity_id" uuid not null;`);
this.addSql(`alter table "audit_log" add constraint "audit_log_solution_id_foreign" foreign key ("solution_id") references "solution" ("id") on update cascade;`);
}

override async down(): Promise<void> {
this.addSql(`alter table "audit_log" drop constraint "audit_log_solution_id_foreign";`);

this.addSql(`alter table "audit_log" drop column "solution_id", drop column "entity_id";`);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const onUpdate = async (data: Assumption) => {
</p>
<XDataTable :viewModel="{ name: 'text', statement: 'text' }" :createModel="{ name: 'text', statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', statement: 'text' }" :datasource="assumptions" :on-create="onCreate"
:on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'">
:on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'" :show-history="true"
:organizationSlug="organizationslug">
</XDataTable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const onUpdate = async (data: EnvironmentComponent) => {
</p>
<XDataTable :viewModel="{ name: 'text', statement: 'text' }" :createModel="{ name: 'text', statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', statement: 'text' }" :datasource="environmentComponents"
:on-create="onCreate" :on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'">
:on-create="onCreate" :on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'"
:show-history="true" :organizationSlug="organizationslug">
</XDataTable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@ const onUpdate = async (data: Constraint) => {
:createModel="{ name: 'text', category: Object.values(ConstraintCategory), statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', category: Object.values(ConstraintCategory), statement: 'text' }"
:datasource="constraints" :on-create="onCreate" :on-delete="onDelete" :on-update="onUpdate"
:loading="status === 'pending'">
:loading="status === 'pending'" :show-history="true" :organizationSlug="organizationslug">
</XDataTable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const onDelete = async (id: string) => {
</p>
<XDataTable :viewModel="{ name: 'text', statement: 'text' }" :createModel="{ name: 'text', statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', statement: 'text' }" :datasource="effects" :on-create="onCreate"
:on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'">
:on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'" :show-history="true"
:organizationSlug="organizationslug">
</XDataTable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const onDelete = async (id: string) => {
</p>
<XDataTable :viewModel="{ name: 'text', statement: 'text' }" :createModel="{ name: 'text', statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', statement: 'text' }" :datasource="glossaryTerms" :on-create="onCreate"
:on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'">
:on-delete="onDelete" :on-update="onUpdate" :loading="status === 'pending'" :show-history="true"
:organizationSlug="organizationslug">
</XDataTable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const onDelete = async (id: string) => {

<XDataTable :viewModel="{ name: 'text', statement: 'text' }" :createModel="{ name: 'text', statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', statement: 'text' }" :datasource="invariants" :on-create="onCreate"
:on-update="onUpdate" :on-delete="onDelete" :loading="status === 'pending'">
:on-update="onUpdate" :on-delete="onDelete" :loading="status === 'pending'" :show-history="true"
:organizationSlug="organizationslug">
</XDataTable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const onDelete = async (id: string) => {

<XDataTable :viewModel="{ name: 'text', statement: 'text' }" :createModel="{ name: 'text', statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', statement: 'text' }" :datasource="functionalBehaviors"
:on-create="onCreate" :on-update="onUpdate" :on-delete="onDelete" :loading="status === 'pending'">
:on-create="onCreate" :on-update="onUpdate" :on-delete="onDelete" :loading="status === 'pending'"
:show-history="true" :organizationSlug="organizationslug">
</XDataTable>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const onDelete = async (id: string) => {

<XDataTable :viewModel="{ name: 'text', statement: 'text' }" :createModel="{ name: 'text', statement: 'text' }"
:editModel="{ id: 'hidden', name: 'text', statement: 'text' }" :datasource="limits" :on-create="onCreate"
:on-update="onUpdate" :on-delete="onDelete" :loading="status === 'pending'">
:on-update="onUpdate" :on-delete="onDelete" :loading="status === 'pending'" :show-history="true"
:organizationSlug="organizationslug">
</XDataTable>
</template>
Loading

0 comments on commit b1f2d6f

Please sign in to comment.