Skip to content

Commit

Permalink
Simplify risk assessments detail view
Browse files Browse the repository at this point in the history
  • Loading branch information
nas-tabchiche committed Feb 27, 2024
1 parent 8b52b80 commit 2c26988
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 184 deletions.
162 changes: 31 additions & 131 deletions frontend/src/routes/(app)/risk-assessments/[id=uuid]/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { BASE_API_URL } from '$lib/utils/constants';
import { getModelInfo, type ModelMapEntry } from '$lib/utils/crud';
import { getModelInfo } from '$lib/utils/crud';

import { modelSchema } from '$lib/utils/schemas';
import { listViewFields } from '$lib/utils/table';
import { tableSourceMapper, type TableSource } from '@skeletonlabs/skeleton';
import { superValidate } from 'sveltekit-superforms/server';
import { z, type AnyZodObject } from 'zod';
import { z } from 'zod';
import type { LayoutServerLoad } from './$types';
import type { UUID } from 'crypto';
import { tableSourceMapper, type TableSource } from '@skeletonlabs/skeleton';
import type { SuperValidated } from 'sveltekit-superforms';
import type { urlModel } from '$lib/utils/types';

export const load: LayoutServerLoad = async ({ fetch, params }) => {
const endpoint = `${BASE_API_URL}/risk-assessments/${params.id}/`;
Expand All @@ -18,139 +14,43 @@ export const load: LayoutServerLoad = async ({ fetch, params }) => {
const scenarios = await fetch(`${BASE_API_URL}/risk-scenarios/?risk_assessment=${params.id}`)
.then((res) => res.json())
.then((res) => res.results);
const scenariosFilter: string =
'?' +
scenarios.map((scenario: Record<string, any>) => `risk_scenarios=${scenario.id}`).join('&');

const measures = await fetch(`${BASE_API_URL}/security-measures/${scenariosFilter}`).then((res) =>
res.json().then((res) => {
const sorted = res.results.sort((a: Record<string, any>, b: Record<string, any>) => {
const dateA = new Date(a.created_at);
const dateB = new Date(b.created_at);
return dateA.getTime() - dateB.getTime();
});
return sorted;
})
);
const risk_matrix = await fetch(
`${BASE_API_URL}/risk-matrices/${risk_assessment.risk_matrix.id}/`
).then((res) => res.json());

// Create a lookup for measures based on their id
const measureLookup: { [id: string]: Record<string, any> } = measures.reduce(
(acc: Record<string, any>, measure: Record<string, any>) => {
acc[measure.id] = measure;
return acc;
},
{}
);

// Replace the measures' UUIDs in each scenario with the corresponding measure instances
const transformedScenarios = scenarios.map((scenario: Record<string, any>) => ({
...scenario,
security_measures: scenario.security_measures.map((childId: UUID) => measureLookup[childId])
}));

risk_assessment.risk_scenarios = transformedScenarios;
risk_assessment.risk_matrix = risk_matrix;

type RelatedModel = {
urlModel: urlModel;
info: ModelMapEntry;
table: TableSource;
deleteForm: SuperValidated<AnyZodObject>;
createForm: SuperValidated<AnyZodObject>;
foreignKeys: Record<string, any>;
selectOptions: Record<string, any>;
};

type RelatedModels = {
[K in urlModel]: RelatedModel;
const scenariosTable: TableSource = {
head: [
'rid',
'name',
'threats',
'existingMeasures',
'currentLevel',
'securityMeasures',
'residualLevel'
],
body: tableSourceMapper(scenarios, [
'rid',
'name',
'threats',
'existing_measures',
'current_level',
'security_measures',
'residual_level'
]),
meta: scenarios
};

const model = getModelInfo('risk-assessments');
const relatedModels = {} as RelatedModels;

if (model.reverseForeignKeyFields) {
await Promise.all(
model.reverseForeignKeyFields.map(async (e) => {
const relEndpoint = `${BASE_API_URL}/${e.urlModel}/?${e.field}=${params.id}`;
const res = await fetch(relEndpoint);
const data = await res.json().then((res) => res.results);

const metaData = tableSourceMapper(data, ['id']);

const bodyData = tableSourceMapper(
data,
listViewFields[e.urlModel].body.filter((field) => field !== 'risk_assessment')
);

const table: TableSource = {
head: listViewFields[e.urlModel].head.filter((field) => field !== 'riskAssessment'),
body: bodyData,
meta: metaData
};

const info = getModelInfo(e.urlModel);
const urlModel = e.urlModel;

const deleteForm = await superValidate(z.object({ id: z.string().uuid() }));
const createSchema = modelSchema(e.urlModel);
const createForm = await superValidate(
{ risk_assessment: risk_assessment.id },
createSchema,
{
errors: false
}
);

const foreignKeys: Record<string, any> = {};
risk_assessment.risk_scenarios = scenarios;
risk_assessment.risk_matrix = risk_matrix;

if (info.foreignKeyFields) {
for (const keyField of info.foreignKeyFields) {
const queryParams = keyField.urlParams ? `?${keyField.urlParams}` : '';
const url = `${BASE_API_URL}/${keyField.urlModel}/${queryParams}`;
const response = await fetch(url);
if (response.ok) {
foreignKeys[keyField.field] = await response.json().then((data) => data.results);
} else {
console.error(`Failed to fetch data for ${keyField.field}: ${response.statusText}`);
}
}
}
const deleteSchema = z.object({ id: z.string() });
const scenarioDeleteForm = await superValidate(deleteSchema);

const selectOptions: Record<string, any> = {};
const scenarioSchema = modelSchema('risk-scenarios');
const scenarioCreateForm = await superValidate(scenarioSchema);

if (info.selectFields) {
for (const selectField of info.selectFields) {
const url = `${BASE_API_URL}/${urlModel}/${selectField.field}/`;
const response = await fetch(url);
if (response.ok) {
selectOptions[selectField.field] = await response.json().then((data) =>
Object.entries(data).map(([key, value]) => ({
label: value,
value: key
}))
);
} else {
console.error(
`Failed to fetch data for ${selectField.field}: ${response.statusText}`
);
}
}
}
relatedModels[e.urlModel] = {
urlModel,
info,
table,
deleteForm,
createForm,
foreignKeys,
selectOptions
};
})
);
}
const scenarioModel = getModelInfo('risk-scenarios');

return { risk_assessment, relatedModels };
return { risk_assessment, scenarioModel, scenariosTable, scenarioDeleteForm, scenarioCreateForm };
};
105 changes: 52 additions & 53 deletions frontend/src/routes/(app)/risk-assessments/[id=uuid]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
<script lang="ts">
import type { RiskScenario, RiskMatrixJsonDefinition } from '$lib/utils/types';
import { page } from '$app/stores';
import CreateModal from '$lib/components/Modals/CreateModal.svelte';
import ModelTable from '$lib/components/ModelTable/ModelTable.svelte';
import RiskMatrix from '$lib/components/RiskMatrix/RiskMatrix.svelte';
import { URL_MODEL_MAP, getModelInfo } from '$lib/utils/crud.js';
import { breadcrumbObject } from '$lib/utils/stores';
import type { RiskMatrixJsonDefinition, RiskScenario } from '$lib/utils/types';
import type {
ModalComponent,
ModalSettings,
ModalStore,
PopupSettings,
ToastStore
} from '@skeletonlabs/skeleton';
import CreateModal from '$lib/components/Modals/CreateModal.svelte';
import ModelTable from '$lib/components/ModelTable/ModelTable.svelte';
import RiskMatrix from '$lib/components/RiskMatrix/RiskMatrix.svelte';
import type { PopupSettings } from '@skeletonlabs/skeleton';
import { popup } from '@skeletonlabs/skeleton';
import { getModalStore, getToastStore } from '@skeletonlabs/skeleton';
import { breadcrumbObject } from '$lib/utils/stores';
import { getModalStore, getToastStore, popup } from '@skeletonlabs/skeleton';
import { superForm } from 'sveltekit-superforms/client';
import { page } from '$app/stores';
import { URL_MODEL_MAP } from '$lib/utils/crud.js';
import * as m from '$paraglide/messages';
import { languageTag } from '$paraglide/runtime';
import { localItems, capitalizeFirstLetter } from '$lib/utils/locales.js';
import RiskScenarioItem from '$lib/components/RiskMatrix/RiskScenarioItem.svelte';
import * as m from '$paraglide/messages';
export let data;
const showRisks = true;
Expand Down Expand Up @@ -56,38 +53,43 @@
}
}
function getForms(model: Record<string, any>) {
let { form: createForm, message: createMessage } = superForm(model.createForm, {
let { form: deleteForm, message: deleteMessage } = {
form: {},
message: {}
};
let { form: createForm, message: createMessage } = {
form: {},
message: {}
};
// NOTE: This is a workaround for an issue we had with getting the return value from the form actions after switching pages in route /[model=urlmodel]/ without a full page reload.
// invalidateAll() did not work.
$: {
({ form: createForm, message: createMessage } = superForm(data.scenarioCreateForm, {
onUpdated: ({ form }) =>
handleFormUpdated({ form, pageStatus: $page.status, closeModal: true })
});
let { form: deleteForm, message: deleteMessage } = superForm(model.deleteForm, {
}));
({ form: deleteForm, message: deleteMessage } = superForm(data.scenarioDeleteForm, {
onUpdated: ({ form }) =>
handleFormUpdated({ form, pageStatus: $page.status, closeModal: true })
});
return { createForm, createMessage, deleteForm, deleteMessage };
}));
}
let forms = {};
$: Object.entries(data.relatedModels).forEach(([key, value]) => {
forms[key] = getForms(value);
});
function modalCreateForm(model: Record<string, any>): void {
function modalCreateForm(): void {
const modalComponent: ModalComponent = {
ref: CreateModal,
props: {
form: model.createForm,
model: model,
form: data.scenarioCreateForm,
model: data.scenarioModel,
debug: false
}
};
const modal: ModalSettings = {
type: 'component',
component: modalComponent,
// Data
title: localItems(languageTag())['add' + capitalizeFirstLetter(model.info.localName)]
title: m.addRiskScenario()
};
modalStore.trigger(modal);
}
Expand Down Expand Up @@ -250,30 +252,27 @@
</div>
<!--Risk risk_assessment-->
<div class="card m-4 p-4 shadow bg-white">
{#if data.relatedModels}
{#each Object.entries(data.relatedModels) as [urlmodel, model]}
<div class="bg-white">
<div class="flex flex-row justify-between">
<h4 class="text-lg font-semibold lowercase capitalize-first my-auto">
{localItems(languageTag())[
'associated' + capitalizeFirstLetter(model.info.localNamePlural)
]}
</h4>
</div>
{#if model.table}
<ModelTable source={model.table} deleteForm={model.deleteForm} URLModel={urlmodel}>
<button
slot="addButton"
class="btn variant-filled-primary self-end my-auto"
on:click={(_) => modalCreateForm(model)}
><i class="fa-solid fa-plus mr-2 lowercase" />
{localItems(languageTag())['add' + capitalizeFirstLetter(model.info.localName)]}
</button>
</ModelTable>
{/if}
</div>
{/each}
{/if}
<div class="bg-white">
<div class="flex flex-row justify-between">
<h4 class="text-lg font-semibold lowercase capitalize-first my-auto">
{m.associatedRiskScenarios()}
</h4>
</div>
<ModelTable
source={data.scenariosTable}
deleteForm={data.scenarioDeleteForm}
model={getModelInfo('risk-scenarios')}
URLModel="risk-scenarios"
>
<button
slot="addButton"
class="btn variant-filled-primary self-end my-auto"
on:click={(_) => modalCreateForm()}
><i class="fa-solid fa-plus mr-2 lowercase" />
{m.addRiskScenario()}
</button>
</ModelTable>
</div>
</div>
<!--Matrix view-->
<div class="card m-4 p-4 shadow bg-white page-break">
Expand Down

0 comments on commit 2c26988

Please sign in to comment.