-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
329 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
<script lang="ts"> | ||
import {getRelayQuality, relaysByUrl, thunks} from "@welshman/app" | ||
import {AuthStatus, PublishStatus, SocketStatus, type Connection} from "@welshman/net" | ||
import {ctx} from "@welshman/lib" | ||
import {displayRelayUrl} from "@welshman/util" | ||
import {onMount} from "svelte" | ||
import cx from "classnames" | ||
import AltColor from "src/partials/AltColor.svelte" | ||
import {quantify} from "hurdak" | ||
import {get} from "svelte/store" | ||
import Tile from "src/partials/Tile.svelte" | ||
export let selected: string | ||
export let activeTab: string | ||
const connectionsStatus: {[key: string]: Map<string, Connection>} = {} | ||
const filters: {[key: string]: boolean} = {} | ||
const pendingStatuses = [ | ||
AuthStatus.Requested, | ||
AuthStatus.PendingSignature, | ||
AuthStatus.PendingResponse, | ||
] | ||
const failureStatuses = [AuthStatus.DeniedSignature, AuthStatus.Forbidden] | ||
function getStatus(status: PublishStatus, cxn: Connection) { | ||
return Object.values($thunks).filter(t => get(t.status)[cxn.url]?.status == status) | ||
} | ||
$: connections = Array.from(ctx.net.pool.data.entries()) | ||
.filter(([url, cxn]) => | ||
Object.keys(filters).filter(f => filters[f]).length | ||
? Object.keys(filters).some(f => filters[f] && connectionsStatus[f]?.has(url)) | ||
: true, | ||
) | ||
.map(([url, cxn]) => cxn) | ||
onMount(() => { | ||
const interval = setInterval(() => { | ||
for (const [url, cxn] of ctx.net.pool.data.entries()) { | ||
if (pendingStatuses.includes(cxn.auth.status)) { | ||
connectionsStatus["Logging in"] = (connectionsStatus["Logging in"] || new Map()).set( | ||
url, | ||
cxn, | ||
) | ||
} else if (failureStatuses.includes(cxn.auth.status)) { | ||
connectionsStatus["Failed to log in"] = ( | ||
connectionsStatus["Failed to log in"] || new Map() | ||
).set(url, cxn) | ||
} else if (cxn.socket.status === SocketStatus.Error) { | ||
connectionsStatus["Failed to connect"] = ( | ||
connectionsStatus["Failed to connect"] || new Map() | ||
).set(url, cxn) | ||
} else if (cxn.socket.status === SocketStatus.Closed) { | ||
connectionsStatus["Waiting to reconnect"] = ( | ||
connectionsStatus["Waiting to reconnect"] || new Map() | ||
).set(url, cxn) | ||
} else if (cxn.socket.status === SocketStatus.New) { | ||
connectionsStatus["Not connected"] = ( | ||
connectionsStatus["Not connected"] || new Map() | ||
).set(url, cxn) | ||
} else if (getRelayQuality(cxn.url) < 0.5) { | ||
connectionsStatus["Unstable connection"] = ( | ||
connectionsStatus["Unstable connection"] || new Map() | ||
).set(url, cxn) | ||
} else { | ||
connectionsStatus["Connected"] = (connectionsStatus["Connected"] || new Map()).set( | ||
url, | ||
cxn, | ||
) | ||
} | ||
} | ||
}, 800) | ||
return () => { | ||
clearInterval(interval) | ||
} | ||
}) | ||
</script> | ||
|
||
<div class="grid grid-cols-6 justify-between gap-2 sm:grid-cols-4"> | ||
<Tile | ||
class={cx({border: filters["Connected"]}, "cursor-pointer")} | ||
background | ||
on:click={() => (filters["Connected"] = !filters["Connected"])}> | ||
<p class="text-lg sm:text-2xl"> | ||
{Array.from(connectionsStatus["Connected"]?.values() || []).length || 0} | ||
</p> | ||
<span class="text-sm">Connected</span> | ||
</Tile> | ||
<Tile | ||
class={cx({border: filters["Logging in"]}, "cursor-pointer")} | ||
background | ||
on:click={() => (filters["Logging in"] = !filters["Logging in"])}> | ||
<p class="text-lg sm:text-2xl"> | ||
{Array.from(connectionsStatus["Logging in"]?.values() || []).length || 0} | ||
</p> | ||
<span class="text-sm">Logging in</span> | ||
</Tile> | ||
<Tile | ||
class={cx({border: filters["Failed to log in"]}, "cursor-pointer")} | ||
background | ||
on:click={() => (filters["Failed to log in"] = !filters["Failed to log in"])}> | ||
<p class="text-lg sm:text-2xl"> | ||
{Array.from(connectionsStatus["Failed to log in"]?.values() || []).length || 0} | ||
</p> | ||
<span class="text-sm">Login failed</span> | ||
</Tile> | ||
<Tile | ||
class={cx({border: filters["Failed to connect"]}, "cursor-pointer")} | ||
background | ||
on:click={() => (filters["Failed to connect"] = !filters["Failed to connect"])} | ||
lass="hidden sm:block"> | ||
<p class="text-lg sm:text-2xl"> | ||
{Array.from(connectionsStatus["Failed to connect"]?.values() || []).length || 0} | ||
</p> | ||
<span class="text-sm">Connection failed</span> | ||
</Tile> | ||
<Tile | ||
class={cx({border: filters["Waiting to reconnect"]}, "cursor-pointer")} | ||
background | ||
on:click={() => (filters["Waiting to reconnect"] = !filters["Waiting to reconnect"])}> | ||
<p class="text-lg sm:text-2xl"> | ||
{Array.from(connectionsStatus["Waiting to reconnect"]?.values() || []).length || 0} | ||
</p> | ||
<span class="text-sm">Reconnecting</span> | ||
</Tile> | ||
<Tile | ||
class={cx({border: filters["Not connected"]}, "cursor-pointer")} | ||
background | ||
on:click={() => (filters["Not connected"] = !filters["Not connected"])}> | ||
<p class="text-lg sm:text-2xl"> | ||
{Array.from(connectionsStatus["Not connected"]?.values() || []).length || 0} | ||
</p> | ||
<span class="text-sm">Not Connected</span> | ||
</Tile> | ||
</div> | ||
{#each connections as cxn (cxn.url)} | ||
{@const relay = $relaysByUrl.get(cxn.url)} | ||
<AltColor | ||
background | ||
class="cursor-pointer justify-between rounded-md p-6 shadow" | ||
on:click={() => { | ||
selected = cxn.url | ||
activeTab = "notices" | ||
}}> | ||
<div class="flex min-w-0 shrink-0 items-center 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(cxn.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 justify-end gap-2"> | ||
{#if getStatus(PublishStatus.Success, cxn).length > 0} | ||
{@const success = getStatus(PublishStatus.Success, cxn).length} | ||
<div class="flex items-center gap-2"> | ||
<i class="fa fa-check-circle text-success"></i>{success || ""} | ||
</div> | ||
{/if} | ||
{#if getStatus(PublishStatus.Failure, cxn).length > 0} | ||
{@const failure = getStatus(PublishStatus.Failure, cxn).length} | ||
<div class="flex items-center gap-2"> | ||
<i class="fa fa-times-circle text-danger"></i>{failure || ""} | ||
</div> | ||
{/if} | ||
{#if getStatus(PublishStatus.Pending, cxn).length > 0} | ||
{@const pending = getStatus(PublishStatus.Pending, cxn).length} | ||
<div class="flex items-center gap-2"> | ||
<i class="fa fa-hourglass-half text-accent"></i>{pending || ""} | ||
</div> | ||
{/if} | ||
{#if getStatus(PublishStatus.Timeout, cxn).length > 0} | ||
{@const timeout = getStatus(PublishStatus.Pending, cxn).length} | ||
<div class="flex items-center gap-2"> | ||
<i class="fa fa-clock"></i>{timeout || ""} | ||
</div> | ||
{/if} | ||
</div> | ||
</div> | ||
</AltColor> | ||
{/each} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
<script lang="ts"> | ||
import {formatTimestamp, thunks, type Thunk} from "@welshman/app" | ||
import {ctx, identity, uniq} from "@welshman/lib" | ||
import {PublishStatus, type Connection} from "@welshman/net" | ||
import {get} from "svelte/store" | ||
import {fly} from "svelte/transition" | ||
import SearchSelect from "src/partials/SearchSelect.svelte" | ||
import {fuzzy} from "src/util/misc" | ||
import Anchor from "src/partials/Anchor.svelte" | ||
import {router} from "src/app/util" | ||
import AltColor from "src/partials/AltColor.svelte" | ||
export let selected: string[] = [] | ||
const searchConnections = fuzzy(Array.from(ctx.net.pool.data.keys())) | ||
function getNotices(connections: Connection[]) { | ||
return Object.values($thunks).filter(t => | ||
connections.some(cxn => get(t.status)[cxn.url]?.status), | ||
) | ||
} | ||
function iconStatus(status: PublishStatus) { | ||
switch (status) { | ||
case PublishStatus.Success: | ||
return "fa fa-check-circle text-success" | ||
case PublishStatus.Pending: | ||
return "fa fa-clock" | ||
case PublishStatus.Failure: | ||
return "fa fa-times-circle text-danger" | ||
case PublishStatus.Timeout: | ||
return "fa fa-hourglass-half text-accent" | ||
case PublishStatus.Aborted: | ||
return "fa fa-ban" | ||
} | ||
} | ||
$: notices = getNotices( | ||
selected.map(url => ctx.net.pool.data.get(url)).filter(Boolean), | ||
) as Thunk[] | ||
</script> | ||
|
||
<SearchSelect | ||
placeholder="Search notices by relay" | ||
multiple | ||
search={searchConnections} | ||
bind:value={selected} | ||
termToItem={identity}> | ||
<div slot="item" let:item> | ||
<strong>{item}</strong> | ||
</div> | ||
</SearchSelect> | ||
|
||
{#if !notices.length && selected.length} | ||
<div transition:fly|local={{y: 20}}> | ||
<AltColor background class="rounded-md p-6 shadow"> | ||
<div class="place text-center text-neutral-100">No notices found for selected relays.</div> | ||
</AltColor> | ||
</div> | ||
{:else} | ||
{#each notices as thunk} | ||
<AltColor background class="rounded-md p-6 shadow"> | ||
<div class="flex justify-between"> | ||
<span>Kind {thunk.event.kind}, published {formatTimestamp(thunk.event.created_at)}</span> | ||
<Anchor | ||
underline | ||
modal | ||
class="text-sm" | ||
on:click={() => router.at("notes").of(thunk.event.id).open()}>View Note</Anchor> | ||
</div> | ||
{#each uniq(Object.entries(get(thunk.status)).filter( ([k, v]) => selected.includes(k), )) as [url, status]} | ||
<div class="mt-4 flex items-center justify-between"> | ||
<strong>{url}</strong> | ||
<div class="flex items-center gap-1"> | ||
<i class={iconStatus(status.status)} /> | ||
<span class="ml-2">{status.status}</span> | ||
</div> | ||
</div> | ||
{/each} | ||
</AltColor> | ||
{/each} | ||
{/if} |