From 957bdd5c19bbcae77f83b63fb088c05a98b195b7 Mon Sep 17 00:00:00 2001 From: Dmitriy Koltsov <dkoltsov@picodata.io> Date: Tue, 13 Aug 2024 17:43:20 +0300 Subject: [PATCH] feat(webui/plugins): display plagins in WebUI Add services to each tier and list of installed plugins to cluster info Closes #809 --- src/http_server.rs | 21 ++- src/storage.rs | 27 ++- test/int/test_http_server.py | 166 +++++++++++++++++- .../ClusterInfo/ClusterInfo.module.scss | 4 + .../nodesPage/ClusterInfo/ClusterInfo.tsx | 19 ++ .../TierCard/TierCard.module.scss | 4 +- .../NodesContent/TierCard/TierCard.tsx | 21 ++- webui/src/shared/entity/cluster/info/types.ts | 1 + webui/src/shared/entity/tier/common/types.ts | 2 +- webui/src/shared/entity/tier/list/mock.ts | 4 +- .../intl/translations/en/pages/instances.ts | 7 +- .../intl/translations/ru/pages/instances.ts | 7 +- 12 files changed, 260 insertions(+), 23 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index f8adf82139..b4644a837f 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -138,7 +138,7 @@ pub(crate) struct TierInfo { #[serde(rename = "can_vote")] // for compatibility with lua version can_vote: bool, name: String, - plugins: Vec<String>, + services: Vec<String>, } // From the Lua version: @@ -168,6 +168,8 @@ pub(crate) struct ClusterInfo { instances_current_state_offline: usize, memory: MemoryInfo, instances_current_state_online: usize, + // list of serialized plugin identifiers - "<plugin_name> <plugin_version>" + plugins: Vec<String>, } fn get_replicasets( @@ -365,6 +367,14 @@ pub(crate) fn http_api_cluster() -> Result<ClusterInfo, Box<dyn Error>> { let storage = Clusterwide::get(); let replicasets = get_replicasets_info(storage, true)?; + let plugins = storage + .plugin + .get_all() + .expect("storage shouldn't fail") + .iter() + .map(|plugin| [plugin.name.clone(), plugin.version.clone()].join(" ")) + .collect(); + let mut instances = 0; let mut instances_online = 0; let mut replicasets_count = 0; @@ -395,6 +405,7 @@ pub(crate) fn http_api_cluster() -> Result<ClusterInfo, Box<dyn Error>> { instances_current_state_offline: (instances - instances_online), memory: mem_info, instances_current_state_online: instances_online, + plugins, }; Ok(res) @@ -416,7 +427,13 @@ pub(crate) fn http_api_tiers() -> Result<Vec<TierInfo>, Box<dyn Error>> { instance_count: 0, can_vote: true, name: item.name.clone(), - plugins: Vec::new(), + services: storage + .service + .get_by_tier(&item.name) + .expect("storage shouldn't fail") + .iter() + .map(|service| service.name.clone()) + .collect(), }, ) }) diff --git a/src/storage.rs b/src/storage.rs index 61f77ee12e..6f776ce116 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -3627,12 +3627,18 @@ impl Plugins { #[inline] pub fn get_all_versions(&self, plugin_name: &str) -> tarantool::Result<Vec<PluginDef>> { - let plugins = self - .space + self.space .select(IteratorType::All, &(plugin_name,))? .map(|tuple| tuple.decode()) - .collect::<tarantool::Result<Vec<_>>>(); - plugins + .collect() + } + + #[inline] + pub fn get_all(&self) -> tarantool::Result<Vec<PluginDef>> { + self.space + .select(IteratorType::All, &())? + .map(|tuple| tuple.decode()) + .collect::<tarantool::Result<Vec<_>>>() } #[inline] @@ -3726,6 +3732,19 @@ impl Services { Ok(result) } + #[inline] + pub fn get_by_tier(&self, tier_name: &String) -> tarantool::Result<Vec<ServiceDef>> { + let all_services = self.space.select(IteratorType::All, &())?; + let mut result = vec![]; + for tuple in all_services { + let svc = tuple.decode::<ServiceDef>()?; + if svc.tiers.contains(tier_name) { + result.push(svc) + } + } + Ok(result) + } + #[inline] pub fn delete(&self, plugin: &str, service: &str, version: &str) -> tarantool::Result<()> { self.space.delete(&[plugin, service, version])?; diff --git a/test/int/test_http_server.py b/test/int/test_http_server.py index 702ca4a311..856b0e7463 100644 --- a/test/int/test_http_server.py +++ b/test/int/test_http_server.py @@ -1,4 +1,12 @@ -from conftest import Instance +from conftest import ( + Cluster, + Instance, + _PLUGIN, + _PLUGIN_SERVICES, + _PLUGIN_SMALL, + _PLUGIN_SMALL_SERVICES, + _PLUGIN_VERSION_1, +) from urllib.request import urlopen import pytest import json @@ -23,7 +31,7 @@ def test_http_routes(instance: Instance): @pytest.mark.webui -def test_webui(instance: Instance): +def test_webui_basic(instance: Instance): http_listen = instance.env["PICODATA_HTTP_LISTEN"] instance_version = instance.eval("return pico.PICODATA_VERSION") @@ -66,7 +74,7 @@ def test_webui(instance: Instance): "instanceCount": 1, "can_vote": True, "name": "default", - "plugins": [], + "services": [], } ] @@ -79,6 +87,158 @@ def test_webui(instance: Instance): "currentInstaceVersion": instance_version, "memory": {"usable": 67108864, "used": 33554432}, "instancesCurrentStateOnline": 1, + "plugins": [], + } + + +@pytest.mark.webui +def test_webui_with_plugin(cluster: Cluster): + cluster_cfg = """ + cluster: + cluster_id: test + tier: + red: + replication_factor: 1 + blue: + replication_factor: 1 + green: + replication_factor: 1 + """ + cluster.set_config_file(yaml=cluster_cfg) + + i1 = cluster.add_instance(wait_online=True, tier="red", enable_http=True) + i2 = cluster.add_instance(wait_online=True, tier="blue") + i3 = cluster.add_instance(wait_online=True, tier="green") + + i1.call("pico.install_plugin", _PLUGIN, _PLUGIN_VERSION_1) + i1.call("pico.install_plugin", _PLUGIN_SMALL, _PLUGIN_VERSION_1) + i1.call( + "pico.service_append_tier", + _PLUGIN, + _PLUGIN_VERSION_1, + _PLUGIN_SERVICES[0], + "red", + ) + i1.call( + "pico.service_append_tier", + _PLUGIN, + _PLUGIN_VERSION_1, + _PLUGIN_SERVICES[1], + "blue", + ) + i1.call( + "pico.service_append_tier", + _PLUGIN_SMALL, + _PLUGIN_VERSION_1, + _PLUGIN_SMALL_SERVICES[0], + "blue", + ) + + http_listen = i1.env["PICODATA_HTTP_LISTEN"] + instance_version = i1.eval("return pico.PICODATA_VERSION") + + with urlopen(f"http://{http_listen}/") as response: + assert response.headers.get("content-type") == "text/html" + + instance_template = { + "failureDomain": {}, + "isLeader": True, + "currentState": "Online", + "targetState": "Online", + "version": instance_version, + } + instance_1 = { + **instance_template, + "name": "i1", + "binaryAddress": i1.listen, + "httpAddress": http_listen, + } + instance_2 = { + **instance_template, + "name": "i2", + "binaryAddress": i2.listen, + "httpAddress": "", + } + instance_3 = { + **instance_template, + "name": "i3", + "binaryAddress": i3.listen, + "httpAddress": "", + } + + replicaset_template = { + "state": "Online", + "version": instance_version, + "instanceCount": 1, + "capacityUsage": 50, + "memory": { + "usable": 67108864, + "used": 33554432, + }, + "uuid": i1.replicaset_uuid(), + "id": "r1", + } + r1 = { + **replicaset_template, + "uuid": i1.replicaset_uuid(), + "id": "r1", + "instances": [instance_1], + } + r2 = { + **replicaset_template, + "uuid": i2.replicaset_uuid(), + "id": "r2", + "instances": [instance_2], + } + r3 = { + **replicaset_template, + "uuid": i3.replicaset_uuid(), + "id": "r3", + "instances": [instance_3], + } + + tier_template = { + "replicasetCount": 1, + "rf": 1, + "instanceCount": 1, + "can_vote": True, + } + + tier_red = { + **tier_template, + "name": "red", + "services": [_PLUGIN_SERVICES[0]], + "replicasets": [r1], + } + tier_blue = { + **tier_template, + "name": "blue", + "services": [_PLUGIN_SERVICES[1], _PLUGIN_SMALL_SERVICES[0]], + "replicasets": [r2], + } + tier_green = {**tier_template, "name": "green", "services": [], "replicasets": [r3]} + + with urlopen(f"http://{http_listen}/api/v1/tiers") as response: + assert response.headers.get("content-type") == "application/json" + assert sorted(json.load(response), key=lambda tier: tier["name"]) == [ + tier_blue, + tier_green, + tier_red, + ] + + with urlopen(f"http://{http_listen}/api/v1/cluster") as response: + assert response.headers.get("content-type") == "application/json" + assert json.load(response) == { + "capacityUsage": 50, + "replicasetsCount": 3, + "instancesCurrentStateOnline": 3, + "instancesCurrentStateOffline": 0, + "currentInstaceVersion": instance_version, + "memory": {"usable": 201326592, "used": 100663296}, + "plugins": [ + _PLUGIN + " " + _PLUGIN_VERSION_1, + _PLUGIN_SMALL + " " + _PLUGIN_VERSION_1, + ], } diff --git a/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.module.scss b/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.module.scss index dd34919954..7c647d2fd5 100644 --- a/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.module.scss +++ b/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.module.scss @@ -24,6 +24,10 @@ display: flex; } +.pluginsWrapper { + display: flex; +} + .columnName { font-size: 16px; font-weight: 500; diff --git a/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.tsx b/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.tsx index 35eb26caf7..91312e918b 100644 --- a/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.tsx +++ b/webui/src/modules/nodes/nodesPage/ClusterInfo/ClusterInfo.tsx @@ -41,6 +41,25 @@ export const ClusterInfo = (props: ClusterInfoProps) => { </div> </div> <div className={styles.right}> + <div className={cn(styles.rightColumn)}> + <div className={styles.columnName}> + {clusterTranslations.plugins.label} + </div> + <div className={styles.columnContent}> + <div className={styles.columnValue}> + {clusterInfoData.plugins.map((plugin, i) => + i == 0 ? ( + <>{plugin}</> + ) : ( + <> + <br /> + {plugin} + </> + ) + )} + </div> + </div> + </div> <div className={cn(styles.rightColumn)}> <div className={styles.columnName}> {clusterTranslations.replicasets.label} diff --git a/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.module.scss b/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.module.scss index 1ae0b185c2..58671e3017 100644 --- a/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.module.scss +++ b/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.module.scss @@ -50,7 +50,7 @@ $chevron-column-flex: 0.2; align-items: flex-start; } -.pluginColumn { +.servicesColumn { flex: 1 167px; } @@ -100,7 +100,7 @@ $chevron-column-flex: 0.2; @include ellipsis; } -.pluginValue { +.servicesValue { text-align: center; } diff --git a/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.tsx b/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.tsx index 50ec1afae2..25776faece 100644 --- a/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.tsx +++ b/webui/src/modules/nodes/nodesPage/NodesContent/TierCard/TierCard.tsx @@ -46,20 +46,31 @@ export const TierCard: FC<TierCardProps> = React.memo(({ tier }) => { <div className={cn( styles.infoColumn, - styles.pluginColumn, + styles.servicesColumn, styles.hiddenColumn )} > - <div className={styles.label}>{tierTranslations.plugins.label}</div> + <div className={styles.label}>{tierTranslations.services.label}</div> <div className={cn( styles.infoValue, styles.hiddenValue, - styles.pluginValue + styles.servicesValue )} > - {tier.plugins.length ? ( - <HiddenWrapper>{tier.plugins.join(", ")}</HiddenWrapper> + {tier.services.length ? ( + <HiddenWrapper> + {tier.services.map((service, i) => + i == 0 ? ( + <>{service}</> + ) : ( + <> + <br /> + {service} + </> + ) + )} + </HiddenWrapper> ) : ( <InfoNoData text={translation.components.infoNoData.label} /> )} diff --git a/webui/src/shared/entity/cluster/info/types.ts b/webui/src/shared/entity/cluster/info/types.ts index 4e51e2c7d9..d733659649 100644 --- a/webui/src/shared/entity/cluster/info/types.ts +++ b/webui/src/shared/entity/cluster/info/types.ts @@ -8,6 +8,7 @@ export type ServerClusterInfoType = { instancesCurrentStateOnline: number; instancesCurrentStateOffline: number; currentInstaceVersion: string; + plugins: string[]; }; export type ClusterInfoType = ServerClusterInfoType; diff --git a/webui/src/shared/entity/tier/common/types.ts b/webui/src/shared/entity/tier/common/types.ts index 7e19abce39..3d9a157d7e 100644 --- a/webui/src/shared/entity/tier/common/types.ts +++ b/webui/src/shared/entity/tier/common/types.ts @@ -6,7 +6,7 @@ import { export type ServerTierType = { name: string; - plugins: string[]; + services: string[]; replicasetCount: number; instanceCount: number; rf: number; diff --git a/webui/src/shared/entity/tier/list/mock.ts b/webui/src/shared/entity/tier/list/mock.ts index df61a02c96..b223cc70ac 100644 --- a/webui/src/shared/entity/tier/list/mock.ts +++ b/webui/src/shared/entity/tier/list/mock.ts @@ -84,7 +84,7 @@ export const mock: ServerTiersListType = [ instanceCount: 4, can_vote: true, name: "red", - plugins: [], + services: [], }, { replicasets: [ @@ -144,6 +144,6 @@ export const mock: ServerTiersListType = [ instanceCount: 2, can_vote: true, name: "blue", - plugins: [], + services: [], }, ]; diff --git a/webui/src/shared/intl/translations/en/pages/instances.ts b/webui/src/shared/intl/translations/en/pages/instances.ts index daf322fad1..e5e95b39c9 100644 --- a/webui/src/shared/intl/translations/en/pages/instances.ts +++ b/webui/src/shared/intl/translations/en/pages/instances.ts @@ -1,5 +1,8 @@ export const instances = { cluster: { + plugins: { + label: "Plugins", + }, capacityProgress: { label: "Capacity Usage", valueLabel: "Useful capacity", @@ -57,8 +60,8 @@ export const instances = { name: { label: "Tier Name", }, - plugins: { - label: "Plugins", + services: { + label: "Services", }, replicasets: { label: "Replicasets", diff --git a/webui/src/shared/intl/translations/ru/pages/instances.ts b/webui/src/shared/intl/translations/ru/pages/instances.ts index 187258c467..56da1d5a92 100644 --- a/webui/src/shared/intl/translations/ru/pages/instances.ts +++ b/webui/src/shared/intl/translations/ru/pages/instances.ts @@ -2,6 +2,9 @@ import { TPages } from "./types"; export const instances: TPages["instances"] = { cluster: { + plugins: { + label: "Плагины", + }, capacityProgress: { label: "Потребление памÑти", valueLabel: "ИÑпользовано", @@ -59,8 +62,8 @@ export const instances: TPages["instances"] = { name: { label: "Ðазвание тира", }, - plugins: { - label: "Плагин", + services: { + label: "СервиÑÑ‹", }, replicasets: { label: "РепликаÑеты", -- GitLab