web/mnd: nodes domain, api and store

Change-Id: I022c5153dfc85a25eebce6e8ba91b97e906736cb
This commit is contained in:
crawter 2021-01-21 19:04:42 +02:00 committed by Yehor Butko
parent 135d846aff
commit 9820145e14
7 changed files with 473 additions and 35 deletions

View File

@ -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",

View 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);
}
}

View 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);
}
}
}

View File

@ -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),
);

View File

@ -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);
}
}

View File

@ -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,
) {}
}

View 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();
}
}