Show node state indicator

This commit is contained in:
Sebastian Martinez
2023-11-15 16:21:47 +01:00
committed by Rūdolfs Ošiņš
parent 94985d0489
commit 84addf467d
11 changed files with 392 additions and 143 deletions
+2
View File
@@ -3,6 +3,7 @@
import * as router from "@app/lib/router";
import * as httpd from "@app/lib/httpd";
import * as node from "@app/lib/node";
import { unreachable } from "@app/lib/utils";
import Footer from "./App/Footer.svelte";
@@ -30,6 +31,7 @@
const activeRouteStore = router.activeRouteStore;
void httpd.initialize().finally(() => void router.loadFromLocation());
void node.initialize();
if (!window.VITEST && !window.PLAYWRIGHT && import.meta.env.PROD) {
const plausible = Plausible({
+12 -4
View File
@@ -1,6 +1,10 @@
<script lang="ts">
import Connect from "@app/App/Header/Connect.svelte";
import Link from "@app/components/Link.svelte";
import NodeInfo from "@app/App/Header/NodeInfo.svelte";
import Authenticate from "./Header/Authenticate.svelte";
import { httpdStore } from "@app/lib/httpd";
import { nodeStore } from "@app/lib/node";
</script>
<style>
@@ -44,9 +48,13 @@
</Link>
</div>
<div class="right">
<div class="layout-desktop">
<Connect />
</div>
<div class="right layout-desktop-flex">
{#if $httpdStore.state !== "stopped"}
{#if $nodeStore}
<NodeInfo running={$nodeStore === "running"} />
{/if}
<Authenticate />
{/if}
<Connect />
</div>
</header>
+162
View File
@@ -0,0 +1,162 @@
<script lang="ts">
import * as httpd from "@app/lib/httpd";
import * as modal from "@app/lib/modal";
import { closeFocused } from "@app/components/Popover.svelte";
import { httpdStore } from "@app/lib/httpd";
import Button from "@app/components/Button.svelte";
import ConnectModal from "@app/modals/ConnectModal.svelte";
import IconButton from "@app/components/IconButton.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import Link from "@app/components/Link.svelte";
import NodeId from "@app/components/NodeId.svelte";
import Popover from "@app/components/Popover.svelte";
</script>
<style>
.container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.host {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--font-size-small);
}
.status {
font-size: var(--font-size-tiny);
color: var(--color-fill-gray);
text-align: left;
}
.separator {
height: 1px;
background-color: var(--color-border-hint);
}
.avatar {
height: 1.5rem;
color: var(--color-fill-primary) !important;
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
font-weight: var(--font-weight-regular);
font-family: var(--font-family-monospace);
}
.indicator {
width: 0.75rem;
height: 0.75rem;
background-color: var(--color-fill-secondary);
border-radius: var(--border-radius-round);
position: absolute;
top: -0.375rem;
right: -0.375rem;
}
.row {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.user {
display: flex;
justify-content: space-between;
align-items: center;
}
.identity {
color: var(--color-fill-secondary);
display: flex;
}
</style>
{#if $httpdStore.state === "authenticated"}
<Popover
popoverPositionTop="3rem"
popoverPositionRight="-10rem"
popoverPositionLeft="0">
<Button
slot="toggle"
let:toggle
on:click={toggle}
size="large"
variant="primary">
<div class="avatar">
<NodeId
large
disableTooltip
styleColor="var(--color-fill-primary)"
nodeId={$httpdStore.session.publicKey}
alias={$httpdStore.session.alias} />
</div>
</Button>
<div slot="popover" class="container">
<div class="row">
<div class="status">Httpd server running</div>
<div class="host">
radicle.local
<Link
on:afterNavigate={closeFocused}
route={{
resource: "nodes",
params: {
baseUrl: httpd.api.baseUrl,
projectPageIndex: 0,
},
}}>
<IconButton>Browse</IconButton>
</Link>
</div>
</div>
<div class="separator" />
<div class="row">
<div class="status">Authenticated as</div>
<div class="user">
<div class="identity">
<NodeId
nodeId={$httpdStore.session.publicKey}
alias={$httpdStore.session.alias} />
</div>
<IconButton
on:click={() => {
void httpd.disconnect();
closeFocused();
}}>
Disconnect
</IconButton>
</div>
</div>
</div>
</Popover>
{:else if $httpdStore.state === "running"}
<Button
on:click={() => {
modal.show({
component: ConnectModal,
props: {},
});
}}
size="large"
variant="outline">
<IconSmall name="key" />
Authenticate
<div class="indicator" />
</Button>
{:else}
<Button
on:click={() => {
modal.show({
component: ConnectModal,
props: {},
});
}}
size="large"
variant="outline">
<IconSmall name="chat" />
Authenticate
</Button>
{/if}
+30 -133
View File
@@ -1,18 +1,12 @@
<script lang="ts">
import type { HttpdState } from "@app/lib/httpd";
import * as httpd from "@app/lib/httpd";
import * as modal from "@app/lib/modal";
import { closeFocused } from "@app/components/Popover.svelte";
import { httpdStore } from "@app/lib/httpd";
import Button from "@app/components/Button.svelte";
import ConnectModal from "@app/modals/ConnectModal.svelte";
import Icon from "@app/components/Icon.svelte";
import IconButton from "@app/components/IconButton.svelte";
import Link from "@app/components/Link.svelte";
import NodeId from "@app/components/NodeId.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
import Popover from "@app/components/Popover.svelte";
import Command from "@app/components/Command.svelte";
const buttonTitle: Record<HttpdState["state"], string> = {
stopped: "radicle-httpd is stopped",
@@ -22,64 +16,19 @@
</script>
<style>
.container {
display: flex;
flex-direction: column;
gap: 1.5rem;
width: "23rem";
}
.host {
display: flex;
justify-content: space-between;
align-items: center;
.label {
display: block;
font-size: var(--font-size-small);
}
.status {
font-size: var(--font-size-tiny);
color: var(--color-fill-gray);
text-align: left;
}
.separator {
height: 1px;
background-color: var(--color-border-hint);
}
.avatar {
height: 1.5rem;
color: var(--color-fill-secondary);
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
font-weight: var(--font-weight-regular);
font-family: var(--font-family-monospace);
}
.indicator {
width: 0.75rem;
height: 0.75rem;
background-color: var(--color-fill-secondary);
border-radius: var(--border-radius-round);
position: absolute;
top: -0.375rem;
right: -0.375rem;
}
.row {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.user {
display: flex;
justify-content: space-between;
align-items: center;
}
.identity {
color: var(--color-fill-secondary);
display: flex;
margin-bottom: 0.75rem;
}
</style>
{#if $httpdStore.state === "authenticated"}
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
{#if $httpdStore.state === "stopped"}
<Popover
popoverPositionTop="3rem"
popoverPositionRight="0"
popoverPositionLeft="-13rem">
<Button
slot="toggle"
let:toggle
@@ -87,84 +36,32 @@
title={buttonTitle[$httpdStore.state]}
size="large"
variant="outline">
<div class="avatar">
<NodeId
large
disableTooltip
nodeId={$httpdStore.session.publicKey}
alias={$httpdStore.session.alias} />
</div>
<IconSmall name="device" />
Connect
</Button>
<div slot="popover" class="container">
<div class="row">
<div class="status">Httpd server running</div>
<div class="host">
radicle.local
<Link
on:afterNavigate={closeFocused}
route={{
resource: "nodes",
params: {
baseUrl: httpd.api.baseUrl,
projectPageIndex: 0,
},
}}>
<IconButton>Browse</IconButton>
</Link>
<svelte:fragment slot="popover">
<div>
<div class="label">
Use the <a
target="_blank"
rel="noreferrer"
href="https://radicle.xyz/#try"
class="txt-link txt-bold">
Radicle CLI
</a>
to connect your device.
</div>
</div>
<div class="separator" />
<div class="row">
<div class="status">Authenticated as</div>
<div class="user">
<div class="identity">
<NodeId
nodeId={$httpdStore.session.publicKey}
alias={$httpdStore.session.alias} />
</div>
<IconButton
on:click={() => {
void httpd.disconnect();
closeFocused();
}}>
Disconnect
</IconButton>
<div class="label">
Run the following command to start the httpd daemon.
</div>
<Command command="radicle-httpd" fullWidth />
</div>
</div>
</svelte:fragment>
</Popover>
{:else if $httpdStore.state === "running"}
<Button
on:click={() => {
modal.show({
component: ConnectModal,
props: {},
});
}}
title={buttonTitle[$httpdStore.state]}
size="large"
variant="outline">
<Icon name="device" />
Read only
<div class="indicator" />
</Button>
{:else}
<Button
on:click={() => {
modal.show({
component: ConnectModal,
props: {},
});
}}
title={buttonTitle[$httpdStore.state]}
size="large"
variant="secondary">
<Icon name="device" />
Connect
<Button title={buttonTitle[$httpdStore.state]} size="large" variant="primary">
<IconSmall name="device" />
Connected
</Button>
{/if}
+64
View File
@@ -0,0 +1,64 @@
<script lang="ts">
import Button from "@app/components/Button.svelte";
import Popover from "@app/components/Popover.svelte";
import Command from "@app/components/Command.svelte";
import IconSmall from "@app/components/IconSmall.svelte";
export let running: boolean = false;
</script>
<style>
.label {
display: block;
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
margin-bottom: 0.75rem;
}
</style>
<Popover
popoverPositionTop="3rem"
popoverPositionRight="-13rem"
popoverPositionLeft="0">
<Button
slot="toggle"
let:toggle
on:click={toggle}
size="large"
variant={running ? "primary" : "outline"}>
<IconSmall name="broadcasting" />
{#if running}
Syncing
{:else}
Sync
{/if}
</Button>
<svelte:fragment slot="popover">
{#if running}
<div class="label">
Use the <a
target="_blank"
rel="noreferrer"
href="https://radicle.xyz/#try"
class="txt-link txt-bold">
Radicle CLI
</a>
to stop your node.
</div>
<Command command="rad node stop" fullWidth />
{:else}
<div class="label">
Use the <a
target="_blank"
rel="noreferrer"
href="https://radicle.xyz/#try"
class="txt-link txt-bold">
Radicle CLI
</a>
to start your node.
</div>
<Command command="rad node start" fullWidth />
{/if}
</svelte:fragment>
</Popover>
+4 -3
View File
@@ -138,8 +138,9 @@
}
.primary {
color: var(--color-foreground-match-background);
background-color: var(--color-fill-primary);
color: var(--color-fill-primary);
background-color: var(--color-fill-merged);
border: 1px solid var(--color-border-merged);
}
.primary[disabled] {
@@ -148,7 +149,7 @@
}
.primary:not([disabled]):hover {
background-color: var(--color-fill-primary-hover);
border: 1px solid var(--color-fill-primary);
}
.secondary {
+24
View File
@@ -5,6 +5,7 @@
| "arrow-box-up-right"
| "arrow-reply"
| "branch"
| "broadcasting"
| "brush"
| "chat"
| "checkmark"
@@ -18,6 +19,7 @@
| "commit"
| "cross"
| "delegate"
| "device"
| "diff"
| "download"
| "edit"
@@ -28,6 +30,7 @@
| "face"
| "file"
| "issue"
| "key"
| "logo"
| "more"
| "network"
@@ -106,6 +109,17 @@
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.83331 3.58333L8.83331 2.25L9.83331 2.25L9.83331 3.58333C9.83331 3.85948 9.60946 4.08333 9.33331 4.08333C9.05717 4.08333 8.83331 3.85948 8.83331 3.58333Z" />
{:else if name === "broadcasting"}
<path
d="M4.51041 2.85355C4.70567 2.65829 4.70567 2.34171 4.51041 2.14645C4.31515 1.95118 3.99856 1.95118 3.8033 2.14645C1.3989 4.55085 1.3989 8.44916 3.8033 10.8536C3.99856 11.0488 4.31515 11.0488 4.51041 10.8536C4.70567 10.6583 4.70567 10.3417 4.51041 10.1465C2.49653 8.13258 2.49653 4.86743 4.51041 2.85355Z" />
<path
d="M12.5104 2.14645C12.3152 1.95118 11.9986 1.95118 11.8033 2.14645C11.608 2.34171 11.608 2.65829 11.8033 2.85355C13.8172 4.86743 13.8172 8.13258 11.8033 10.1465C11.608 10.3417 11.608 10.6583 11.8033 10.8536C11.9986 11.0488 12.3152 11.0488 12.5104 10.8536C14.9148 8.44916 14.9148 4.55085 12.5104 2.14645Z" />
<path
d="M6.01041 4.35355C6.20567 4.15829 6.20567 3.84171 6.01041 3.64645C5.81515 3.45118 5.49856 3.45118 5.3033 3.64645C3.72733 5.22242 3.72733 7.77758 5.3033 9.35356C5.49856 9.54882 5.81515 9.54882 6.01041 9.35356C6.20567 9.1583 6.20567 8.84171 6.01041 8.64645C4.82496 7.461 4.82496 5.539 6.01041 4.35355Z" />
<path
d="M11.0104 3.64645C10.8152 3.45118 10.4986 3.45118 10.3033 3.64645C10.108 3.84171 10.108 4.15829 10.3033 4.35355C11.4888 5.539 11.4888 7.461 10.3033 8.64645C10.108 8.84171 10.108 9.1583 10.3033 9.35356C10.4986 9.54882 10.8152 9.54882 11.0104 9.35356C12.5864 7.77758 12.5864 5.22242 11.0104 3.64645Z" />
<path
d="M8.92309 8.34797C9.64744 8.04729 10.1569 7.33314 10.1569 6.5C10.1569 5.39543 9.26143 4.5 8.15686 4.5C7.05229 4.5 6.15686 5.39543 6.15686 6.5C6.15686 7.33314 6.66629 8.0473 7.39065 8.34798L5.65202 14.4332C5.57759 14.6937 5.74329 14.9348 6.02212 14.9718C6.30095 15.0087 6.58732 14.8275 6.66175 14.567L8.15687 9.33407L9.65198 14.5669C9.7264 14.8274 10.0128 15.0087 10.2916 14.9717C10.5704 14.9348 10.7361 14.6936 10.6617 14.4331L8.92309 8.34797Z" />
{:else if name === "chat"}
<path
fill-rule="evenodd"
@@ -173,6 +187,11 @@
d="M3.163 3.163a.556.556 0 01.785 0L8 7.214l4.052-4.051a.556.556 0 01.785.785L8.786 8l4.051 4.052a.556.556 0 01-.785.785L8 8.786l-4.052 4.051a.556.556 0 01-.785-.785L7.214 8 3.163 3.948a.556.556 0 010-.785z"
clip-rule="evenodd">
</path>
{:else if name === "device"}
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.5306 4C4.47537 4 4.4306 4.04477 4.4306 4.1V8.4H11.8306V4.1C11.8306 4.04477 11.7858 4 11.7306 4H4.5306ZM12.0216 9.4H4.23962L3.01198 11.8553C2.97873 11.9218 3.02708 12 3.10142 12H13.1598C13.2341 12 13.2825 11.9218 13.2492 11.8553L12.0216 9.4ZM3.4306 8.78197V4.1C3.4306 3.49249 3.92309 3 4.5306 3H11.7306C12.3381 3 12.8306 3.49249 12.8306 4.1V8.78197L14.1436 11.4081C14.5093 12.1395 13.9775 13 13.1598 13H3.10142C2.2837 13 1.75185 12.1395 2.11755 11.4081L3.4306 8.78197Z" />
{:else if name === "delegate"}
<path
fill-rule="evenodd"
@@ -328,6 +347,11 @@
<path d="M7.40908 13.3182H6.22726V14.5H7.40908V13.3182Z" />
<path d="M9.77273 13.3182H8.59091V14.5H9.77273V13.3182Z" />
<path d="M10.9546 13.3182H9.77274V14.5H10.9546V13.3182Z" />
{:else if name === "key"}
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.97946 2.31295C3.17472 2.11769 3.4913 2.11769 3.68657 2.31295L5.68657 4.31295C5.88183 4.50821 5.88183 4.8248 5.68657 5.02006C5.49131 5.21532 5.17473 5.21532 4.97946 5.02006L4.60568 4.64628C4.43287 4.47347 4.24071 4.46105 4.10338 4.51851C3.97096 4.57393 3.83227 4.71955 3.83301 5.0046C3.83422 5.47296 4.01312 5.93932 4.37 6.2962C5.08596 7.01216 6.24676 7.01216 6.96272 6.2962C7.67868 5.58024 7.67868 4.41944 6.96272 3.70348C6.60482 3.34558 6.1368 3.16669 5.66707 3.16651C5.39093 3.1664 5.16715 2.94246 5.16726 2.66631C5.16737 2.39017 5.39131 2.1664 5.66745 2.16651C6.39143 2.16678 7.11696 2.4435 7.66983 2.99637C8.77631 4.10285 8.77631 5.89682 7.66983 7.00331C6.56334 8.10979 4.76937 8.10979 3.66289 7.00331C3.11163 6.45204 2.83488 5.72909 2.83301 5.00719C2.83141 4.38747 3.14273 3.8914 3.60722 3.64782L2.97946 3.02006C2.7842 2.8248 2.7842 2.50821 2.97946 2.31295ZM8.31281 7.64628C8.50807 7.45102 8.82466 7.45102 9.01992 7.64629L13.0199 11.6463C13.2152 11.8415 13.2152 12.1581 13.0199 12.3534C12.8247 12.5487 12.5081 12.5487 12.3128 12.3534L11.6664 11.7069L10.3532 13.0201C10.158 13.2153 9.8414 13.2153 9.64614 13.0201C9.45088 12.8248 9.45088 12.5082 9.64614 12.313L10.9593 10.9998L10.333 10.3736L9.01991 11.6867C8.82465 11.882 8.50807 11.882 8.31281 11.6867C8.11754 11.4915 8.11754 11.1749 8.31281 10.9796L9.62592 9.6665L8.31281 8.35339C8.11755 8.15813 8.11755 7.84155 8.31281 7.64628Z" />
{:else if name === "more"}
<path
fill-rule="evenodd"
+1 -1
View File
@@ -136,7 +136,7 @@ async function checkState() {
update(httpdState);
}
} else {
await api.getNode();
await api.getNodeInfo();
update({ state: "running" });
}
} catch (error) {
+91
View File
@@ -0,0 +1,91 @@
import { derived, writable } from "svelte/store";
import { withTimeout, Mutex, E_CANCELED, E_TIMEOUT } from "async-mutex";
import { HttpdClient } from "@httpd-client";
import { config } from "@app/lib/config";
export type NodeState = "stopped" | "running";
const NODE_STATE_STORAGE_KEY = "nodeState";
const store = writable<NodeState>("stopped");
export const nodeStore = derived(store, s => s);
export const api = new HttpdClient({
hostname: "127.0.0.1",
port: config.nodes.defaultLocalHttpdPort,
scheme: "http",
});
let pollHttpdStateHandle: number | undefined = undefined;
function update(state: NodeState) {
window.localStorage.setItem(NODE_STATE_STORAGE_KEY, state);
store.set(state);
}
const stateMutex = withTimeout(new Mutex(), 5_000);
async function checkState() {
let nodeState: NodeState | null = null;
const rawNodeState = window.localStorage.getItem(NODE_STATE_STORAGE_KEY);
if (rawNodeState === "running" || rawNodeState === "stopped") {
nodeState = rawNodeState;
}
await stateMutex
.runExclusive(async () => {
try {
if (nodeState) {
update(nodeState);
}
const node = await api.getNode();
update(node.state);
} catch (error) {
if (error instanceof TypeError) {
console.error(error);
}
update("stopped");
}
})
.catch(error => {
if (error !== E_CANCELED && error !== E_TIMEOUT) {
throw error;
}
});
}
function pollSession() {
if (pollHttpdStateHandle) {
return;
}
pollHttpdStateHandle = window.setInterval(() => checkState(), 10_000);
}
export async function initialize() {
// Sync session state changes with other open tabs and windows.
addEventListener("storage", event => {
if (
event.key === NODE_STATE_STORAGE_KEY &&
event.oldValue !== event.newValue
) {
void checkState();
}
});
await checkState();
// Properly clean up setInterval and restart session polling when Vite
// performs hot module reload on file changes.
if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose(() => {
clearInterval(pollHttpdStateHandle);
pollHttpdStateHandle = undefined;
pollSession();
});
}
pollSession();
}
+1 -1
View File
@@ -7,7 +7,7 @@ test("rad web command reacts to port change", async ({ page, peerManager }) => {
await peer.startHttpd(8090);
await page.goto("/");
await page.getByRole("button", { name: "Read only" }).click();
await page.getByRole("button", { name: "Authenticate" }).click();
await expect(
page.getByText(
+1 -1
View File
@@ -169,7 +169,7 @@ export const test = base.extend<{
await peer.startHttpd();
await peer.startNode();
await page.goto("/");
await page.getByRole("button", { name: "Read only" }).click();
await page.getByRole("button", { name: "Authenticate" }).click();
await page
.locator('input[name="port"]')
.fill(peer.httpdBaseUrl.port.toString());