web/mnd: nodes domain, api and store
Change-Id: I022c5153dfc85a25eebce6e8ba91b97e906736cb
This commit is contained in:
parent
135d846aff
commit
9820145e14
@ -5,7 +5,7 @@
|
||||
"serve": "vue-cli-service serve",
|
||||
"lint": "vue-cli-service lint && stylelint '**/*.{vue,scss}' --fix",
|
||||
"build": "vue-cli-service build",
|
||||
"debug": "vue-cli-service build --mode development",
|
||||
"dev": "vue-cli-service build --mode development",
|
||||
"test": "vue-cli-service test:unit"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -23,7 +23,7 @@
|
||||
"@vue/cli-service": "4.5.9",
|
||||
"babel-core": "6.26.3",
|
||||
"core-js": "3.8.1",
|
||||
"node-sass": "4.14.1",
|
||||
"sass": "^1.32.0",
|
||||
"sass-loader": "8.0.0",
|
||||
"stylelint": "13.8.0",
|
||||
"stylelint-config-standard": "20.0.0",
|
||||
|
29
web/multinode/src/api/index.ts
Normal file
29
web/multinode/src/api/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* ErrorUnauthorized is a custom error type for performing unauthorized operations.
|
||||
*/
|
||||
export class UnauthorizedError extends Error {
|
||||
public constructor(message: string = 'authorization required') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BadRequestError is a custom error type for performing bad request.
|
||||
*/
|
||||
export class BadRequestError extends Error {
|
||||
public constructor(message: string = 'bad request') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* InternalError is a custom error type for internal server error.
|
||||
*/
|
||||
export class InternalError extends Error {
|
||||
public constructor(message: string = 'internal server error') {
|
||||
super(message);
|
||||
}
|
||||
}
|
206
web/multinode/src/api/nodes.ts
Normal file
206
web/multinode/src/api/nodes.ts
Normal file
@ -0,0 +1,206 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { BadRequestError, InternalError, UnauthorizedError } from '@/api/index';
|
||||
import { CreateNodeFields, Node, NodeURL } from '@/nodes';
|
||||
import { HttpClient } from '@/private/http/client';
|
||||
|
||||
/**
|
||||
* client for nodes controller of MND api.
|
||||
*/
|
||||
export class NodesClient {
|
||||
private readonly http: HttpClient = new HttpClient();
|
||||
private readonly ROOT_PATH: string = '/api/v0/nodes';
|
||||
|
||||
/**
|
||||
* handles node addition.
|
||||
*
|
||||
* @param node - node to add.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async add(node: CreateNodeFields): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}`;
|
||||
const response = await this.http.post(path, JSON.stringify(node));
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns list of node infos.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async list(): Promise<Node[]> {
|
||||
const path = `${this.ROOT_PATH}/infos`;
|
||||
const response = await this.http.get(path);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
|
||||
const nodeListJson = await response.json();
|
||||
|
||||
return nodeListJson.map(node => new Node(
|
||||
node.id,
|
||||
node.name,
|
||||
node.version,
|
||||
new Date(node.lastContact),
|
||||
node.diskSpaceUsed,
|
||||
node.diskSpaceLeft,
|
||||
node.bandwidthUsed,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
node.totalEarned,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* returns list of node infos by satellite.
|
||||
*
|
||||
* @param satelliteId - id of the satellite.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async listBySatellite(satelliteId: string): Promise<Node[]> {
|
||||
const path = `${this.ROOT_PATH}/infos/${satelliteId}`;
|
||||
const response = await this.http.get(path);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
|
||||
const nodeListJson = await response.json();
|
||||
|
||||
return nodeListJson.map(node => new Node(
|
||||
node.id,
|
||||
node.name,
|
||||
node.version,
|
||||
new Date(node.lastContact),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
node.onlineScore,
|
||||
node.auditScore,
|
||||
node.suspensionScore,
|
||||
node.totalEarned,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* updates nodes name.
|
||||
*
|
||||
* @param id - id of the node.
|
||||
* @param name - new node name.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async updateName(id: string, name: string): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/${id}`;
|
||||
const response = await this.http.patch(path, JSON.stringify({name: name}));
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes node.
|
||||
*
|
||||
* @param id - id of the node.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/${id}`;
|
||||
const response = await this.http.delete(path);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves list of trusted satellites node urls for a node.
|
||||
*/
|
||||
public async trustedSatellites(): Promise<NodeURL[]> {
|
||||
const path = `${this.ROOT_PATH}/trusted-satellites`;
|
||||
const response = await this.http.get(path);
|
||||
|
||||
if (!response.ok) {
|
||||
await this.handleError(response);
|
||||
}
|
||||
|
||||
const urlListJson = await response.json();
|
||||
|
||||
return urlListJson.map(url => new NodeURL(
|
||||
url.ID,
|
||||
url.Name,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* handles error due to response code.
|
||||
* @param response - response from server.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid ISBN number.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the ISBN number is valid, but no such book exists in the catalog.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if the ISBN number is valid, but no such book exists in the catalog.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async handleError(response: Response): Promise<void> {
|
||||
const body = await response.json();
|
||||
|
||||
switch (response.status) {
|
||||
case 401: throw new UnauthorizedError(body.error);
|
||||
case 400: throw new BadRequestError(body.error);
|
||||
case 500:
|
||||
default:
|
||||
throw new InternalError(body.error);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,11 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex, { ModuleTree, Store, StoreOptions } from 'vuex';
|
||||
|
||||
import { NodesClient } from '@/api/nodes';
|
||||
import { NodesModule, NodesState } from '@/app/store/nodes';
|
||||
import { Nodes } from '@/nodes/service';
|
||||
|
||||
Vue.use(Vuex); // TODO: place to main.ts when initialization of everything will be there.
|
||||
Vue.use(Vuex);
|
||||
|
||||
/**
|
||||
* RootState is a representation of global state.
|
||||
@ -18,7 +20,7 @@ export class RootState {
|
||||
/**
|
||||
* MultinodeStoreOptions contains all needed data for store creation.
|
||||
*/
|
||||
class MultinodeStoreOptions implements StoreOptions<RootState> {
|
||||
export class MultinodeStoreOptions implements StoreOptions<RootState> {
|
||||
public readonly strict: boolean;
|
||||
public readonly state: RootState;
|
||||
public readonly modules: ModuleTree<RootState>;
|
||||
@ -34,6 +36,14 @@ class MultinodeStoreOptions implements StoreOptions<RootState> {
|
||||
}
|
||||
}
|
||||
|
||||
// Services
|
||||
const nodesClient: NodesClient = new NodesClient();
|
||||
const nodesService: Nodes = new Nodes(nodesClient);
|
||||
|
||||
// Modules
|
||||
const nodesModule: NodesModule = new NodesModule(nodesService);
|
||||
|
||||
// Store
|
||||
export const store: Store<RootState> = new Vuex.Store<RootState>(
|
||||
new MultinodeStoreOptions(new NodesModule()),
|
||||
new MultinodeStoreOptions(nodesModule),
|
||||
);
|
||||
|
@ -4,13 +4,16 @@
|
||||
import { ActionContext, ActionTree, GetterTree, Module, MutationTree } from 'vuex';
|
||||
|
||||
import { RootState } from '@/app/store/index';
|
||||
import { Node } from '@/nodes';
|
||||
import { CreateNodeFields, Node, NodeURL } from '@/nodes';
|
||||
import { Nodes } from '@/nodes/service';
|
||||
|
||||
/**
|
||||
* NodesState is a representation of nodes module state.
|
||||
*/
|
||||
export class NodesState {
|
||||
public nodes: Node[] = [];
|
||||
public selectedSatellite: NodeURL | null = null;
|
||||
public trustedSatellites: NodeURL[] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,17 +26,23 @@ export class NodesModule implements Module<NodesState, RootState> {
|
||||
public readonly actions: ActionTree<NodesState, RootState>;
|
||||
public readonly mutations: MutationTree<NodesState>;
|
||||
|
||||
public constructor() { // here should be services, apis, 3d party dependencies.
|
||||
private readonly nodes: Nodes;
|
||||
|
||||
public constructor(nodes: Nodes) {
|
||||
this.nodes = nodes;
|
||||
|
||||
this.namespaced = true;
|
||||
|
||||
this.state = new NodesState();
|
||||
|
||||
this.mutations = {
|
||||
populate: this.populate,
|
||||
saveTrustedSatellites: this.saveTrustedSatellites,
|
||||
setSelectedSatellite: this.setSelectedSatellite,
|
||||
};
|
||||
|
||||
this.actions = {
|
||||
fetch: this.fetch,
|
||||
fetch: this.fetch.bind(this),
|
||||
add: this.add.bind(this),
|
||||
trustedSatellites: this.trustedSatellites.bind(this),
|
||||
selectSatellite: this.selectSatellite.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
@ -47,10 +56,60 @@ export class NodesModule implements Module<NodesState, RootState> {
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch action loads all nodes.
|
||||
* saveTrustedSatellites mutation will save new list of trusted satellites to store.
|
||||
* @param state
|
||||
* @param trustedSatellites
|
||||
*/
|
||||
public saveTrustedSatellites(state: NodesState, trustedSatellites: NodeURL[]) {
|
||||
state.trustedSatellites = trustedSatellites;
|
||||
}
|
||||
|
||||
/**
|
||||
* setSelectedSatellite mutation will selected satellite to store.
|
||||
* @param state
|
||||
* @param satelliteId - id of the satellite to select.
|
||||
*/
|
||||
public setSelectedSatellite(state: NodesState, satelliteId: string) {
|
||||
state.selectedSatellite = state.trustedSatellites.find((satellite: NodeURL) => satellite.id === satelliteId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch action loads all nodes information.
|
||||
* @param ctx - context of the Vuex action.
|
||||
*/
|
||||
public async fetch(ctx: ActionContext<NodesState, RootState>): Promise<void> {
|
||||
await new Promise(() => null);
|
||||
const nodes = ctx.state.selectedSatellite ? await this.nodes.listBySatellite(ctx.state.selectedSatellite.id) : await this.nodes.list();
|
||||
ctx.commit('populate', nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds node to multinode list.
|
||||
* @param ctx - context of the Vuex action.
|
||||
* @param node - to add.
|
||||
*/
|
||||
public async add(ctx: ActionContext<NodesState, RootState>, node: CreateNodeFields): Promise<void> {
|
||||
await this.nodes.add(node);
|
||||
await this.fetch(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves list of trusted satellites node urls for a node.
|
||||
* @param ctx - context of the Vuex action.
|
||||
*/
|
||||
public async trustedSatellites(ctx: ActionContext<NodesState, RootState>): Promise<void> {
|
||||
const satellites: NodeURL[] = await this.nodes.trustedSatellites();
|
||||
|
||||
ctx.commit('saveTrustedSatellites', satellites);
|
||||
}
|
||||
|
||||
/**
|
||||
* save satellite as selected satellite.
|
||||
* @param ctx - context of the Vuex action.
|
||||
* @param satelliteId - satellite id to select.
|
||||
*/
|
||||
public async selectSatellite(ctx: ActionContext<NodesState, RootState>, satelliteId: string): Promise<void> {
|
||||
ctx.commit('setSelectedSatellite', satelliteId);
|
||||
|
||||
await this.fetch(ctx);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,6 @@
|
||||
// Copyright (C) 2020 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
/**
|
||||
* NodeToAdd is a representation of storagenode, that SNO could add to the Multinode Dashboard.
|
||||
*/
|
||||
export class NodeToAdd {
|
||||
public id: string; // TODO: create ts analog of storj.NodeID;
|
||||
/**
|
||||
* apiSecret is a secret issued by storagenode, that will be main auth mechanism in MND <-> SNO api.
|
||||
*/
|
||||
public apiSecret: string; // TODO: change to Uint8Array[];
|
||||
public publicAddress: string;
|
||||
public name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes node online statuses.
|
||||
*/
|
||||
@ -22,19 +9,54 @@ export enum NodeStatus {
|
||||
Offline = 'offline',
|
||||
}
|
||||
|
||||
// TODO: refactor this
|
||||
/**
|
||||
* Node holds all information of node for the Multinode Dashboard.
|
||||
* NodeInfo contains basic node internal state.
|
||||
*/
|
||||
export class Node {
|
||||
public status: NodeStatus = NodeStatus.Offline;
|
||||
private readonly STATUS_TRESHHOLD_MILISECONDS: number = 10.8e6;
|
||||
|
||||
public constructor(
|
||||
public id: string,
|
||||
public name: string,
|
||||
public version: string,
|
||||
public lastContact: Date,
|
||||
public diskSpaceUsed: number,
|
||||
public diskSpaceLeft: number,
|
||||
public bandwidthUsed: number,
|
||||
public onlineScore: number,
|
||||
public auditScore: number,
|
||||
public suspensionScore: number,
|
||||
public earned: number,
|
||||
) {
|
||||
const now = new Date();
|
||||
if (now.getTime() - this.lastContact.getTime() < this.STATUS_TRESHHOLD_MILISECONDS) {
|
||||
this.status = NodeStatus.Online;
|
||||
}
|
||||
}
|
||||
|
||||
public get displayedName(): string {
|
||||
return this.name || this.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CreateNodeFields is a representation of storagenode, that SNO could add to the Multinode Dashboard.
|
||||
*/
|
||||
export class CreateNodeFields {
|
||||
public constructor(
|
||||
public id: string = '',
|
||||
public name: string = '',
|
||||
public diskSpaceUsed: number = 0,
|
||||
public diskSpaceLeft: number = 0,
|
||||
public bandwidthUsed: number = 0,
|
||||
public earned: number = 0,
|
||||
public version: string = '',
|
||||
public status: NodeStatus = NodeStatus.Offline,
|
||||
public apiSecret: string = '',
|
||||
public publicAddress: string = '',
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* NodeURL defines a structure for connecting to a node.
|
||||
*/
|
||||
export class NodeURL {
|
||||
public constructor(
|
||||
public id: string,
|
||||
public address: string,
|
||||
) {}
|
||||
}
|
||||
|
112
web/multinode/src/nodes/service.ts
Normal file
112
web/multinode/src/nodes/service.ts
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { NodesClient } from '@/api/nodes';
|
||||
import { CreateNodeFields, Node, NodeURL } from '@/nodes/index';
|
||||
|
||||
/**
|
||||
* exposes all nodes related logic
|
||||
*/
|
||||
export class Nodes {
|
||||
private readonly nodes: NodesClient;
|
||||
|
||||
public constructor(nodes: NodesClient) {
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* handles node addition.
|
||||
*
|
||||
* @param node - node to add.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async add(node: CreateNodeFields): Promise<void> {
|
||||
await this.nodes.add(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns list of node infos.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async list(): Promise<Node[]> {
|
||||
return await this.nodes.list();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns list of node infos by satellite.
|
||||
*
|
||||
* @param satelliteId - id of the satellite.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async listBySatellite(satelliteId: string): Promise<Node[]> {
|
||||
return await this.nodes.listBySatellite(satelliteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* updates nodes name.
|
||||
*
|
||||
* @param id - id of the node.
|
||||
* @param name - new node name.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async updateName(id: string, name: string): Promise<void> {
|
||||
await this.nodes.updateName(id, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes node.
|
||||
*
|
||||
* @param id - id of the node.
|
||||
*
|
||||
* @throws {@link BadRequestError}
|
||||
* This exception is thrown if the input is not a valid.
|
||||
*
|
||||
* @throws {@link UnauthorizedError}
|
||||
* Thrown if the auth cookie is missing or invalid.
|
||||
*
|
||||
* @throws {@link InternalError}
|
||||
* Thrown if something goes wrong on server side.
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
await this.nodes.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves list of trusted satellites node urls for a node.
|
||||
*/
|
||||
public async trustedSatellites(): Promise<NodeURL[]> {
|
||||
return await this.nodes.trustedSatellites();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user