upend/webui/src/components/Inspect.svelte

596 lines
14 KiB
Svelte
Raw Normal View History

2021-11-11 23:37:42 +01:00
<script lang="ts">
2023-06-19 11:53:35 +02:00
import EntryView, { type Widget } from "./EntryView.svelte";
2023-06-28 14:26:34 +02:00
import { useEntity } from "../lib/entity";
2022-03-22 21:57:00 +01:00
import UpObject from "./display/UpObject.svelte";
2022-02-20 18:04:47 +01:00
import { createEventDispatcher, setContext } from "svelte";
2023-06-28 14:26:34 +02:00
import { derived, writable, type Readable } from "svelte/store";
import type { UpEntry } from "@upnd/upend";
2022-01-21 15:57:53 +01:00
import Spinner from "./utils/Spinner.svelte";
import NotesEditor from "./utils/NotesEditor.svelte";
2023-09-07 18:57:45 +02:00
import type { WidgetChange } from "../types/base";
import type { EntityInfo } from "@upnd/upend/types";
2022-01-30 16:40:48 +01:00
import IconButton from "./utils/IconButton.svelte";
import type { BrowseContext } from "../util/browse";
2023-01-10 21:45:03 +01:00
import { Link, useParams } from "svelte-navigator";
import BlobViewer from "./display/BlobViewer.svelte";
import { i18n } from "../i18n";
2023-01-07 11:00:55 +01:00
import EntryList from "./widgets/EntryList.svelte";
import api from "../lib/api";
2023-09-07 15:40:20 +02:00
import EntityList from "./widgets/EntityList.svelte";
import {
ATTR_IN,
ATTR_LABEL,
ATTR_KEY,
ATTR_OF,
} from "@upnd/upend/constants";
import InspectGroups from "./InspectGroups.svelte";
2023-07-29 19:50:00 +02:00
import InspectTypeEditor from "./InspectTypeEditor.svelte";
import LabelBorder from "./utils/LabelBorder.svelte";
2023-09-09 12:09:56 +02:00
import { debug } from "debug";
const dbg = debug("kestrel:Inspect");
2022-02-20 18:04:47 +01:00
const dispatch = createEventDispatcher();
const params = useParams();
2021-11-11 23:37:42 +01:00
export let address: string;
export let index: number | undefined;
export let detail: boolean;
2023-04-23 14:11:19 +02:00
let showAsEntries = false;
let highlightedType: string | undefined;
2021-11-11 23:37:42 +01:00
2022-03-25 14:11:00 +01:00
let blobHandled = false;
let indexStore = writable(index);
$: $indexStore = index;
let addressesStore = writable([]);
$: $addressesStore = $params.addresses?.split(",") || [];
setContext("browse", {
index: indexStore,
addresses: addressesStore,
} as BrowseContext);
2023-01-07 11:00:55 +01:00
$: ({ entity, entityInfo, error, revalidate } = useEntity(address));
2021-11-11 23:37:42 +01:00
2023-06-28 14:26:34 +02:00
$: allTypes = derived(
entityInfo,
($entityInfo, set) => {
getAllTypes($entityInfo).then((allTypes) => {
set(allTypes);
});
},
2023-08-01 22:02:52 +02:00
{},
2023-06-28 14:26:34 +02:00
) as Readable<{
[key: string]: {
labels: string[];
attributes: string[];
};
}>;
async function getAllTypes(entityInfo: EntityInfo) {
const allTypes = {};
if (!entityInfo) {
return {};
}
2021-11-11 23:37:42 +01:00
2023-06-28 14:26:34 +02:00
const typeAddresses: string[] = [
await api.getAddress(entityInfo.t),
2023-07-29 12:39:23 +02:00
...($entity?.attr[ATTR_IN] || []).map((e) => e.value.c as string),
2023-06-28 14:26:34 +02:00
];
const typeAddressesIn = typeAddresses.map((addr) => `@${addr}`).join(" ");
2021-11-11 23:37:42 +01:00
2023-06-28 14:26:34 +02:00
const labelsQuery = await api.query(
2023-08-01 22:02:52 +02:00
`(matches (in ${typeAddressesIn}) "${ATTR_LABEL}" ?)`,
2023-06-28 14:26:34 +02:00
);
typeAddresses.forEach((address) => {
let labels = labelsQuery.getObject(address).identify();
2023-06-28 18:50:33 +02:00
let typeLabel: string | undefined;
if (typeLabel) {
labels.unshift(typeLabel);
}
2023-06-28 18:50:33 +02:00
allTypes[address] = {
labels,
attributes: [],
};
});
2021-11-11 23:37:42 +01:00
2023-06-28 14:26:34 +02:00
const attributes = await api.query(
2023-08-01 22:02:52 +02:00
`(matches ? "${ATTR_OF}" (in ${typeAddressesIn}))`,
2023-06-28 14:26:34 +02:00
);
await Promise.all(
typeAddresses.map(async (address) => {
allTypes[address].attributes = (
await Promise.all(
(attributes.getObject(address).attr[`~${ATTR_OF}`] || []).map(
async (e) => {
try {
const { t, c } = await api.addressToComponents(e.entity);
if (t == "Attribute") {
return c;
}
} catch (err) {
console.error(err);
return false;
}
2023-08-01 22:02:52 +02:00
},
),
2023-06-28 14:26:34 +02:00
)
).filter(Boolean);
2023-08-01 22:02:52 +02:00
}),
2023-06-28 14:26:34 +02:00
);
2021-11-11 23:37:42 +01:00
const result = {};
Object.keys(allTypes).forEach((addr) => {
if (allTypes[addr].attributes.length > 0) {
result[addr] = allTypes[addr];
}
});
return result;
2021-11-11 23:37:42 +01:00
}
let untypedProperties = [] as UpEntry[];
2023-07-16 20:20:56 +02:00
let untypedLinks = [] as UpEntry[];
2021-11-11 23:37:42 +01:00
$: {
untypedProperties = [];
2023-07-16 20:20:56 +02:00
untypedLinks = [];
2021-11-11 23:37:42 +01:00
($entity?.attributes || []).forEach((entry) => {
2023-06-28 14:26:34 +02:00
const entryTypes = Object.entries($allTypes || {}).filter(([_, t]) =>
2023-08-01 22:02:52 +02:00
t.attributes.includes(entry.attribute),
2021-11-11 23:37:42 +01:00
);
if (entryTypes.length === 0) {
2023-07-16 20:20:56 +02:00
if (entry.value.t === "Address") {
untypedLinks.push(entry);
} else {
untypedProperties.push(entry);
2023-07-16 20:20:56 +02:00
}
2021-11-11 23:37:42 +01:00
}
});
untypedProperties = untypedProperties;
2023-07-16 20:20:56 +02:00
untypedLinks = untypedLinks;
2021-11-11 23:37:42 +01:00
}
2022-01-21 15:57:53 +01:00
$: filteredUntypedProperties = untypedProperties.filter(
(entry) =>
2022-03-22 21:57:00 +01:00
![
2023-06-24 16:26:14 +02:00
ATTR_LABEL,
ATTR_IN,
ATTR_KEY,
2022-03-22 21:57:00 +01:00
"NOTE",
"LAST_VISITED",
"NUM_VISITED",
"LAST_ATTRIBUTE_WIDGET",
2023-08-01 22:02:52 +02:00
].includes(entry.attribute),
2022-01-21 15:57:53 +01:00
);
$: currentUntypedProperties = filteredUntypedProperties;
2023-07-16 20:20:56 +02:00
$: filteredUntypedLinks = untypedLinks.filter(
2023-08-01 22:02:52 +02:00
(entry) => ![ATTR_IN, ATTR_OF].includes(entry.attribute),
2023-07-16 20:20:56 +02:00
);
2023-09-01 19:52:49 +02:00
$: currentUntypedLinks = filteredUntypedLinks;
2023-07-16 20:20:56 +02:00
2022-01-28 22:39:08 +01:00
$: currentBacklinks =
2023-09-01 19:52:49 +02:00
$entity?.backlinks.filter(
(entry) => ![ATTR_IN, ATTR_OF].includes(entry.attribute),
) || [];
2022-01-28 22:39:08 +01:00
2023-06-24 16:26:14 +02:00
$: tagged = $entity?.attr[`~${ATTR_IN}`] || [];
2022-01-28 22:39:08 +01:00
2023-01-07 11:00:55 +01:00
let attributesUsed: UpEntry[] = [];
$: {
if ($entityInfo?.t === "Attribute") {
api
.query(`(matches ? "${$entityInfo.c}" ?)`)
.then((result) => (attributesUsed = result.entries));
2023-01-07 11:00:55 +01:00
}
}
2023-09-07 18:57:45 +02:00
async function onChange(ev: CustomEvent<WidgetChange>) {
2023-09-09 12:09:56 +02:00
dbg("onChange", ev.detail);
const change = ev.detail;
switch (change.type) {
case "create":
await api.putEntry({
entity: address,
attribute: change.attribute,
value: change.value,
});
break;
case "delete":
await api.deleteEntry(change.address);
break;
case "update":
await api.putEntityAttribute(address, change.attribute, change.value);
break;
2023-09-07 18:57:45 +02:00
case "entry-add":
await api.putEntry({
2023-09-07 21:55:16 +02:00
entity: change.address,
2023-09-07 18:57:45 +02:00
attribute: ATTR_IN,
2023-09-07 21:55:16 +02:00
value: { t: "Address", c: address },
2023-09-07 18:57:45 +02:00
});
break;
case "entry-delete": {
const inEntry = $entity?.attr[`~${ATTR_IN}`].find(
(e) => e.entity === change.address,
);
if (inEntry) {
await api.deleteEntry(inEntry.address);
} else {
console.warn(
"Couldn't find IN entry for entity %s?!",
change.address,
);
}
break;
}
default:
console.error("Unimplemented AttributeChange", change);
return;
}
revalidate();
}
2022-02-20 18:04:47 +01:00
let identities = [address];
2022-02-20 18:04:47 +01:00
function onResolved(ev: CustomEvent<string[]>) {
identities = ev.detail;
dispatch("resolved", ev.detail);
}
async function deleteObject() {
if (confirm(`${$i18n.t("Really delete")} "${identities.join(" | ")}"?`)) {
await api.deleteEntry(address);
2022-02-20 18:04:47 +01:00
dispatch("close");
}
}
2023-06-19 11:53:35 +02:00
const attributeWidgets: Widget[] = [
{
name: "List",
2023-06-19 12:58:42 +02:00
icon: "list-check",
components: ({ entries }) => [
2023-06-19 11:53:35 +02:00
{
component: EntryList,
props: {
entries,
columns: "attribute, value",
},
},
],
},
2023-07-16 20:20:56 +02:00
];
const linkWidgets: Widget[] = [
{
name: "List",
icon: "list-check",
components: ({ entries, group }) => [
2023-07-16 20:20:56 +02:00
{
component: EntryList,
props: {
entries,
columns: "attribute, value",
attributes: $allTypes[group]?.attributes || [],
2023-07-16 20:20:56 +02:00
},
},
],
},
2023-06-19 11:53:35 +02:00
{
name: "Entity List",
2023-06-19 12:58:42 +02:00
icon: "image",
2023-09-07 18:57:45 +02:00
components: ({ entries, address }) => [
2023-06-19 11:53:35 +02:00
{
2023-09-07 15:40:20 +02:00
component: EntityList,
2023-06-19 11:53:35 +02:00
props: {
2023-09-07 18:57:45 +02:00
address,
2023-06-19 11:53:35 +02:00
entities: entries
.filter((e) => e.value.t == "Address")
.map((e) => e.value.c),
thumbnails: true,
},
},
],
},
];
const taggedWidgets: Widget[] = [
{
name: "List",
2023-06-19 12:58:42 +02:00
icon: "list-check",
2023-09-07 18:57:45 +02:00
components: ({ entries, address }) => [
2023-06-19 11:53:35 +02:00
{
2023-09-07 15:40:20 +02:00
component: EntityList,
2023-06-19 11:53:35 +02:00
props: {
2023-09-07 18:57:45 +02:00
address,
2023-06-19 11:53:35 +02:00
entities: entries.map((e) => e.entity),
thumbnails: false,
},
},
],
},
{
2023-09-07 15:40:20 +02:00
name: "EntityList",
2023-06-19 12:58:42 +02:00
icon: "image",
2023-09-07 18:57:45 +02:00
components: ({ entries, address }) => [
2023-06-19 11:53:35 +02:00
{
2023-09-07 15:40:20 +02:00
component: EntityList,
2023-06-19 11:53:35 +02:00
props: {
2023-09-07 18:57:45 +02:00
address,
2023-06-19 11:53:35 +02:00
entities: entries.map((e) => e.entity),
thumbnails: true,
},
},
],
},
];
$: entity.subscribe(async (object) => {
if (object && object.listing.entries.length) {
2023-09-09 12:09:56 +02:00
dbg("Updating visit stats for %o", object);
2023-06-19 16:45:55 +02:00
await api.putEntityAttribute(
object.address,
"LAST_VISITED",
{
t: "Number",
c: new Date().getTime() / 1000,
},
2023-08-01 22:02:52 +02:00
"IMPLICIT",
2023-06-19 16:45:55 +02:00
);
await api.putEntityAttribute(
object.address,
"NUM_VISITED",
{
t: "Number",
c: (parseInt(String(object.get("NUM_VISITED"))) || 0) + 1,
},
2023-08-01 22:02:52 +02:00
"IMPLICIT",
2023-06-19 16:45:55 +02:00
);
}
});
2021-11-11 23:37:42 +01:00
</script>
<div
class="inspect"
class:detail
class:blob={blobHandled}
data-address-multi={($entity?.attr["~IN"]?.map((e) => e.entity) || []).join(",")}
>
2022-01-28 22:39:08 +01:00
<header>
<h2>
{#if $entity}
2022-03-22 21:57:00 +01:00
<UpObject banner {address} on:resolved={onResolved} />
2022-01-28 22:39:08 +01:00
{:else}
2022-02-06 12:20:56 +01:00
<Spinner centered />
2022-01-28 22:39:08 +01:00
{/if}
</h2>
</header>
2023-04-23 14:11:19 +02:00
{#if !showAsEntries}
<div class="main-content">
<div class="detail-col">
<div class="blob-viewer">
<BlobViewer
{address}
{detail}
on:handled={(ev) => (blobHandled = ev.detail)}
/>
</div>
2023-09-01 19:52:49 +02:00
<NotesEditor {address} on:change={onChange} />
{#if !$error}
2023-07-29 19:50:00 +02:00
<InspectGroups
{entity}
2023-07-29 19:50:00 +02:00
on:highlighted={(ev) => (highlightedType = ev.detail)}
on:change={() => revalidate()}
/>
<div class="properties">
2023-09-01 19:52:49 +02:00
<InspectTypeEditor {entity} on:change={() => revalidate()} />
{#each Object.entries($allTypes) as [typeAddr, { labels, attributes }]}
2023-06-16 16:30:17 +02:00
<EntryView
entries={($entity?.attributes || []).filter((e) =>
attributes.includes(e.attribute),
)}
2023-07-16 20:20:56 +02:00
widgets={linkWidgets}
2023-06-16 16:30:17 +02:00
on:change={onChange}
highlighted={highlightedType == typeAddr}
title={labels.join(" | ")}
group={typeAddr}
2023-09-07 18:57:45 +02:00
{address}
2023-06-16 16:30:17 +02:00
/>
{/each}
{#if currentUntypedProperties.length > 0}
2023-06-16 16:30:17 +02:00
<EntryView
title={$i18n.t("Other Properties")}
2023-06-19 11:53:35 +02:00
widgets={attributeWidgets}
entries={currentUntypedProperties}
2023-06-16 16:30:17 +02:00
on:change={onChange}
2023-09-07 18:57:45 +02:00
{address}
2023-06-16 16:30:17 +02:00
/>
{/if}
2022-03-25 14:11:00 +01:00
2023-09-01 19:52:49 +02:00
{#if currentUntypedLinks.length > 0}
2023-07-16 20:20:56 +02:00
<EntryView
title={$i18n.t("Links")}
widgets={linkWidgets}
entries={currentUntypedLinks}
on:change={onChange}
2023-09-07 18:57:45 +02:00
{address}
2023-07-16 20:20:56 +02:00
/>
{/if}
2023-09-07 21:53:56 +02:00
<EntryView
title={`${$i18n.t("Members")}`}
widgets={taggedWidgets}
entries={tagged}
on:change={onChange}
{address}
/>
2023-01-07 11:00:55 +01:00
2023-07-09 19:27:20 +02:00
{#if currentBacklinks.length > 0}
2023-06-16 16:30:17 +02:00
<EntryView
2023-07-09 19:27:20 +02:00
title={`${$i18n.t("Referred to")} (${currentBacklinks.length})`}
entries={currentBacklinks}
2023-06-16 16:30:17 +02:00
on:change={onChange}
2023-09-07 18:57:45 +02:00
{address}
2023-06-16 16:30:17 +02:00
/>
{/if}
2023-04-23 14:11:19 +02:00
2023-06-16 16:30:17 +02:00
{#if $entityInfo?.t === "Attribute"}
<div class="buttons">
<div class="button">
<Link to="/surface?x={$entityInfo.c}">
{$i18n.t("Surface view")}
</Link>
2023-04-23 14:11:19 +02:00
</div>
2023-01-10 21:45:03 +01:00
</div>
2023-06-16 16:30:17 +02:00
<LabelBorder>
<span slot="header"
>{$i18n.t("Used")} ({attributesUsed.length})</span
>
<EntryList
columns="entity,value"
columnWidths={["auto", "33%"]}
entries={attributesUsed}
orderByValue
/>
</LabelBorder>
2023-01-07 11:00:55 +01:00
{/if}
2023-06-16 16:30:17 +02:00
</div>
2022-03-25 14:11:00 +01:00
{:else}
2023-04-23 14:11:19 +02:00
<div class="error">
{$error}
</div>
2022-02-20 18:04:47 +01:00
{/if}
2023-04-23 14:11:19 +02:00
</div>
</div>
{:else}
<div class="entries">
<h2>{$i18n.t("Attributes")}</h2>
<EntryList
entries={$entity.attributes}
columns={detail
? "timestamp, provenance, attribute, value"
: "attribute, value"}
/>
2023-04-23 14:11:19 +02:00
<h2>{$i18n.t("Backlinks")}</h2>
<EntryList
entries={$entity.backlinks}
columns={detail
? "timestamp, provenance, entity, attribute"
: "entity, attribute"}
/>
2022-03-25 14:11:00 +01:00
</div>
2023-04-23 14:11:19 +02:00
{/if}
<div class="footer">
2023-04-23 14:11:19 +02:00
<IconButton
name="detail"
title={$i18n.t("Show as entries")}
active={showAsEntries}
on:click={() => (showAsEntries = !showAsEntries)}
2023-04-23 14:11:19 +02:00
/>
</div>
<IconButton
name="trash"
outline
subdued
color="#dc322f"
on:click={deleteObject}
title={$i18n.t("Delete object")}
/>
2021-11-11 23:37:42 +01:00
</div>
<style scoped lang="scss">
header h2 {
margin-bottom: 0;
}
.inspect,
.main-content {
2021-11-30 00:29:27 +01:00
flex: auto;
display: flex;
flex-direction: column;
2022-01-28 22:39:08 +01:00
gap: 0.5rem;
min-height: 0;
2021-11-30 00:29:27 +01:00
}
.properties {
2021-11-30 00:29:27 +01:00
flex: auto;
height: 0; // https://stackoverflow.com/a/14964944
min-height: 12em;
2021-11-30 00:29:27 +01:00
overflow-y: auto;
}
@media screen and (max-height: 1080px) {
.main-content {
overflow-y: auto;
// min-height: 0;
}
.properties {
height: unset;
min-height: unset;
overflow-y: unset;
}
}
@media screen and (min-width: 1600px) {
.inspect.detail {
.main-content {
position: relative;
flex-direction: row;
justify-content: end;
}
2022-03-25 14:11:00 +01:00
&.blob {
.detail-col {
width: 25%;
flex-grow: 0;
}
.blob-viewer {
width: 73%;
height: 100%;
position: absolute;
left: 1%;
top: 0;
}
2022-03-25 14:11:00 +01:00
}
}
}
2022-04-09 19:58:44 +02:00
.main-content .detail-col {
display: flex;
flex-direction: column;
flex-grow: 1;
}
2023-04-23 14:11:19 +02:00
.entries {
flex-grow: 1;
}
.footer {
margin-top: 2rem;
2023-04-23 14:11:19 +02:00
display: flex;
justify-content: end;
}
2023-01-10 21:45:03 +01:00
.buttons {
display: flex;
}
2021-11-11 23:37:42 +01:00
.error {
color: red;
}
</style>