Skip to content

Commit

Permalink
Enhance note status (#487)
Browse files Browse the repository at this point in the history
* enhance notices view

* UI refinements

* multiple select button

* align button to the left

* filter out undefined selected

* refactor connectionstatus code

* add subscription Notices

* track subscriptions notices and display them

* clean up and refactor

* - add fuzzy search
- Publishes view refactor

* log verbs are configurable via environment variable

* have notice verb settings hardcoded

* tighten the fuzzy search threshold
  • Loading branch information
ticruz38 authored Dec 10, 2024
1 parent 548b8a3 commit 651bfee
Show file tree
Hide file tree
Showing 8 changed files with 420 additions and 100 deletions.
52 changes: 17 additions & 35 deletions src/app/shared/RelayStatus.svelte
Original file line number Diff line number Diff line change
@@ -1,48 +1,18 @@
<script lang="ts">
import {onMount} from "svelte"
import {ctx} from "@welshman/lib"
import {SocketStatus, AuthStatus} from "@welshman/net"
import {getRelayQuality} from "@welshman/app"
import Popover from "src/partials/Popover.svelte"
import {ConnectionType, displayConnectionType, getConnectionStatus} from "src/domain/connection"
export let url
const pendingStatuses = [
AuthStatus.Requested,
AuthStatus.PendingSignature,
AuthStatus.PendingResponse,
]
const failureStatuses = [AuthStatus.DeniedSignature, AuthStatus.Forbidden]
let description = "Not connected"
let className = "bg-neutral-600"
let status = ConnectionType.NotConnected
onMount(() => {
const interval = setInterval(() => {
const cxn = ctx.net.pool.get(url)
if (pendingStatuses.includes(cxn.auth.status)) {
className = "bg-warning"
description = "Logging in"
} else if (failureStatuses.includes(cxn.auth.status)) {
className = "bg-danger"
description = "Failed to log in"
} else if (cxn.socket.status === SocketStatus.Error) {
className = "bg-danger"
description = "Failed to connect"
} else if (cxn.socket.status === SocketStatus.Closed) {
className = "bg-warning"
description = "Waiting to reconnect"
} else if (cxn.socket.status === SocketStatus.New) {
className = "bg-neutral-600"
description = "Not connected"
} else if (getRelayQuality(cxn.url) < 0.5) {
className = "bg-warning"
description = "Unstable connection"
} else {
className = "bg-success"
description = "Connected"
}
status = getConnectionStatus(cxn)
}, 800)
return () => {
Expand All @@ -52,8 +22,20 @@
</script>

<Popover triggerType="mouseenter">
<div slot="trigger" class="h-2 w-2 cursor-pointer rounded-full {className}" />
<div
slot="trigger"
class="h-2 w-2 cursor-pointer rounded-full bg-neutral-600"
class:bg-neutral-600={ConnectionType.NotConnected == status}
class:bg-danger={[ConnectionType.LoginFailed, ConnectionType.ConnectFailed].some(
s => s == status,
)}
class:bg-success={ConnectionType.Connected == status}
class:bg-warning={[
ConnectionType.Logging,
ConnectionType.WaitReconnect,
ConnectionType.UnstableConnection,
].some(s => s == status)} />
<div slot="tooltip" class="transition-all sm:block">
{description}
{displayConnectionType(status)}
</div>
</Popover>
78 changes: 15 additions & 63 deletions src/app/views/Publishes.svelte
Original file line number Diff line number Diff line change
@@ -1,70 +1,22 @@
<script lang="ts">
import {pluralize, seconds} from "hurdak"
import {assoc, now, remove, sortBy} from "@welshman/lib"
import {LOCAL_RELAY_URL} from "@welshman/util"
import {PublishStatus} from "@welshman/net"
import Tile from "src/partials/Tile.svelte"
import PublishesConnections from "src/app/views/PublishesConnections.svelte"
import PublishesNotices from "src/app/views/PublishesNotices.svelte"
import PublishesEvents from "src/app/views/PublishesEvents.svelte"
import Subheading from "src/partials/Subheading.svelte"
import PublishCard from "src/app/shared/PublishCard.svelte"
import {thunks, type Thunk} from "@welshman/app"
import {get} from "svelte/store"
import Tabs from "src/partials/Tabs.svelte"
const hasStatus = (thunk: Thunk, statuses: PublishStatus[]) =>
Object.values(get(thunk.status)).some(s => statuses.includes(s.status))
const tabs = ["events", "connections", "notices"]
let activeTab = "events"
$: recent = (Object.values($thunks) as Thunk[]).filter(
t =>
remove(LOCAL_RELAY_URL, t.request.relays).length > 0 &&
t.event.created_at > now() - seconds(24, "hour"),
)
$: relays = new Set(
remove(
LOCAL_RELAY_URL,
recent.flatMap(({request}) => request.relays),
),
)
$: success = recent.filter(t => hasStatus(t, [PublishStatus.Success]))
$: pending = recent.filter(
t => hasStatus(t, [PublishStatus.Pending]) && !hasStatus(t, [PublishStatus.Success]),
)
// If the page gets refreshed before pending finishes, it hangs. Set stuff to failed
$: {
for (const t of recent) {
if (t.event.created_at < now() - seconds(1, "minute")) {
for (const [url, s] of Object.entries(t.status)) {
if (s.status === PublishStatus.Pending) {
t.status.update(assoc(url, {status: PublishStatus.Failure, message: ""}))
}
}
}
}
}
let selected: string
</script>

<Subheading>Published Events</Subheading>
<div class="grid grid-cols-4 justify-between gap-2 sm:grid-cols-5">
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length}</p>
<span class="text-sm">{pluralize(recent.length, "Event")}</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{relays.size}</p>
<span class="text-sm">{pluralize(relays.size, "Relay")}</span>
</Tile>
<Tile background lass="hidden sm:block">
<p class="text-lg sm:text-2xl">{pending.length}</p>
<span class="text-sm">Pending</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{success.length}</p>
<span class="text-sm">Succeeded</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length - pending.length - success.length}</p>
<span class="text-sm">Failed</span>
</Tile>
</div>
{#each sortBy(t => -t.event.created_at, recent) as thunk (thunk.event.id)}
<PublishCard {thunk} />
{/each}
<Tabs {tabs} {activeTab} setActiveTab={tab => (activeTab = tab)} />
{#if activeTab === "events"}
<PublishesEvents />
{:else if activeTab === "connections"}
<PublishesConnections bind:selected bind:activeTab />
{:else if activeTab === "notices"}
<PublishesNotices search={selected} />
{/if}
105 changes: 105 additions & 0 deletions src/app/views/PublishesConnections.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import {relaysByUrl} from "@welshman/app"
import {addToMapKey, ctx} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util"
import {quantify} from "hurdak"
import {onMount} from "svelte"
import AltColor from "src/partials/AltColor.svelte"
import SelectButton from "src/partials/SelectButton.svelte"
import {ConnectionType, displayConnectionType, getConnectionStatus} from "src/domain/connection"
export let selected: string
export let activeTab: string
let selectedOptions: ConnectionType[] = []
let connectionsStatus: Map<ConnectionType, Set<string>> = new Map()
const options = [
ConnectionType.Connected,
ConnectionType.Logging,
ConnectionType.LoginFailed,
ConnectionType.ConnectFailed,
ConnectionType.WaitReconnect,
ConnectionType.NotConnected,
ConnectionType.UnstableConnection,
]
$: connections = Array.from(ctx.net.pool.data.keys()).filter(url =>
selectedOptions.length ? selectedOptions.some(s => connectionsStatus.get(s)?.has(url)) : true,
)
function fetchConnectionStatus() {
const newConnectionStatus: Map<ConnectionType, Set<string>> = new Map()
for (const [url, cxn] of ctx.net.pool.data.entries()) {
addToMapKey(newConnectionStatus, getConnectionStatus(cxn), url)
}
connectionsStatus = newConnectionStatus
}
onMount(() => {
fetchConnectionStatus()
const interval = setInterval(fetchConnectionStatus, 800)
return () => {
clearInterval(interval)
}
})
</script>

<SelectButton {options} bind:value={selectedOptions} multiple class="text-left">
<div class="flex items-center gap-2" slot="item" let:option>
{connectionsStatus.get(option)?.size || 0}
{displayConnectionType(option)}
</div>
</SelectButton>
{#each connections as url (url)}
{@const relay = $relaysByUrl.get(url)}
<AltColor
background
class="cursor-pointer justify-between rounded-md p-6 shadow"
on:click={() => {
selected = url
activeTab = "notices"
}}>
<div class="flex min-w-0 shrink-0 items-start gap-3">
{#if relay?.profile?.icon}
<img class="h-9 w-9 shrink-0 rounded-full border" src={relay.profile.icon} />
{:else}
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full border">
<i class="fa fa-server text-xl text-neutral-100"></i>
</div>
{/if}
<div class="shrink-0">
<div class="flex items-center gap-2">
<div class="text-md overflow-hidden text-ellipsis whitespace-nowrap">
{displayRelayUrl(url)}
</div>
</div>
<div class="flex gap-4 text-xs text-neutral-400">
{#if relay?.profile?.supported_nips}
<span>
{relay.profile.supported_nips.length} NIPs
</span>
{/if}
<span>
Connected {quantify(relay?.stats?.open_count || 0, "time")}
</span>
</div>
</div>
<div class="flex w-full items-center justify-end gap-2 text-sm">
{#each options.filter(o => connectionsStatus.get(o)?.has(url)) as o}
{@const opt = displayConnectionType(o)}
<div class="flex items-center gap-2">
<span>{opt}</span>
<div
class:!bg-danger={opt.includes("Failed") || opt.includes("Not")}
class:!bg-warning={opt == "Logging in" ||
o == ConnectionType.WaitReconnect ||
o == ConnectionType.UnstableConnection}
class="h-3 w-3 rounded-full bg-success" />
</div>
{/each}
</div>
</div>
</AltColor>
{/each}
68 changes: 68 additions & 0 deletions src/app/views/PublishesEvents.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script lang="ts">
import {thunks, type Thunk} from "@welshman/app"
import {assoc, now, remove, sortBy} from "@welshman/lib"
import {pluralize, seconds} from "hurdak"
import Tile from "src/partials/Tile.svelte"
import PublishCard from "src/app/shared/PublishCard.svelte"
import {LOCAL_RELAY_URL} from "@welshman/util"
import {PublishStatus} from "@welshman/net"
import {get} from "svelte/store"
const hasStatus = (thunk: Thunk, statuses: PublishStatus[]) =>
Object.values(get(thunk.status)).some(s => statuses.includes(s.status))
$: recent = (Object.values($thunks) as Thunk[]).filter(
t =>
remove(LOCAL_RELAY_URL, t.request.relays).length > 0 &&
t.event.created_at > now() - seconds(24, "hour"),
)
$: relays = new Set(
remove(
LOCAL_RELAY_URL,
recent.flatMap(({request}) => request.relays),
),
)
$: success = recent.filter(t => hasStatus(t, [PublishStatus.Success]))
$: pending = recent.filter(
t => hasStatus(t, [PublishStatus.Pending]) && !hasStatus(t, [PublishStatus.Success]),
)
// If the page gets refreshed before pending finishes, it hangs. Set stuff to failed
$: {
for (const t of recent) {
if (t.event.created_at < now() - seconds(1, "minute")) {
for (const [url, s] of Object.entries(t.status)) {
if (s.status === PublishStatus.Pending) {
t.status.update(assoc(url, {status: PublishStatus.Failure, message: ""}))
}
}
}
}
}
</script>

<div class="grid grid-cols-4 justify-between gap-2 sm:grid-cols-5">
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length}</p>
<span class="text-sm">{pluralize(recent.length, "Event")}</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{relays.size}</p>
<span class="text-sm">{pluralize(relays.size, "Relay")}</span>
</Tile>
<Tile background lass="hidden sm:block">
<p class="text-lg sm:text-2xl">{pending.length}</p>
<span class="text-sm">Pending</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{success.length}</p>
<span class="text-sm">Succeeded</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length - pending.length - success.length}</p>
<span class="text-sm">Failed</span>
</Tile>
</div>
{#each sortBy(t => -t.event.created_at, recent) as thunk (thunk.event.id)}
<PublishCard {thunk} />
{/each}
Loading

0 comments on commit 651bfee

Please sign in to comment.