sharedstate 0.5.0

Sync heavily read state across many servers
Documentation
import { PlugZap, Power, Save, Send, Trash2 } from "lucide-react";
import { FormEvent, useEffect, useState } from "react";
import { deleteKey, setBlockedPeers, setKey, setNetworking, startNode, stopNode } from "../api";
import type { NodeView } from "../types";

interface Props {
  node: NodeView | null;
  onChanged: () => Promise<void>;
}

function list(values: number[] | null | undefined) {
  return values && values.length > 0 ? values.join(", ") : "none";
}

export function NodePanel({ node, onChanged }: Props) {
  const [blockedPeers, setBlockedPeersInput] = useState("");
  const [setKeyName, setSetKeyName] = useState("");
  const [setValue, setSetValue] = useState("");
  const [deleteKeyName, setDeleteKeyName] = useState("");
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    setBlockedPeersInput(node?.blocked_peers.join(",") ?? "");
    setError(null);
  }, [node?.address]);

  if (!node) {
    return (
      <aside className="context-panel empty-panel">
        <h2>Node Details</h2>
        <p>Select a node in the graph to inspect its election state, network controls, and key/value data.</p>
      </aside>
    );
  }

  async function run(action: () => Promise<void>) {
    setError(null);
    try {
      await action();
      await onChanged();
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    }
  }

  function parseBlockedPeers() {
    if (!blockedPeers.trim()) {
      return [];
    }
    return blockedPeers
      .split(",")
      .map((part) => part.trim())
      .filter(Boolean)
      .map((part) => {
        const parsed = Number(part);
        if (!Number.isSafeInteger(parsed) || parsed <= 0) {
          throw new Error(`invalid peer address: ${part}`);
        }
        return parsed;
      });
  }

  async function submitSet(event: FormEvent) {
    event.preventDefault();
    if (!setKeyName.trim()) {
      setError("set requires a key");
      return;
    }
    await run(async () => {
      await setKey(node.address, setKeyName.trim(), setValue);
      setSetKeyName("");
      setSetValue("");
    });
  }

  async function submitDelete(event: FormEvent) {
    event.preventDefault();
    if (!deleteKeyName.trim()) {
      setError("delete requires a key");
      return;
    }
    await run(async () => {
      await deleteKey(node.address, deleteKeyName.trim());
      setDeleteKeyName("");
    });
  }

  const values = Object.entries(node.state?.values ?? {});

  return (
    <aside className="context-panel">
      <div className="panel-heading">
        <div>
          <h2>{node.address}</h2>
          <span className={`status-pill status-${node.status}`}>{node.status}</span>
        </div>
      </div>

      {error && <div className="form-error">{error}</div>}

      <section className="panel-section action-grid">
        <button
          className="secondary-button"
          onClick={() => run(() => (node.online ? stopNode(node.address) : startNode(node.address)))}
        >
          <Power size={16} />
          {node.online ? "Turn Off" : "Turn On"}
        </button>
        <button
          className="secondary-button"
          disabled={!node.online}
          onClick={() => run(() => setNetworking(node.address, !node.networking_disabled))}
        >
          <PlugZap size={16} />
          {node.networking_disabled ? "Enable Networking" : "Disable Networking"}
        </button>
      </section>

      <section className="panel-section facts">
        <div><span>can_lead</span><b>{node.can_lead ? "yes" : "no"}</b></div>
        <div><span>leader</span><b>{node.leader ?? "none"}</b></div>
        <div><span>term</span><b>{node.term ?? "none"}</b></div>
        <div><span>follow remote</span><b>{node.follow_remote ?? "none"}</b></div>
        <div><span>leader path</span><b>{list(node.leader_path)}</b></div>
        <div><span>known peers</span><b>{list(node.known_peers)}</b></div>
        <div><span>known can_lead</span><b>{list(node.known_can_lead)}</b></div>
      </section>

      <section className="panel-section">
        <label>
          Blocked peers
          <input value={blockedPeers} onChange={(event) => setBlockedPeersInput(event.target.value)} placeholder="7002,7003" />
        </label>
        <button
          className="secondary-button full-width"
          onClick={() => run(() => setBlockedPeers(node.address, parseBlockedPeers()))}
        >
          <Save size={16} />
          Save Blocked Peers
        </button>
      </section>

      <section className="panel-section">
        <h3>Key Value State</h3>
        <div className="kv-table">
          {values.length === 0 && <div className="empty-row">empty</div>}
          {values.map(([key, value]) => (
            <div className="kv-row" key={key}>
              <span>{key}</span>
              <b>{value}</b>
            </div>
          ))}
        </div>
      </section>

      <form className="panel-section compact-form" onSubmit={submitSet}>
        <h3>Set</h3>
        <input value={setKeyName} onChange={(event) => setSetKeyName(event.target.value)} placeholder="key" />
        <input value={setValue} onChange={(event) => setSetValue(event.target.value)} placeholder="value" />
        <button className="primary-button" disabled={!node.online}>
          <Send size={16} />
          Send
        </button>
      </form>

      <form className="panel-section compact-form" onSubmit={submitDelete}>
        <h3>Delete</h3>
        <input value={deleteKeyName} onChange={(event) => setDeleteKeyName(event.target.value)} placeholder="key" />
        <button className="danger-button" disabled={!node.online}>
          <Trash2 size={16} />
          Delete
        </button>
      </form>

      <section className="panel-section">
        <h3>Peers</h3>
        <div className="peer-list">
          {node.debug?.peers.map((peer) => (
            <div className="peer-row" key={peer.address}>
              <b>{peer.address}</b>
              <span>{peer.connected ? "connected" : "not connected"}</span>
              <span>leader {peer.observed_leader ?? "none"}</span>
            </div>
          )) ?? <div className="empty-row">offline</div>}
        </div>
      </section>
    </aside>
  );
}