diff --git a/CHANGELOG.md b/CHANGELOG.md index 12716910..65639372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,3 +17,4 @@ Bob Management GUI changelog - Node list page, backend (#19) - Home page, frontend (#22) - Node list page, frontend (#23) +- VDisk list page, backend (#20) diff --git a/api/openapi.yaml b/api/openapi.yaml index 1d74e2d1..2e4a9134 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -240,6 +240,65 @@ paths: description: Node Not Found security: - api_key: [] + /api/v1/vdisks/list: + get: + tags: + - services::api + summary: Returns simple list of all known vdisks + description: |- + Returns simple list of all known vdisks + + # Errors + + This function will return an error if a call to the primary node will fail + operationId: get_vdisks_list + responses: + '200': + description: Simple Node List + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/dto.VDisk' + '401': + description: Unauthorized + security: + - api_key: [] + /api/v1/vdisks/{vdisk_id}: + get: + tags: + - services::api + summary: Returns vdisk inforamtion by their id + description: |- + Returns vdisk inforamtion by their id + + # Errors + + This function will return an error if a call to the main node will fail or vdisk with + specified id not found + operationId: get_vdisk_info + parameters: + - name: vdisk_id + in: path + required: true + schema: + type: integer + format: int64 + minimum: 0 + responses: + '200': + description: VDisk Inforamtion + content: + application/json: + schema: + $ref: '#/components/schemas/VDisk' + '401': + description: Unauthorized + '404': + description: VDisk not found + security: + - api_key: [] components: schemas: BobConnectionData: @@ -310,26 +369,26 @@ components: type: object description: Disk count by their status required: - - good - - bad - - offline + - Good + - Bad + - Offline properties: - bad: + Bad: type: integer format: int64 minimum: 0 - good: + Good: type: integer format: int64 minimum: 0 - offline: + Offline: type: integer format: int64 minimum: 0 example: - bad: 0 - good: 0 - offline: 0 + Bad: 0 + Good: 0 + Offline: 0 DiskProblem: type: string description: Defines kind of problem on disk @@ -344,7 +403,7 @@ components: status: type: string enum: - - good + - Good - type: object required: - status @@ -362,7 +421,7 @@ components: status: type: string enum: - - bad + - Bad - type: object required: - status @@ -370,7 +429,7 @@ components: status: type: string enum: - - offline + - Offline description: |- Defines disk status @@ -382,9 +441,9 @@ components: type: string description: Defines disk status names enum: - - good - - bad - - offline + - Good + - Bad + - Offline Hostname: type: string MetricsEntryModel: @@ -423,26 +482,26 @@ components: type: object description: Node count by their status required: - - good - - bad - - offline + - Good + - Bad + - Offline properties: - bad: + Bad: type: integer format: int64 minimum: 0 - good: + Good: type: integer format: int64 minimum: 0 - offline: + Offline: type: integer format: int64 minimum: 0 example: - bad: 0 - good: 0 - offline: 0 + Bad: 0 + Good: 0 + Offline: 0 NodeInfo: type: object required: @@ -497,7 +556,7 @@ components: status: type: string enum: - - good + - Good - type: object required: - status @@ -515,7 +574,7 @@ components: status: type: string enum: - - bad + - Bad - type: object required: - status @@ -523,7 +582,7 @@ components: status: type: string enum: - - offline + - Offline description: |- Defines status of node @@ -536,9 +595,9 @@ components: type: string description: Defines node status names enum: - - good - - bad - - offline + - Good + - Bad + - Offline Operation: type: string description: Types of operations on BOB cluster @@ -629,7 +688,7 @@ components: status: type: string enum: - - good + - Good - type: object required: - status @@ -647,7 +706,7 @@ components: status: type: string enum: - - offline + - Offline description: |- Replica status. It's either good or offline with the reasons why it is offline @@ -1021,36 +1080,16 @@ components: status: $ref: '#/components/schemas/VDiskStatus' VDiskStatus: - oneOf: - - type: object - required: - - status - properties: - status: - type: string - enum: - - good - - type: object - required: - - status - properties: - status: - type: string - enum: - - bad - - type: object - required: - - status - properties: - status: - type: string - enum: - - offline + type: string description: |- Virtual disk status. Variants - Virtual Disk status status == 'bad' when at least one of its replicas has problems + enum: + - Good + - Bad + - Offline dto.Node: type: object required: diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 21fb1523..59a828b2 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -45,6 +45,8 @@ impl Modify for SecurityAddon { services::api::raw_configuration_by_node, services::api::get_node_info, services::api::get_nodes_list, + services::api::get_vdisk_info, + services::api::get_vdisks_list, ), components( schemas(models::shared::Credentials, models::shared::Hostname, models::shared::BobConnectionData, diff --git a/backend/src/models/api.rs b/backend/src/models/api.rs index a2eb1a33..33517d40 100644 --- a/backend/src/models/api.rs +++ b/backend/src/models/api.rs @@ -51,11 +51,8 @@ pub enum DiskProblem { #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] #[tsync] pub enum DiskStatus { - #[serde(rename = "good")] Good, - #[serde(rename = "bad")] Bad { problems: Vec }, - #[serde(rename = "offline")] Offline, } @@ -82,7 +79,6 @@ impl DiskStatus { /// Defines disk status names #[derive(Debug, Clone, Eq, PartialEq, Serialize, Hash, EnumIter)] -#[serde(rename_all = "camelCase")] #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] #[tsync] pub enum DiskStatusName { @@ -190,11 +186,8 @@ impl NodeProblem { #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] #[tsync] pub enum NodeStatus { - #[serde(rename = "good")] Good, - #[serde(rename = "bad")] Bad { problems: Vec }, - #[serde(rename = "offline")] Offline, } @@ -227,7 +220,6 @@ impl TypedMetrics { /// Defines node status names #[derive(Debug, Clone, Eq, PartialEq, Serialize, Hash, EnumIter)] -#[serde(rename_all = "camelCase")] #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] #[tsync] pub enum NodeStatusName { @@ -273,9 +265,7 @@ pub enum ReplicaProblem { #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] #[tsync] pub enum ReplicaStatus { - #[serde(rename = "good")] Good, - #[serde(rename = "offline")] Offline { problems: Vec }, } @@ -330,6 +320,7 @@ impl Add for SpaceInfo { /// Virtual disk Component #[derive(Debug, Clone, Eq, PartialEq, Serialize)] #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] +#[tsync] pub struct VDisk { pub id: u64, @@ -347,17 +338,14 @@ pub struct VDisk { /// Variants - Virtual Disk status /// status == 'bad' when at least one of its replicas has problems #[derive(Debug, Clone, Eq, PartialEq, Serialize)] -#[serde(tag = "status")] +// #[serde(tag = "status")] #[cfg_attr(all(feature = "swagger", debug_assertions), derive(ToSchema))] #[tsync] // #[cfg_attr(all(feature = "swagger", debug_assertions), // schema(example = json!({"status": "good"})))] pub enum VDiskStatus { - #[serde(rename = "good")] Good, - #[serde(rename = "bad")] Bad, - #[serde(rename = "offline")] Offline, } diff --git a/backend/src/services/api.rs b/backend/src/services/api.rs index fee57f4d..b684fdf4 100644 --- a/backend/src/services/api.rs +++ b/backend/src/services/api.rs @@ -1,4 +1,8 @@ -use super::prelude::*; +use super::{auth::HttpClient, prelude::*}; + +// TODO: For methods, that requires information from all nodes (/disks/count, /nodes/rps, etc.), +// think of better method of returning info +// another thread that constantly updates info in period and cache the results? // TODO: For methods, that requires information from all nodes (/disks/count, /nodes/rps, etc.), // think of better method of returning info @@ -458,3 +462,27 @@ pub async fn raw_configuration_by_node( .await?, )) } + +async fn get_client_by_node( + client: &HttpBobClient, + node_name: NodeName, +) -> AxumResult> { + let nodes = fetch_nodes(client.api_main()).await?; + + let node = nodes + .iter() + .find(|node| node.name == node_name) + .ok_or_else(|| { + tracing::error!("Couldn't find specified node"); + APIError::RequestFailed + })?; + + client + .cluster_with_addr() + .get(&node.name) + .ok_or_else(|| { + tracing::error!("Couldn't find specified node"); + APIError::RequestFailed.into() + }) + .cloned() +} diff --git a/backend/src/services/mod.rs b/backend/src/services/mod.rs index 7ac21ae8..659220fe 100644 --- a/backend/src/services/mod.rs +++ b/backend/src/services/mod.rs @@ -32,6 +32,8 @@ use api::{ use auth::{login, logout, require_auth, AuthState, BobUser, HttpBobClient, InMemorySessionStore}; use prelude::*; +use self::api::{get_vdisk_info, get_vdisks_list}; + type BobAuthState = AuthState< BobUser, Uuid, @@ -54,6 +56,8 @@ pub fn api_router_v1(auth_state: BobAuthState) -> Result, R .api_route("/nodes/space", &Method::GET, get_space) .api_route("/nodes/list", &Method::GET, get_nodes_list) .api_route("/nodes/:node_name", &Method::GET, get_node_info) + .api_route("/vdisks/list", &Method::GET, get_vdisks_list) + .api_route("/vdisks/:vdisk_id", &Method::GET, get_vdisk_info) .api_route( "/nodes/:node_name/metrics", &Method::GET, diff --git a/frontend/src/components/VDiskList/VDiskList.tsx b/frontend/src/components/VDiskList/VDiskList.tsx new file mode 100644 index 00000000..64d163bf --- /dev/null +++ b/frontend/src/components/VDiskList/VDiskList.tsx @@ -0,0 +1,114 @@ +import { Context } from '@appTypes/context.ts'; +import defaultTheme from '@layouts/DefaultTheme.ts'; +import { Box, ThemeProvider } from '@mui/system'; +import { useStore } from '@nanostores/react'; +import axios from 'axios'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import FetchingBackdrop from '../backdrop/backdrop.tsx'; +import VDiskTable from '../VDiskTable/VDiskTable.tsx'; + +const stubVDisk: VDisk = { + id: 0, + status: 'Offline', + partition_count: 0, + replicas: [], +}; + +axios.defaults.withCredentials = true; + +const VDiskPage = () => { + const [vdisks, setVdisks] = useState([]); + const [vdiskList, setVdiskList] = useState([]); + const [isPageLoaded, setIsPageLoaded] = useState(false); + const context = useStore(Context); + + const fetchVdiskList = useMemo( + () => async () => { + try { + const [res] = await Promise.all([axios.get('/api/v1/vdisks/list')]); + setVdisks( + res.data + .map((dtoVdisk: DTOVDisk) => { + return { + ...stubVDisk, + id: dtoVdisk.id, + } as VDisk; + }) + .sort((a, b) => (a.id < b.id ? -1 : 1)), + ); + setVdiskList(res.data); + } catch (err) { + console.log(err); + } + }, + [], + ); + + const fetchVdisk = useCallback( + (vdisk: number) => async () => { + try { + const [res] = await Promise.all([axios.get('/api/v1/vdisks/' + vdisk)]); + return res.data; + } catch (err) { + console.log(err); + } + }, + [], + ); + useEffect(() => { + fetchVdiskList(); + }, [fetchVdiskList]); + + useEffect(() => { + const fetchNodes = async () => { + const res = ( + await Promise.all( + vdiskList.map(async (vdisk) => { + return fetchVdisk(vdisk.id)() + .catch(console.error) + .then((resultVdisk) => resultVdisk); + }), + ) + ).filter((vdisk): vdisk is VDisk => { + return typeof vdisk !== undefined; + }); + setVdisks(res.concat(vdisks.filter((item) => !res.find((n) => (n?.id || '') == item.id)))); + }; + if (!isPageLoaded && vdiskList.length !== 0) { + fetchNodes(); + setIsPageLoaded(true); + } + const interval = setInterval(() => { + fetchNodes(); + }, context.refreshTime * 1000); + + return () => clearInterval(interval); + }, [isPageLoaded, fetchVdisk, context.enabled, context.refreshTime, vdiskList, vdisks]); + + if (!isPageLoaded) { + return ; + } + return ( + + + + + + ); +}; + +export default VDiskPage; diff --git a/frontend/src/components/VDiskTable/VDiskTable.module.css b/frontend/src/components/VDiskTable/VDiskTable.module.css new file mode 100644 index 00000000..6a01bc59 --- /dev/null +++ b/frontend/src/components/VDiskTable/VDiskTable.module.css @@ -0,0 +1,27 @@ +.greendot { + height: 16px; + width: 16px; + background-color: #34b663; + border-radius: 50%; + display: inline-block; +} + +.reddot { + height: 16px; + width: 16px; + background-color: #c3234b; + border-radius: 50%; + display: inline-block; +} + +.graydot { + height: 16px; + width: 16px; + background-color: #7b817e; + border-radius: 50%; + display: inline-block; +} + +.greyHeader { + background-color: #35373c; +} diff --git a/frontend/src/components/VDiskTable/VDiskTable.tsx b/frontend/src/components/VDiskTable/VDiskTable.tsx new file mode 100644 index 00000000..4e4d7472 --- /dev/null +++ b/frontend/src/components/VDiskTable/VDiskTable.tsx @@ -0,0 +1,145 @@ +import { Box } from '@mui/system'; +import { + DataGrid, + type GridColDef, + type GridRenderCellParams, + GridToolbar, + type GridValidRowModel, +} from '@mui/x-data-grid'; +import React from 'react'; + +import style from './VDiskTable.module.css'; + +const BarLabelColor: Record = { + Good: style.greendot, + Bad: style.graydot, + Offline: style.reddot, +}; + +const columns: GridColDef[] = [ + { + field: 'vdiskid', + headerName: 'VDisk Number', + flex: 1, + align: 'center', + headerAlign: 'center', + headerClassName: style.greyHeader, + }, + { + field: 'replicas', + headerName: 'Replicas on nodes', + flex: 3, + align: 'center', + headerAlign: 'center', + headerClassName: style.greyHeader, + renderCell: (params: GridRenderCellParams) => { + return ( + + {params.value?.map((replica) => replica.node).join(', ') || ''} + + ); + }, + }, + { + field: 'availability', + headerName: 'Availability', + flex: 1, + align: 'center', + headerAlign: 'center', + headerClassName: style.greyHeader, + renderCell: (params: GridRenderCellParams) => { + return ( + + + {params.value?.goodReplicas || 0} / {params.value?.totalReplicas || 0} + + + ); + }, + }, + { + field: 'status', + headerName: 'Status', + flex: 1, + align: 'left', + headerAlign: 'center', + headerClassName: style.greyHeader, + renderCell: (params: GridRenderCellParams) => { + const status = params.value || 'Offline'; + return ( + + + {params.value} + + ); + }, + }, +]; +const VDiskTable = ({ vdisks }: { vdisks: VDisk[] }) => { + const data = vdisks.sort() + ? vdisks.map((vdisk, i) => { + return { + id: i, + vdiskid: vdisk.id, + replicas: vdisk.replicas, + availability: { + goodReplicas: vdisk.replicas.filter((replica) => replica.status.status === 'Good').length, + totalReplicas: vdisk.replicas.length, + }, + status: vdisk.status, + } as VDiskTableCols; + }) + : []; + return ( + searchInput.split(',').map((value) => value.trim()), + }, + }, + }} + /> + ); +}; + +export default VDiskTable; diff --git a/frontend/src/components/diskState/diskState.tsx b/frontend/src/components/diskState/diskState.tsx index ba3b4140..000e9192 100644 --- a/frontend/src/components/diskState/diskState.tsx +++ b/frontend/src/components/diskState/diskState.tsx @@ -4,19 +4,19 @@ import React from 'react'; import style from './diskState.module.css'; const BarColor: Record = { - good: '#5EB36B', - bad: '#7C817E', - offline: '#B3344D', + Good: '#5EB36B', + Bad: '#7C817E', + Offline: '#B3344D', }; const BarLabelColor: Record = { - good: style.totalGoodDisksLabel, - bad: style.totalBadDisksLabel, - offline: style.totalOfflineDisksLabel, + Good: style.totalGoodDisksLabel, + Bad: style.totalBadDisksLabel, + Offline: style.totalOfflineDisksLabel, }; const DiskState = ({ diskCount, status }: { diskCount: Record; status: DiskStatusName }) => { - const total = diskCount.good + diskCount.bad + diskCount.offline; + const total = diskCount.Good + diskCount.Bad + diskCount.Offline; const percent = Math.floor((diskCount[status] / total) * 100) || 0; return (

State of the physical disks in the cluster

- - - + + +
); }; diff --git a/frontend/src/components/nodeTable/nodeTable.tsx b/frontend/src/components/nodeTable/nodeTable.tsx index c8609f86..36642a76 100644 --- a/frontend/src/components/nodeTable/nodeTable.tsx +++ b/frontend/src/components/nodeTable/nodeTable.tsx @@ -11,9 +11,9 @@ import style from './nodeTable.module.css'; axios.defaults.withCredentials = true; const DotMap: Record = { - good: style.greendot, - bad: style.graydot, - offline: style.reddot, + Good: style.greendot, + Bad: style.graydot, + Offline: style.reddot, }; const defaultRps: RPS = { @@ -68,7 +68,7 @@ const columns: GridColDef[] = [ headerAlign: 'center', headerClassName: style.greyHeader, renderCell: (params: GridRenderCellParams) => { - const status = params.value || 'offline'; + const status = params.value || 'Offline'; return ( { id: i, nodename: node.name, hostname: node.hostname, - status: node.status.status.toLowerCase(), + status: node.status.status, space: node.space, rps: node.rps, aliens: node.alienCount || 0, diff --git a/frontend/src/components/totalNodes/totalNodes.tsx b/frontend/src/components/totalNodes/totalNodes.tsx index 6b6ad0f9..c9ddbbe5 100644 --- a/frontend/src/components/totalNodes/totalNodes.tsx +++ b/frontend/src/components/totalNodes/totalNodes.tsx @@ -4,19 +4,19 @@ import React from 'react'; import style from './totalNodes.module.css'; const NodeColor: Record = { - good: '#5EB36B', - bad: '#7C817E', - offline: '#B3344D', + Good: '#5EB36B', + Bad: '#7C817E', + Offline: '#B3344D', }; const NodeLabelColor: Record = { - good: style.totalGoodNodesLabel, - bad: style.totalBadNodesLabel, - offline: style.totalOfflineNodesLabel, + Good: style.totalGoodNodesLabel, + Bad: style.totalBadNodesLabel, + Offline: style.totalOfflineNodesLabel, }; const NodeState = ({ nodeCount, status }: { nodeCount: Record; status: NodeStatusName }) => { - const total = nodeCount.good + nodeCount.bad + nodeCount.offline; + const total = nodeCount.Good + nodeCount.Bad + nodeCount.Offline; const percent = Math.floor((nodeCount[status] / total) * 100) || 0; return ( }} >

State of the nodes in the cluster

- - - + + +
); }; diff --git a/frontend/src/pages/vdisklist/index.astro b/frontend/src/pages/vdisklist/index.astro new file mode 100644 index 00000000..6d07c6a6 --- /dev/null +++ b/frontend/src/pages/vdisklist/index.astro @@ -0,0 +1,10 @@ +--- +import Layout from '@layouts/Layout.astro'; +import VDiskPage from '@components/VDiskList/VDiskList.tsx'; +--- + + +
+ +
+
diff --git a/frontend/src/pages/vdiskslist/page.module.css b/frontend/src/pages/vdisklist/page.module.css similarity index 100% rename from frontend/src/pages/vdiskslist/page.module.css rename to frontend/src/pages/vdisklist/page.module.css diff --git a/frontend/src/pages/vdiskslist/index.astro b/frontend/src/pages/vdiskslist/index.astro deleted file mode 100644 index 8309eb95..00000000 --- a/frontend/src/pages/vdiskslist/index.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import Layout from '@layouts/Layout.astro'; ---- - - diff --git a/frontend/src/types/data.d.ts b/frontend/src/types/data.d.ts index 07e37272..b04db5b7 100644 --- a/frontend/src/types/data.d.ts +++ b/frontend/src/types/data.d.ts @@ -22,3 +22,16 @@ interface NodeTableCols { aliens?: number; corruptedBlobs?: number; } + +interface ReplicaCount { + goodReplicas: number; + totalReplicas: number; +} + +interface VDiskTableCols { + id: number; + vdiskid: number; + replicas: Replica[]; + availability: ReplicaCount; + status: VDiskStatus; +} diff --git a/frontend/src/types/rust.d.ts b/frontend/src/types/rust.d.ts index 294e15d9..07e8a814 100644 --- a/frontend/src/types/rust.d.ts +++ b/frontend/src/types/rust.d.ts @@ -41,7 +41,7 @@ type DiskStatus__Offline = { /** Defines disk status names */ type DiskStatusName = - | "good" | "bad" | "offline"; + | "Good" | "Bad" | "Offline"; interface NodeInfo { name: string; @@ -83,7 +83,7 @@ type NodeStatus__Offline = { /** Defines node status names */ type NodeStatusName = - | "good" | "bad" | "offline"; + | "Good" | "Bad" | "Offline"; /** [`VDisk`]'s replicas */ interface Replica { @@ -128,6 +128,14 @@ interface SpaceInfo { occupied_disk: number; } +/** Virtual disk Component */ +interface VDisk { + id: number; + status: VDiskStatus; + partition_count: number; + replicas: Array; +} + /** * Virtual disk status. *