mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-09 20:21:01 +00:00
Allow to pin nodes to top of the list (#48)
* Refactored persistent state a bit * Allow nodes to be pinned to top
This commit is contained in:
@@ -2,21 +2,21 @@ import * as React from 'react';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Chains, Chain, Ago, OfflineIndicator } from './components';
|
||||
import { Connection } from './Connection';
|
||||
import { Persistent } from './Persistent';
|
||||
import { PersistentObject, PersistentSet } from './persist';
|
||||
import { State } from './state';
|
||||
|
||||
import './App.css';
|
||||
|
||||
export default class App extends React.Component<{}, State> {
|
||||
public state: State;
|
||||
private connection: Promise<Connection>;
|
||||
private settings: Persistent<State.Settings>;
|
||||
private setSettings: Persistent<State.Settings>['set'];
|
||||
private readonly settings: PersistentObject<State.Settings>;
|
||||
private readonly pins: PersistentSet<Types.NodeId>;
|
||||
private readonly connection: Promise<Connection>;
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
this.settings = new Persistent(
|
||||
this.settings = new PersistentObject(
|
||||
'settings',
|
||||
{
|
||||
validator: true,
|
||||
@@ -34,7 +34,15 @@ export default class App extends React.Component<{}, State> {
|
||||
(settings) => this.setState({ settings })
|
||||
);
|
||||
|
||||
this.setSettings = this.settings.set.bind(this.settings);
|
||||
this.pins = new PersistentSet<Types.NodeId>('pinned', (pins) => {
|
||||
const { nodes } = this.state;
|
||||
|
||||
for (const node of nodes.values()) {
|
||||
node.pinned = pins.has(node.id);
|
||||
}
|
||||
|
||||
this.setState({ nodes, pins });
|
||||
});
|
||||
|
||||
this.state = {
|
||||
status: 'offline',
|
||||
@@ -45,10 +53,11 @@ export default class App extends React.Component<{}, State> {
|
||||
subscribed: null,
|
||||
chains: new Map(),
|
||||
nodes: new Map(),
|
||||
settings: this.settings.get(),
|
||||
settings: this.settings.raw(),
|
||||
pins: this.pins.get(),
|
||||
};
|
||||
|
||||
this.connection = Connection.create((changes) => {
|
||||
this.connection = Connection.create(this.pins, (changes) => {
|
||||
if (changes) {
|
||||
this.setState(changes);
|
||||
}
|
||||
@@ -75,7 +84,7 @@ export default class App extends React.Component<{}, State> {
|
||||
<div className="App">
|
||||
<OfflineIndicator status={status} />
|
||||
<Chains chains={chains} subscribed={subscribed} connection={this.connection} />
|
||||
<Chain appState={this.state} setSettings={this.setSettings} />
|
||||
<Chain appState={this.state} settings={this.settings} pins={this.pins} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
||||
import { State, Update } from './state';
|
||||
import { PersistentSet } from './persist';
|
||||
|
||||
const { Actions } = FeedMessage;
|
||||
|
||||
@@ -7,8 +8,8 @@ const TIMEOUT_BASE = (1000 * 5) as Types.Milliseconds; // 5 seconds
|
||||
const TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes
|
||||
|
||||
export class Connection {
|
||||
public static async create(update: Update): Promise<Connection> {
|
||||
return new Connection(await Connection.socket(), update);
|
||||
public static async create(pins: PersistentSet<Types.NodeId>, update: Update): Promise<Connection> {
|
||||
return new Connection(await Connection.socket(), update, pins);
|
||||
}
|
||||
|
||||
private static readonly address = window.location.protocol === 'https:'
|
||||
@@ -64,10 +65,12 @@ export class Connection {
|
||||
private socket: WebSocket;
|
||||
private state: Readonly<State>;
|
||||
private readonly update: Update;
|
||||
private readonly pins: PersistentSet<Types.NodeId>;
|
||||
|
||||
constructor(socket: WebSocket, update: Update) {
|
||||
constructor(socket: WebSocket, update: Update, pins: PersistentSet<Types.NodeId>) {
|
||||
this.socket = socket;
|
||||
this.update = update;
|
||||
this.pins = pins;
|
||||
this.bindSocket();
|
||||
}
|
||||
|
||||
@@ -169,7 +172,8 @@ export class Connection {
|
||||
|
||||
case Actions.AddedNode: {
|
||||
const [id, nodeDetails, nodeStats, blockDetails, location] = message.payload;
|
||||
const node = { id, nodeDetails, nodeStats, blockDetails, location };
|
||||
const pinned = this.pins.has(id);
|
||||
const node = { pinned, id, nodeDetails, nodeStats, blockDetails, location };
|
||||
|
||||
nodes.set(id, node);
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
.Chain-node-list th, .Chain-node-list td {
|
||||
text-align: left;
|
||||
padding: 0.5em 1em;
|
||||
padding: 0.35em 1em;
|
||||
}
|
||||
|
||||
.Chain-settings {
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as React from 'react';
|
||||
import { State as AppState } from '../../state';
|
||||
import { formatNumber, secondsWithPrecision, viewport } from '../../utils';
|
||||
import { Tab } from './';
|
||||
import { Tile, Node, Ago, Option } from '../';
|
||||
import { Tile, Node, Ago, Setting } from '../';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Persistent } from '../../Persistent';
|
||||
import { PersistentObject, PersistentSet } from '../../persist';
|
||||
|
||||
import blockIcon from '../../icons/package.svg';
|
||||
import blockTimeIcon from '../../icons/history.svg';
|
||||
@@ -24,7 +24,8 @@ export namespace Chain {
|
||||
|
||||
export interface Props {
|
||||
appState: Readonly<AppState>;
|
||||
setSettings: Persistent<AppState.Settings>['set'];
|
||||
settings: PersistentObject<AppState.Settings>;
|
||||
pins: PersistentSet<Types.NodeId>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -39,12 +40,16 @@ export namespace Chain {
|
||||
}
|
||||
|
||||
function sortNodes(a: AppState.Node, b: AppState.Node): number {
|
||||
if (a.blockDetails[0] === b.blockDetails[0]) {
|
||||
const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number;
|
||||
const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number;
|
||||
if (a.pinned === b.pinned) {
|
||||
if (a.blockDetails[0] === b.blockDetails[0]) {
|
||||
const aPropagation = a.blockDetails[4] == null ? Infinity : a.blockDetails[4] as number;
|
||||
const bPropagation = b.blockDetails[4] == null ? Infinity : b.blockDetails[4] as number;
|
||||
|
||||
// Ascending sort by propagation time
|
||||
return aPropagation - bPropagation;
|
||||
// Ascending sort by propagation time
|
||||
return aPropagation - bPropagation;
|
||||
}
|
||||
} else {
|
||||
return Number(b.pinned) - Number(a.pinned);
|
||||
}
|
||||
|
||||
// Descending sort by block number
|
||||
@@ -124,6 +129,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
|
||||
private renderList() {
|
||||
const { settings } = this.props.appState;
|
||||
const { pins } = this.props;
|
||||
|
||||
return (
|
||||
<table className="Chain-node-list">
|
||||
@@ -133,7 +139,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
this
|
||||
.nodes()
|
||||
.sort(sortNodes)
|
||||
.map((node) => <Node.Row key={node.id} node={node} settings={settings} />)
|
||||
.map((node) => <Node.Row key={node.id} node={node} settings={settings} pins={pins} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -164,7 +170,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
}
|
||||
|
||||
private renderSettings() {
|
||||
const { settings } = this.props.appState;
|
||||
const { settings } = this.props;
|
||||
|
||||
return (
|
||||
<div className="Chain-settings">
|
||||
@@ -177,17 +183,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checked = settings[setting];
|
||||
|
||||
const changeSetting = () => {
|
||||
const change = {};
|
||||
|
||||
change[setting] = !settings[setting];
|
||||
|
||||
this.props.setSettings(change);
|
||||
}
|
||||
|
||||
return <Option key={index} onClick={changeSetting} icon={icon} label={label} checked={checked} />;
|
||||
return <Setting key={index} setting={setting} settings={settings} icon={icon} label={label} />;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.Chains .Icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.Chains-chain {
|
||||
padding: 0 12px;
|
||||
background: #bbb;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Connection } from '../Connection';
|
||||
import { Icon } from './Icon';
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
|
||||
import chainIcon from '../icons/link.svg';
|
||||
import githubIcon from '../icons/mark-github.svg';
|
||||
import './Chains.css';
|
||||
|
||||
@@ -24,7 +23,6 @@ export class Chains extends React.Component<Chains.Props, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="Chains">
|
||||
<Icon src={chainIcon} alt="Observed Chain" />
|
||||
{
|
||||
this.chains.map((chain) => this.renderChain(chain))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import './Icon.css';
|
||||
|
||||
export interface Props {
|
||||
src: string;
|
||||
alt: string;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
@@ -12,8 +12,10 @@ export interface Props {
|
||||
export class Icon extends React.Component<{}, Props> {
|
||||
public props: Props;
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
return false;
|
||||
public shouldComponentUpdate(nextProps: Props) {
|
||||
return this.props.src !== nextProps.src
|
||||
|| this.props.alt !== nextProps.alt
|
||||
|| this.props.className !== nextProps.className;
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.Node-Row {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Node-Row-synced {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import Identicon from 'polkadot-identicon';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { formatNumber, trimHash, milliOrSecond, secondsWithPrecision } from '../../utils';
|
||||
import { State as AppState } from '../../state';
|
||||
import { PersistentSet } from '../../persist';
|
||||
import { SEMVER_PATTERN } from './';
|
||||
import { Ago, Icon } from '../';
|
||||
|
||||
import pinIcon from '../../icons/pin.svg';
|
||||
import pinOnIcon from '../../icons/check-square-solid.svg';
|
||||
import pinOffIcon from '../../icons/square-solid.svg';
|
||||
import nodeIcon from '../../icons/server.svg';
|
||||
import nodeValidatorIcon from '../../icons/shield.svg';
|
||||
import nodeTypeIcon from '../../icons/terminal.svg';
|
||||
@@ -23,6 +28,7 @@ import './Row.css';
|
||||
interface RowProps {
|
||||
node: AppState.Node;
|
||||
settings: AppState.Settings;
|
||||
pins: PersistentSet<Types.NodeId>;
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
@@ -39,6 +45,12 @@ interface Column {
|
||||
|
||||
export default class Row extends React.Component<RowProps, {}> {
|
||||
public static readonly columns: Column[] = [
|
||||
{
|
||||
label: 'Pin to Top',
|
||||
icon: pinIcon,
|
||||
width: 16,
|
||||
render: ({ pinned }) => <Icon src={pinned ? pinOnIcon : pinOffIcon} />
|
||||
},
|
||||
{
|
||||
label: 'Node',
|
||||
icon: nodeIcon,
|
||||
@@ -93,7 +105,7 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Memory use',
|
||||
label: 'Memory Use',
|
||||
icon: memoryIcon,
|
||||
width: 26,
|
||||
setting: 'mem',
|
||||
@@ -177,7 +189,7 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<tr className={className} onClick={this.toggle}>
|
||||
{
|
||||
Row.columns
|
||||
.filter(({ setting }) => setting == null || settings[setting])
|
||||
@@ -186,4 +198,14 @@ export default class Row extends React.Component<RowProps, {}> {
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
public toggle = () => {
|
||||
const { pins, node } = this.props;
|
||||
|
||||
if (node.pinned) {
|
||||
pins.delete(node.id)
|
||||
} else {
|
||||
pins.add(node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Icon } from './';
|
||||
|
||||
import './Option.css';
|
||||
|
||||
export namespace Option {
|
||||
export interface Props {
|
||||
icon: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export function Option(props: Option.Props): React.ReactElement<any> {
|
||||
const className = props.checked ? "Option Option-on" : "Option";
|
||||
|
||||
return (
|
||||
<p className={className} onClick={props.onClick}>
|
||||
<Icon src={props.icon} alt={props.label} />
|
||||
{props.label}
|
||||
<span className="Option-switch">
|
||||
<span className="Option-knob" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
+7
-7
@@ -1,19 +1,19 @@
|
||||
.Option {
|
||||
.Setting {
|
||||
color: #666;
|
||||
padding: 0;
|
||||
margin: 0 0 8px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Option-on {
|
||||
.Setting-on {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.Option .Icon {
|
||||
.Setting .Icon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.Option-switch {
|
||||
.Setting-switch {
|
||||
width: 40px;
|
||||
height: 18px;
|
||||
border-radius: 18px;
|
||||
@@ -24,12 +24,12 @@
|
||||
transition: background-color 0.15s linear, border-color 0.15s linear;
|
||||
}
|
||||
|
||||
.Option-on .Option-switch {
|
||||
.Setting-on .Setting-switch {
|
||||
background: #d64ca8;
|
||||
border-color: #d64ca8;
|
||||
}
|
||||
|
||||
.Option-knob {
|
||||
.Setting-knob {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #fff;
|
||||
@@ -42,6 +42,6 @@
|
||||
transition: left 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.Option-on .Option-knob {
|
||||
.Setting-on .Setting-knob {
|
||||
left: 22px;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { Icon } from './';
|
||||
import { State } from '../state';
|
||||
import { PersistentObject } from '../persist';
|
||||
|
||||
import './Setting.css';
|
||||
|
||||
export namespace Setting {
|
||||
export interface Props {
|
||||
icon: string;
|
||||
label: string;
|
||||
setting: keyof State.Settings;
|
||||
settings: PersistentObject<State.Settings>;
|
||||
}
|
||||
}
|
||||
|
||||
export class Setting extends React.Component<Setting.Props, {}> {
|
||||
public render() {
|
||||
const { icon, label, setting, settings } = this.props;
|
||||
|
||||
const checked = settings.get(setting);
|
||||
const className = checked ? "Setting Setting-on" : "Setting";
|
||||
|
||||
return (
|
||||
<p className={className} onClick={this.toggle}>
|
||||
<Icon src={icon} alt={label} />
|
||||
{label}
|
||||
<span className="Setting-switch">
|
||||
<span className="Setting-knob" />
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
private toggle = () => {
|
||||
const { setting, settings } = this.props;
|
||||
|
||||
settings.set(setting, !settings.get(setting));
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ export * from './Icon';
|
||||
export * from './Tile';
|
||||
export * from './Ago';
|
||||
export * from './OfflineIndicator';
|
||||
export * from './Option';
|
||||
export * from './Setting';
|
||||
|
||||
import * as Node from './Node';
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" data-prefix="far" data-icon="check-square" class="svg-inline--fa fa-check-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48 48 48h352c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zm0 400H48V80h352v352zm-35.864-241.724L191.547 361.48c-4.705 4.667-12.303 4.637-16.97-.068l-90.781-91.516c-4.667-4.705-4.637-12.303.069-16.971l22.719-22.536c4.705-4.667 12.303-4.637 16.97.069l59.792 60.277 141.352-140.216c4.705-4.667 12.303-4.637 16.97.068l22.536 22.718c4.667 4.706 4.637 12.304-.068 16.971z"></path></svg>
|
||||
|
After Width: | Height: | Size: 646 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" data-prefix="fas" data-icon="check-square" class="svg-inline--fa fa-check-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 480H48c-26.51 0-48-21.49-48-48V80c0-26.51 21.49-48 48-48h352c26.51 0 48 21.49 48 48v352c0 26.51-21.49 48-48 48zm-204.686-98.059l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.248-16.379-6.249-22.628 0L184 302.745l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.25 16.379 6.25 22.628.001z"></path></svg>
|
||||
|
After Width: | Height: | Size: 605 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" data-prefix="far" data-icon="circle" class="svg-inline--fa fa-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"></path></svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" data-prefix="fas" data-icon="circle" class="svg-inline--fa fa-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8z"></path></svg>
|
||||
|
After Width: | Height: | Size: 283 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" data-prefix="far" data-icon="square" class="svg-inline--fa fa-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-6 400H54c-3.3 0-6-2.7-6-6V86c0-3.3 2.7-6 6-6h340c3.3 0 6 2.7 6 6v340c0 3.3-2.7 6-6 6z"></path></svg>
|
||||
|
After Width: | Height: | Size: 406 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" data-prefix="fas" data-icon="square" class="svg-inline--fa fa-square fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"></path></svg>
|
||||
|
After Width: | Height: | Size: 319 B |
@@ -5,7 +5,7 @@ export class Persistent<Data> {
|
||||
private readonly key: string;
|
||||
private value: Data;
|
||||
|
||||
constructor(key: string, initial: Data, onChange: (value: Data) => void) {
|
||||
constructor(key: string, initial: Data, onChange: (value: Readonly<Data>) => void) {
|
||||
this.key = key;
|
||||
this.onChange = onChange;
|
||||
|
||||
@@ -26,12 +26,12 @@ export class Persistent<Data> {
|
||||
});
|
||||
}
|
||||
|
||||
public get(): Data {
|
||||
public get(): Readonly<Data> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public set<K extends keyof Data>(changes: Pick<Data, K> | Data) {
|
||||
this.value = Object.assign({}, this.value, changes);
|
||||
public set(value: Data) {
|
||||
this.value = value;
|
||||
window.localStorage.setItem(this.key, stringify(this.value) as any as string);
|
||||
this.onChange(this.value);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Persistent } from './';
|
||||
|
||||
export class PersistentObject<Data extends object> {
|
||||
private readonly inner: Persistent<Data>;
|
||||
|
||||
constructor(key: string, initial: Data, onChange: (value: Data) => void) {
|
||||
this.inner = new Persistent(key, initial, onChange);
|
||||
}
|
||||
|
||||
public raw(): Readonly<Data> {
|
||||
return this.inner.get();
|
||||
}
|
||||
|
||||
public get<K extends keyof Data>(key: K): Data[K] {
|
||||
return this.inner.get()[key];
|
||||
}
|
||||
|
||||
public set<K extends keyof Data>(key: K, value: Data[K]) {
|
||||
const data: Data = Object.assign({}, this.raw());
|
||||
data[key] = value;
|
||||
this.inner.set(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Persistent } from './';
|
||||
|
||||
export class PersistentSet<Item> {
|
||||
private readonly inner: Persistent<Item[]>;
|
||||
private value: Set<Item>;
|
||||
|
||||
constructor(key: string, onChange: (value: Set<Item>) => void) {
|
||||
this.inner = new Persistent(key, [], (raw: Readonly<Item[]>) => onChange(this.value = new Set(raw as Item[])));
|
||||
this.value = new Set(this.inner.get() as Item[]);
|
||||
}
|
||||
|
||||
public get(): Set<Item> {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public add(item: Item) {
|
||||
this.value.add(item);
|
||||
this.inner.set(Array.from(this.value));
|
||||
}
|
||||
|
||||
public delete(item: Item) {
|
||||
this.value.delete(item);
|
||||
this.inner.set(Array.from(this.value));
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.inner.set([]);
|
||||
}
|
||||
|
||||
public has(item: Item): boolean {
|
||||
return this.value.has(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './Persistent';
|
||||
export * from './PersistentObject';
|
||||
export * from './PersistentSet';
|
||||
@@ -2,6 +2,7 @@ import { Types, Maybe } from '@dotstats/common';
|
||||
|
||||
export namespace State {
|
||||
export interface Node {
|
||||
pinned: boolean,
|
||||
id: Types.NodeId;
|
||||
nodeDetails: Types.NodeDetails;
|
||||
nodeStats: Types.NodeStats;
|
||||
@@ -33,7 +34,8 @@ export interface State {
|
||||
subscribed: Maybe<Types.ChainLabel>;
|
||||
chains: Map<Types.ChainLabel, Types.NodeCount>;
|
||||
nodes: Map<Types.NodeId, State.Node>;
|
||||
settings: State.Settings;
|
||||
settings: Readonly<State.Settings>;
|
||||
pins: Readonly<Set<Types.NodeId>>;
|
||||
}
|
||||
|
||||
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
|
||||
|
||||
Reference in New Issue
Block a user