feat(webui): add group view, duplicate group view
ci/woodpecker/push/woodpecker Pipeline failed
Details
ci/woodpecker/push/woodpecker Pipeline failed
Details
parent
f597f0a69a
commit
0b211c237d
|
@ -10,6 +10,7 @@
|
|||
import AddModal from "./components/AddModal.svelte";
|
||||
import Store from "./views/Store.svelte";
|
||||
import Surface from "./views/Surface.svelte";
|
||||
import Groups from "./views/Groups.svelte";
|
||||
|
||||
import "./styles/main.scss";
|
||||
|
||||
|
@ -39,6 +40,10 @@
|
|||
<Store />
|
||||
</Route>
|
||||
|
||||
<Route path="/groups">
|
||||
<Groups />
|
||||
</Route>
|
||||
|
||||
<Footer />
|
||||
|
||||
<AddModal />
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
<script lang="ts">
|
||||
import { ATTR_IN, ATTR_LABEL } from "@upnd/upend/constants";
|
||||
import api from "../lib/api";
|
||||
import { i18n } from "../i18n";
|
||||
import Spinner from "../components/utils/Spinner.svelte";
|
||||
import UpObject from "../components/display/UpObject.svelte";
|
||||
|
||||
const groups = (async () => {
|
||||
const data = await api.query(`(matches ? "${ATTR_IN}" ?)`);
|
||||
|
||||
const addresses = data.entries
|
||||
.filter((e) => e.value.t === "Address")
|
||||
.map((e) => e.value.c) as string[];
|
||||
|
||||
const sortedAddresses = [...new Set(addresses)]
|
||||
.map((address) => ({
|
||||
address,
|
||||
count: addresses.filter((a) => a === address).length,
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
const addressesString = sortedAddresses
|
||||
.map(({ address }) => `@${address}`)
|
||||
.join(" ");
|
||||
const labels = (
|
||||
await api.query(`(matches (in ${addressesString}) "${ATTR_LABEL}" ? )`)
|
||||
).entries.filter((e) => e.value.t === "String");
|
||||
|
||||
const display = sortedAddresses.map(({ address, count }) => ({
|
||||
address,
|
||||
labels: labels
|
||||
.filter((e) => e.entity === address)
|
||||
.map((e) => e.value.c)
|
||||
.sort() as string[],
|
||||
count,
|
||||
}));
|
||||
|
||||
display
|
||||
.sort((a, b) => (a.labels[0] || "").localeCompare(b.labels[0] || ""))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
const labelsToGroups = new Map<string, string[]>();
|
||||
labels.forEach((e) => {
|
||||
const groups = labelsToGroups.get(e.value.c as string) || [];
|
||||
if (!groups.includes(e.entity)) {
|
||||
groups.push(e.entity);
|
||||
}
|
||||
labelsToGroups.set(e.value.c as string, groups);
|
||||
});
|
||||
const duplicates = [...labelsToGroups.entries()]
|
||||
.filter(([_, groups]) => groups.length > 1)
|
||||
.map(([label, groups]) => ({ label, groups }));
|
||||
|
||||
return {
|
||||
groups: display,
|
||||
total: sortedAddresses.length,
|
||||
duplicateGroups: duplicates,
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div class="groups">
|
||||
<h1>{$i18n.t("Groups")}</h1>
|
||||
{#await groups}
|
||||
<Spinner centered />
|
||||
{:then data}
|
||||
<ul>
|
||||
{#each data.groups as group}
|
||||
<li class="group">
|
||||
<UpObject link address={group.address} labels={group.labels} />
|
||||
<div class="count">{group.count}</div>
|
||||
</li>
|
||||
{:else}
|
||||
<li>No groups?</li>
|
||||
{/each}
|
||||
{#if data.groups && data.total > data.groups.length}
|
||||
<li>+ {data.total - data.groups.length}...</li>
|
||||
{/if}
|
||||
</ul>
|
||||
{#if data.duplicateGroups.length > 0}
|
||||
<h2>{$i18n.t("Duplicate groups")}</h2>
|
||||
<ul>
|
||||
{#each data.duplicateGroups as { label, groups }}
|
||||
<li class="duplicate">
|
||||
<div class="label">{label}</div>
|
||||
<ul>
|
||||
{#each groups as group}
|
||||
<li>
|
||||
<UpObject link address={group} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.groups {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 2rem;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: inline-block;
|
||||
font-size: 0.66em;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.duplicate {
|
||||
margin-bottom: 1em;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--foreground);
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
|
@ -178,7 +178,7 @@
|
|||
</section>
|
||||
|
||||
<section class="groups">
|
||||
<h2>{$i18n.t("Groups")}</h2>
|
||||
<h2><Link to="/groups">{$i18n.t("Groups")}</Link></h2>
|
||||
{#await groups}
|
||||
<Spinner centered />
|
||||
{:then data}
|
||||
|
|
Loading…
Reference in New Issue