Dynamic Layout generation
Most layouts (especially lists and cards) in the appliction are generated using "dynamic" components
like DynTable
and DynCard
.
These components allow generating layouts by mapping the properties of the provided data onto TableHead
s, which are objects describing how to display that data.
TableHead
objects contain a value
function and an optional display
property which allow mapping the data to either a string, in which case the data is diplayed as that string,
or to a Component
constructor and its props,
which displays the data using that component and essentially allows data to be displayed in every possible way desired.
This approach allows easily programmatically setting up Table
s and Card
s
without manually having to specify the whole page layout as HTML, greatly increasing maintainability and reusability.
type TableHead<
Value,
DisplayComp extends SvelteComponent | undefined = SvelteComponent | undefined,
DefaultValueReturn = string
> = {
label: string;
display?: DisplayComp extends SvelteComponent ? ComponentType<DisplayComp> : undefined;
value: (
value: Value
) => DisplayComp extends SvelteComponent ? ComponentProps<DisplayComp> : DefaultValueReturn;
};
createTableHeadGenerator
The createTableHeadGenerator
function is an essential utility function that allows generating/validating type safe TableHead
s.
It is generic over the type of data that is supposed to be displayed, which is usually the autogenerated type returned by the OpenAPI fetch client,
and ensures that all generated TableHead
s can only attempt to display properties of the data that are actually available on the provided type.
Signature:
function createTableHeadGenerator<
T,
E extends Record<string, unknown> = Record<string, unknown>
>(): <K extends SvelteComponent | undefined>(tableHead: TableHead<T, K> & E) => TableHead<T, K> & E;
Note that due to several constraints in TypeScript, createTableHeadGenerator
is a wrapper function returning the actual function responsible for validating the TableHead
.
In practice though, this is mostly a convencience instead of an inconvenience, because it allows binding the type of the data to the outer function once
and then receiving the reusable inner function that is already bound to the correct type, resulting in ergonomic code like this:
const data = /* data from some kind of source, usually the MISP API */;
/* Binds the `col` function to `typeof data` */
const col = createTableHeadGenerator<typeof data>();
/* Type checks correctly and only allows using properties available on `data` */
const header = [
col({
icon: 'mdi:id-card',
key: 'name',
label: 'Name',
value: (x) => ({ text: x.name ?? 'unknown' }),
display: Info
}),
col({
icon: 'mdi:telescope',
key: 'scope',
label: 'Scope',
value: (x) => ({ icon: 'mdi:telescope', text: x.scope ?? 'unknown' }),
display: Pill
}),
col({
icon: 'mdi:head-alert',
key: 'overhead',
label: 'Overhead',
value: (x) => ({ value: x.trigger_overhead, options: THREAT_LEVEL_LOOKUP }),
display: LookupPill
})
];