upend/webui/src/components/widgets/Table.svelte

344 lines
8.5 KiB
Svelte
Raw Normal View History

2021-11-11 23:37:42 +01:00
<script lang="ts">
import filesize from "filesize";
import { format, fromUnixTime } from "date-fns";
2021-12-21 20:02:47 +01:00
import Ellipsis from "../utils/Ellipsis.svelte";
import UpObject from "../display/UpObject.svelte";
2022-01-30 16:50:23 +01:00
import { createEventDispatcher } from "svelte";
import type { AttributeChange, AttributeUpdate } from "../../types/base";
import type { UpEntry } from "upend";
import IconButton from "../utils/IconButton.svelte";
import Selector from "../utils/Selector.svelte";
import type { IValue } from "upend/types";
import Editable from "../utils/Editable.svelte";
2021-12-02 18:45:29 +01:00
const dispatch = createEventDispatcher();
2021-11-11 23:37:42 +01:00
2021-12-21 22:16:33 +01:00
export let columns: string;
2021-12-01 15:39:26 +01:00
export let header = true;
2021-11-30 22:59:34 +01:00
export let entries: UpEntry[];
2021-11-11 23:37:42 +01:00
export let editable = false;
2021-12-01 15:39:26 +01:00
// Display
2021-12-21 22:16:33 +01:00
$: columns = columns || "attribute, value";
2021-12-01 15:39:26 +01:00
$: showEntity = columns.includes("entity");
$: showAttribute = columns.includes("attribute");
$: showValue = columns.includes("value");
// Editing
let newEntryAttribute = "";
let newEntryValue: IValue | undefined;
2021-11-11 23:37:42 +01:00
async function addEntry() {
2021-12-02 18:45:29 +01:00
dispatch("change", {
type: "create",
attribute: newEntryAttribute,
value: newEntryValue,
} as AttributeChange);
newEntryAttribute = "";
newEntryValue = undefined;
2021-11-11 23:37:42 +01:00
}
async function removeEntry(address: string) {
2021-12-02 18:45:29 +01:00
if (confirm("Are you sure you want to remove the attribute?")) {
dispatch("change", { type: "delete", address } as AttributeChange);
2021-12-02 18:45:29 +01:00
}
2021-11-11 23:37:42 +01:00
}
2022-01-28 23:24:30 +01:00
async function updateEntry(
address: string,
attribute: string,
value: IValue
) {
2021-12-02 18:45:29 +01:00
dispatch("change", {
type: "update",
address,
2021-12-02 18:45:29 +01:00
attribute,
value,
} as AttributeUpdate);
2021-11-11 23:37:42 +01:00
}
// Sorting
let sortedAttributes = entries;
let sortKeys: { [key: string]: string[] } = {};
2021-12-20 14:11:53 +01:00
function addSortKeys(key: string, vals: string[], resort = true) {
if (!sortKeys[key]) {
sortKeys[key] = [];
}
2021-12-20 14:11:53 +01:00
let changed = false;
vals.forEach((val) => {
if (!sortKeys[key].includes(val)) {
2021-12-20 14:11:53 +01:00
changed = true;
sortKeys[key].push(val);
}
});
2021-12-20 14:11:53 +01:00
if (resort && changed) sortAttributes();
}
function sortAttributes() {
sortedAttributes = entries
.concat()
.sort((aEntry, bEntry) => {
if (
2022-01-28 16:46:08 +01:00
!sortKeys[aEntry.value.c]?.length ||
!sortKeys[bEntry.value.c]?.length
) {
if (
Boolean(sortKeys[aEntry.value.c]?.length) &&
2022-01-28 16:46:08 +01:00
!sortKeys[bEntry.value.c]?.length
) {
return -1;
} else if (
2022-01-28 16:46:08 +01:00
!sortKeys[aEntry.value.c]?.length &&
Boolean(sortKeys[bEntry.value.c]?.length)
) {
return 1;
} else {
return String(aEntry.value.c).localeCompare(String(bEntry.value.c));
}
2021-12-01 20:13:01 +01:00
} else {
return sortKeys[aEntry.value.c][0].localeCompare(
sortKeys[bEntry.value.c][0],
undefined,
{ numeric: true, sensitivity: "base" }
);
2021-12-01 20:13:01 +01:00
}
})
.sort((aEntry, bEntry) => {
return String(aEntry.value.c).length - String(bEntry.value.c).length;
})
.sort((aEntry, bEntry) => {
return aEntry.attribute.localeCompare(bEntry.attribute);
})
.sort((aEntry, bEntry) => {
if (
2022-01-28 16:46:08 +01:00
!sortKeys[aEntry.entity]?.length ||
!sortKeys[bEntry.entity]?.length
) {
if (
Boolean(sortKeys[aEntry.entity]?.length) &&
2022-01-28 16:46:08 +01:00
!sortKeys[bEntry.entity]?.length
) {
return -1;
} else if (
2022-01-28 16:46:08 +01:00
!sortKeys[aEntry.entity]?.length &&
Boolean(sortKeys[bEntry.entity]?.length)
) {
return 1;
} else {
return aEntry.entity.localeCompare(bEntry.entity);
}
2021-12-01 20:13:01 +01:00
} else {
return sortKeys[aEntry.entity][0].localeCompare(
sortKeys[bEntry.entity][0]
);
2021-12-01 20:13:01 +01:00
}
});
}
entries.forEach((entry) => {
addSortKeys(entry.entity, entry.listing.getObject(entry.entity).identify());
if (entry.value.t === "Address") {
addSortKeys(
2022-01-28 20:51:34 +01:00
entry.value.c,
2021-12-20 14:11:53 +01:00
entry.listing.getObject(String(entry.value.c)).identify(),
false
);
}
});
2021-12-20 14:11:53 +01:00
sortAttributes();
// Formatting & Display
2021-11-11 23:37:42 +01:00
const ATTRIBUTE_LABELS: { [key: string]: string } = {
FILE_MIME: "MIME type",
FILE_MTIME: "Last modified",
FILE_SIZE: "File size",
};
const VALUE_FORMATTERS: { [key: string]: (val: string | number) => string } =
{
FILE_MTIME: (val) =>
format(fromUnixTime(parseInt(String(val), 10)), "PPpp"),
FILE_SIZE: (val) => filesize(parseInt(String(val), 10), { base: 2 }),
};
2021-11-11 23:37:42 +01:00
function formatAttribute(attribute: string) {
return ATTRIBUTE_LABELS[attribute];
}
2022-01-08 19:05:01 +01:00
function formatValue(value: string | number, attribute: string): string {
2021-11-11 23:37:42 +01:00
const handler = VALUE_FORMATTERS[attribute];
if (handler) {
2022-01-08 19:05:01 +01:00
try {
return handler(value);
2022-01-08 19:05:01 +01:00
} catch (error) {
console.warn(`Error while formatting "${value}": ${error}`);
}
2021-11-11 23:37:42 +01:00
}
2022-01-08 19:05:01 +01:00
return String(value);
}
// Optimizations
let resolve = [];
2021-11-11 23:37:42 +01:00
</script>
2021-11-30 22:59:34 +01:00
<table>
<colgroup>
{#if editable}
<col class="attr-action-col" />
{/if}
2021-12-01 15:39:26 +01:00
{#if showEntity}
2021-11-30 22:59:34 +01:00
<col class="entity-col" />
{/if}
2021-12-01 15:39:26 +01:00
{#if showAttribute}
2021-11-30 22:59:34 +01:00
<col class="attr-col" />
{/if}
2021-12-01 15:39:26 +01:00
{#if showValue}
2021-11-30 22:59:34 +01:00
<col class="val-col" />
{/if}
</colgroup>
2021-12-01 15:39:26 +01:00
{#if header}
<tr>
{#if editable}
<th />
{/if}
{#if showEntity}
<th>Entity</th>
{/if}
{#if showAttribute}
<th>Attribute</th>
{/if}
{#if showValue}
<th>Value</th>
{/if}
</tr>
{/if}
2021-11-30 22:59:34 +01:00
{#each sortedAttributes as entry (entry.address)}
2022-01-28 23:24:30 +01:00
<tr>
2021-11-11 23:37:42 +01:00
{#if editable}
2021-11-30 22:59:34 +01:00
<td class="attr-action">
<IconButton
name="x-circle"
on:click={() => removeEntry(entry.address)}
/>
2021-11-30 22:59:34 +01:00
</td>
2021-11-11 23:37:42 +01:00
{/if}
2021-11-30 22:59:34 +01:00
2021-12-01 15:39:26 +01:00
{#if showEntity}
<td class="entity">
<UpObject
link
labels={entry.listing.getObject(String(entry.entity)).identify()}
address={entry.entity}
on:resolved={(event) => {
addSortKeys(entry.entity, event.detail);
}}
/>
2021-11-30 22:59:34 +01:00
</td>
2021-11-11 23:37:42 +01:00
{/if}
2021-11-30 22:59:34 +01:00
2021-12-01 15:39:26 +01:00
{#if showAttribute}
2021-11-30 22:59:34 +01:00
<td class:formatted={Boolean(formatAttribute(entry.attribute))}>
2021-12-11 21:37:12 +01:00
<Ellipsis
value={formatAttribute(entry.attribute) || entry.attribute}
/>
2021-11-30 22:59:34 +01:00
</td>
2021-11-11 23:37:42 +01:00
{/if}
2021-11-30 22:59:34 +01:00
2021-12-01 15:39:26 +01:00
{#if showValue}
2021-11-30 22:59:34 +01:00
<td class="value">
<Editable
2021-11-30 22:59:34 +01:00
{editable}
value={entry.value}
on:edit={(ev) =>
updateEntry(entry.address, entry.attribute, ev.detail)}
2021-11-30 22:59:34 +01:00
>
{#if entry.value.t === "Address"}
<UpObject
2021-11-30 22:59:34 +01:00
link
address={String(entry.value.c)}
labels={entry.listing
.getObject(String(entry.value.c))
.identify()}
resolve={Boolean(resolve[entry.address]) || true}
on:resolved={(event) => {
addSortKeys(String(entry.value.c), event.detail);
}}
2021-11-30 22:59:34 +01:00
/>
{:else}
<div
class:formatted={Boolean(
formatValue(entry.value.c, entry.attribute)
)}
>
2021-12-11 21:37:12 +01:00
<Ellipsis
value={formatValue(entry.value.c, entry.attribute) ||
String(entry.value.c)}
2021-12-11 21:37:12 +01:00
/>
2021-11-30 22:59:34 +01:00
</div>
{/if}
</Editable>
2021-11-30 22:59:34 +01:00
</td>
{/if}
</tr>
{/each}
{#if editable}
<tr class="add-row">
2021-11-30 22:59:34 +01:00
<td class="attr-action">
<IconButton name="plus-circle" on:click={addEntry} />
2021-11-30 22:59:34 +01:00
</td>
2021-12-01 15:39:26 +01:00
{#if showAttribute}
2021-11-30 22:59:34 +01:00
<td>
<Selector type="attribute" bind:attribute={newEntryAttribute} />
2021-11-30 22:59:34 +01:00
</td>
{/if}
2021-12-01 15:39:26 +01:00
{#if showValue}
2021-11-30 22:59:34 +01:00
<td>
<Selector type="value" bind:value={newEntryValue} />
2021-11-30 22:59:34 +01:00
</td>
{/if}
</tr>
{/if}
</table>
2021-11-11 23:37:42 +01:00
<style lang="scss" scoped>
table {
width: 100%;
table-layout: fixed;
2021-12-06 14:35:18 +01:00
border-spacing: 0.5em 0;
2021-12-05 13:29:06 +01:00
2021-11-11 23:37:42 +01:00
th {
text-align: left;
}
td {
font-family: var(--monospace-font);
line-break: anywhere;
2021-12-06 15:24:00 +01:00
border-radius: 4px;
padding: 2px;
2021-11-11 23:37:42 +01:00
&.attr-action {
max-width: 1em;
}
&.formatted,
.formatted {
2021-11-11 23:37:42 +01:00
font-family: var(--default-font);
}
}
.attr-action-col {
width: 1.5em;
}
.attr-col {
width: 33%;
}
}
</style>