feat: add attribute view
This commit is contained in:
parent
7f889be0db
commit
7579f83585
8 changed files with 121 additions and 33 deletions
|
@ -280,6 +280,7 @@ fn main() -> Result<()> {
|
||||||
.service(routes::put_object)
|
.service(routes::put_object)
|
||||||
.service(routes::put_object_attribute)
|
.service(routes::put_object_attribute)
|
||||||
.service(routes::delete_object)
|
.service(routes::delete_object)
|
||||||
|
.service(routes::get_address)
|
||||||
.service(routes::get_all_attributes)
|
.service(routes::get_all_attributes)
|
||||||
.service(routes::api_refresh)
|
.service(routes::api_refresh)
|
||||||
.service(routes::list_hier)
|
.service(routes::list_hier)
|
||||||
|
|
|
@ -619,6 +619,33 @@ pub async fn delete_object(
|
||||||
// Ok(HttpResponse::Ok().finish())
|
// Ok(HttpResponse::Ok().finish())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct GetAddressRequest {
|
||||||
|
attribute: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/api/address")]
|
||||||
|
pub async fn get_address(
|
||||||
|
web::Query(query): web::Query<GetAddressRequest>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
let address = match query {
|
||||||
|
GetAddressRequest {
|
||||||
|
attribute: Some(attribute),
|
||||||
|
} => Address::Attribute(attribute),
|
||||||
|
_ => Err(ErrorBadRequest("Specify one of: `attribute`"))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok()
|
||||||
|
.set_header(
|
||||||
|
http::header::CACHE_CONTROL,
|
||||||
|
CacheControl(vec![
|
||||||
|
CacheDirective::MaxAge(2678400),
|
||||||
|
CacheDirective::Extension("immutable".into(), None),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.json(format!("{}", address)))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/api/all/attributes")]
|
#[get("/api/all/attributes")]
|
||||||
pub async fn get_all_attributes(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
pub async fn get_all_attributes(state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||||
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
let connection = state.upend.connection().map_err(ErrorInternalServerError)?;
|
||||||
|
|
|
@ -15,10 +15,16 @@
|
||||||
import type { BrowseContext } from "../util/browse";
|
import type { BrowseContext } from "../util/browse";
|
||||||
import { useParams } from "svelte-navigator";
|
import { useParams } from "svelte-navigator";
|
||||||
import { GROUP_TYPE_ADDR } from "upend/constants";
|
import { GROUP_TYPE_ADDR } from "upend/constants";
|
||||||
import { deleteEntry, putEntityAttribute, putEntry } from "../lib/api";
|
import {
|
||||||
|
deleteEntry,
|
||||||
|
putEntityAttribute,
|
||||||
|
putEntry,
|
||||||
|
queryOnce,
|
||||||
|
} from "../lib/api";
|
||||||
import Icon from "./utils/Icon.svelte";
|
import Icon from "./utils/Icon.svelte";
|
||||||
import BlobViewer from "./display/BlobViewer.svelte";
|
import BlobViewer from "./display/BlobViewer.svelte";
|
||||||
import { i18n } from "../i18n";
|
import { i18n } from "../i18n";
|
||||||
|
import EntryList from "./widgets/EntryList.svelte";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
|
@ -40,7 +46,7 @@
|
||||||
addresses: addressesStore,
|
addresses: addressesStore,
|
||||||
} as BrowseContext);
|
} as BrowseContext);
|
||||||
|
|
||||||
$: ({ entity, error, revalidate } = useEntity(address));
|
$: ({ entity, entityInfo, error, revalidate } = useEntity(address));
|
||||||
|
|
||||||
$: allTypeAddresses = ($entity?.attr["IS"] || []).map((attr) => attr.value.c);
|
$: allTypeAddresses = ($entity?.attr["IS"] || []).map((attr) => attr.value.c);
|
||||||
|
|
||||||
|
@ -127,6 +133,15 @@
|
||||||
.map((e) => [e.address, e.entity])
|
.map((e) => [e.address, e.entity])
|
||||||
.sort(); // TODO
|
.sort(); // TODO
|
||||||
|
|
||||||
|
let attributesUsed: UpEntry[] = [];
|
||||||
|
$: {
|
||||||
|
if ($entityInfo?.t === "Attribute") {
|
||||||
|
queryOnce(`(matches ? "${$entityInfo.c}" ?)`).then(
|
||||||
|
(result) => (attributesUsed = result.entries)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onChange(ev: CustomEvent<AttributeChange>) {
|
async function onChange(ev: CustomEvent<AttributeChange>) {
|
||||||
const change = ev.detail;
|
const change = ev.detail;
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
|
@ -300,6 +315,15 @@
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $entityInfo?.t === "Attribute"}
|
||||||
|
<EntryList
|
||||||
|
columns="entity,value"
|
||||||
|
columnWidths={["auto", "33%"]}
|
||||||
|
entries={attributesUsed}
|
||||||
|
orderByValue
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if editable}
|
{#if editable}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getAddress } from "../../lib/api";
|
||||||
import { getContext } from "svelte";
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
import { useNavigate, useLocation } from "svelte-navigator";
|
import { useNavigate, useLocation } from "svelte-navigator";
|
||||||
import { readable } from "svelte/store";
|
import { readable } from "svelte/store";
|
||||||
import type { Address, VALUE_TYPE } from "upend/types";
|
import type { Address, VALUE_TYPE } from "upend/types";
|
||||||
|
@ -8,34 +8,42 @@
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
export let passthrough = false;
|
||||||
export let to: {
|
export let to: {
|
||||||
entity?: Address;
|
entity?: Address;
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
value?: { t: VALUE_TYPE; c: string };
|
value?: { t: VALUE_TYPE; c: string };
|
||||||
};
|
};
|
||||||
export let passthrough = false;
|
|
||||||
|
const NOOP = "#";
|
||||||
|
let targetHref = NOOP;
|
||||||
|
if (to.entity) {
|
||||||
|
targetHref = to.entity;
|
||||||
|
} else if (to.attribute) {
|
||||||
|
getAddress({ attribute: to.attribute }).then((address) => {
|
||||||
|
targetHref = address;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const context = getContext("browse") as BrowseContext | undefined;
|
const context = getContext("browse") as BrowseContext | undefined;
|
||||||
const index = context ? context.index : readable(0);
|
const index = context ? context.index : readable(0);
|
||||||
const addresses = context ? context.addresses : readable([]);
|
const addresses = context ? context.addresses : readable([]);
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
if (to.entity) {
|
if ($location.pathname.startsWith("/browse")) {
|
||||||
if ($location.pathname.startsWith("/browse")) {
|
let newAddresses = $addresses.concat();
|
||||||
let newAddresses = $addresses.concat();
|
|
||||||
|
|
||||||
const routerTo =
|
const routerTo =
|
||||||
"/browse/" +
|
"/browse/" +
|
||||||
newAddresses
|
newAddresses
|
||||||
.slice(0, $index + 1)
|
.slice(0, $index + 1)
|
||||||
.concat([to.entity])
|
.concat([targetHref])
|
||||||
.join(",");
|
.join(",");
|
||||||
|
|
||||||
navigate(routerTo);
|
navigate(routerTo);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
navigate(`/browse/${to.entity}`);
|
navigate(`/browse/${targetHref}`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -43,7 +51,8 @@
|
||||||
<a
|
<a
|
||||||
class="uplink"
|
class="uplink"
|
||||||
class:passthrough
|
class:passthrough
|
||||||
href="/#/browse/{to.entity}"
|
class:unresolved={targetHref === NOOP}
|
||||||
|
href="/#/browse/{targetHref}"
|
||||||
on:click|preventDefault={onClick}
|
on:click|preventDefault={onClick}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -56,4 +65,7 @@
|
||||||
:global(.uplink.passthrough) {
|
:global(.uplink.passthrough) {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
:global(.uplink.unresolved) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,13 +4,15 @@
|
||||||
import HashBadge from "./HashBadge.svelte";
|
import HashBadge from "./HashBadge.svelte";
|
||||||
import Ellipsis from "../utils/Ellipsis.svelte";
|
import Ellipsis from "../utils/Ellipsis.svelte";
|
||||||
import UpLink from "./UpLink.svelte";
|
import UpLink from "./UpLink.svelte";
|
||||||
import { useEntity } from "../../lib/entity";
|
import { useEntity, type EntityInfo } from "../../lib/entity";
|
||||||
import { API_URL, nativeOpen as nativeOpenApi } from "../../lib/api";
|
import { API_URL, nativeOpen as nativeOpenApi } from "../../lib/api";
|
||||||
import { readable } from "svelte/store";
|
import { readable, type Readable } from "svelte/store";
|
||||||
import { notify, UpNotification } from "../../notifications";
|
import { notify, UpNotification } from "../../notifications";
|
||||||
import IconButton from "../utils/IconButton.svelte";
|
import IconButton from "../utils/IconButton.svelte";
|
||||||
import { vaultInfo } from "../../util/info";
|
import { vaultInfo } from "../../util/info";
|
||||||
import type { BrowseContext } from "../../util/browse";
|
import type { BrowseContext } from "../../util/browse";
|
||||||
|
import type { UpObject } from "upend";
|
||||||
|
import { i18n } from "../../i18n";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let address: string;
|
export let address: string;
|
||||||
|
@ -18,8 +20,9 @@
|
||||||
export let link = false;
|
export let link = false;
|
||||||
export let banner = false;
|
export let banner = false;
|
||||||
|
|
||||||
let entity = readable(undefined);
|
let entity: Readable<UpObject> = readable(undefined);
|
||||||
$: if (labels === undefined) ({ entity } = useEntity(address));
|
let entityInfo: Readable<EntityInfo> = readable(undefined);
|
||||||
|
$: if (labels === undefined) ({ entity, entityInfo } = useEntity(address));
|
||||||
|
|
||||||
// isFile
|
// isFile
|
||||||
$: isFile = $entity?.get("IS") === BLOB_TYPE_ADDR;
|
$: isFile = $entity?.get("IS") === BLOB_TYPE_ADDR;
|
||||||
|
@ -29,9 +32,16 @@
|
||||||
$: inferredIds = $entity?.identify() || [];
|
$: inferredIds = $entity?.identify() || [];
|
||||||
$: resolving = inferredIds.concat(labels || []).length == 0 && !$entity;
|
$: resolving = inferredIds.concat(labels || []).length == 0 && !$entity;
|
||||||
|
|
||||||
$: displayLabel =
|
let displayLabel = address;
|
||||||
Array.from(new Set(inferredIds.concat(labels || []))).join(" | ") ||
|
$: {
|
||||||
address;
|
displayLabel = Array.from(new Set(inferredIds.concat(labels || []))).join(
|
||||||
|
" | "
|
||||||
|
);
|
||||||
|
if (!displayLabel && $entityInfo?.t === "Attribute") {
|
||||||
|
displayLabel = `${$i18n.t("Attribute")}: ${$entityInfo.c}`;
|
||||||
|
}
|
||||||
|
displayLabel = displayLabel || address;
|
||||||
|
}
|
||||||
|
|
||||||
$: dispatch("resolved", inferredIds);
|
$: dispatch("resolved", inferredIds);
|
||||||
|
|
||||||
|
@ -84,7 +94,11 @@
|
||||||
class:left-active={address == $addresses[$index - 1]}
|
class:left-active={address == $addresses[$index - 1]}
|
||||||
class:right-active={address == $addresses[$index + 1]}
|
class:right-active={address == $addresses[$index + 1]}
|
||||||
>
|
>
|
||||||
<div class="address" class:identified={inferredIds.length || labels?.length} class:banner>
|
<div
|
||||||
|
class="address"
|
||||||
|
class:identified={inferredIds.length || labels?.length}
|
||||||
|
class:banner
|
||||||
|
>
|
||||||
<HashBadge {address} />
|
<HashBadge {address} />
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
<div class="label" class:resolving title={displayLabel}>
|
<div class="label" class:resolving title={displayLabel}>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
import { attributeLabels } from "../../util/labels";
|
import { attributeLabels } from "../../util/labels";
|
||||||
import { formatDuration } from "../../util/fragments/time";
|
import { formatDuration } from "../../util/fragments/time";
|
||||||
import { i18n } from "../../i18n";
|
import { i18n } from "../../i18n";
|
||||||
|
import UpLink from "../display/UpLink.svelte";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let columns: string | undefined = undefined;
|
export let columns: string | undefined = undefined;
|
||||||
|
@ -229,10 +230,12 @@
|
||||||
Object.keys($attributeLabels).includes(entry.attribute)
|
Object.keys($attributeLabels).includes(entry.attribute)
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Ellipsis
|
<UpLink to={{ attribute: entry.attribute }}>
|
||||||
value={$attributeLabels[entry.attribute] || entry.attribute}
|
<Ellipsis
|
||||||
title={entry.attribute}
|
value={$attributeLabels[entry.attribute] || entry.attribute}
|
||||||
/>
|
title={entry.attribute}
|
||||||
|
/>
|
||||||
|
</UpLink>
|
||||||
</td>
|
</td>
|
||||||
{:else if column == VALUE_COL}
|
{:else if column == VALUE_COL}
|
||||||
<td class="value">
|
<td class="value">
|
||||||
|
|
|
@ -138,3 +138,10 @@ export async function fetchStoreInfo(): Promise<{ [key: string]: StoreInfo }> {
|
||||||
const response = await fetch(`${API_URL}/store`);
|
const response = await fetch(`${API_URL}/store`);
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAddress(input: { attribute: string }): Promise<string> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${API_URL}/address?attribute=${input.attribute}`
|
||||||
|
);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
|
@ -4068,8 +4068,8 @@ __metadata:
|
||||||
|
|
||||||
"upend@file:../tools/upend_js::locator=upend-kestrel%40workspace%3A.":
|
"upend@file:../tools/upend_js::locator=upend-kestrel%40workspace%3A.":
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=4972b4&locator=upend-kestrel%40workspace%3A."
|
resolution: "upend@file:../tools/upend_js#../tools/upend_js::hash=b32491&locator=upend-kestrel%40workspace%3A."
|
||||||
checksum: 9449fce51433df8ce7a4d4e597fbd8811611f15ed3d8c499a6e14fd29bbba40adaf7f441e5cd4f4f3439e9fb46adf269783cef574904975569ba99c9933e711d
|
checksum: 4e4e5c0a6498ad2ea8369a91e3c67c160186a96d47073469d61794c3717824198bd2de7851f3e0786833fa5f8c0eb85c65f7d7dc9c0114b9328b2a1060f645b2
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue