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 AddModal from "./components/AddModal.svelte";
|
||||||
import Store from "./views/Store.svelte";
|
import Store from "./views/Store.svelte";
|
||||||
import Surface from "./views/Surface.svelte";
|
import Surface from "./views/Surface.svelte";
|
||||||
|
import Groups from "./views/Groups.svelte";
|
||||||
|
|
||||||
import "./styles/main.scss";
|
import "./styles/main.scss";
|
||||||
|
|
||||||
|
@ -39,6 +40,10 @@
|
||||||
<Store />
|
<Store />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/groups">
|
||||||
|
<Groups />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
<AddModal />
|
<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>
|
||||||
|
|
||||||
<section class="groups">
|
<section class="groups">
|
||||||
<h2>{$i18n.t("Groups")}</h2>
|
<h2><Link to="/groups">{$i18n.t("Groups")}</Link></h2>
|
||||||
{#await groups}
|
{#await groups}
|
||||||
<Spinner centered />
|
<Spinner centered />
|
||||||
{:then data}
|
{:then data}
|
||||||
|
|
Loading…
Reference in New Issue