From f33e77a81f76c776ed4672043377c03315b2487c Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Wed, 21 Apr 2021 19:33:21 -0300 Subject: [PATCH] feat(network nodes): add network nodes page Also add a screen to mark disconnected nodes as gone chore(translations): add spanish translations for network nodes Also fix some texts keys chore(reachable-nodes): rename plugin --- i18n/generic.json | 17 +++ i18n/translations/en.json | 4 + i18n/translations/es.json | 21 ++- plugins/lime-plugin-reachable-nodes/index.js | 13 ++ .../networkNodes.spec.js | 134 ++++++++++++++++++ .../networkNodes.stories.js | 34 +++++ .../deleteNodesPage/deleteNodesPage.js | 86 +++++++++++ .../src/containers/deleteNodesPage/index.js | 2 + .../src/containers/deleteNodesPage/style.less | 17 +++ .../src/networkNodesApi.js | 9 ++ .../src/networkNodesApi.spec.js | 49 +++++++ .../src/networkNodesMenu.js | 7 + .../src/networkNodesPage.js | 85 +++++++++++ .../src/networkNodesQueries.js | 16 +++ .../src/style.less | 15 ++ 15 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 plugins/lime-plugin-reachable-nodes/index.js create mode 100644 plugins/lime-plugin-reachable-nodes/networkNodes.spec.js create mode 100644 plugins/lime-plugin-reachable-nodes/networkNodes.stories.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/index.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/style.less create mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesApi.spec.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesMenu.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesPage.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/style.less diff --git a/i18n/generic.json b/i18n/generic.json index 7fddcae1..83695d01 100644 --- a/i18n/generic.json +++ b/i18n/generic.json @@ -28,6 +28,8 @@ "confirm_6556b3a6": "Confirm", "confirm_location_2fe5ae11": "confirm location", "congratulations_ffe43bf9": "Congratulations", + "connected_howmany_228826ea": "Connected (%{howMany})", + "connected_nodes_4b7e6f49": "Connected Nodes", "count_days_de0c6a32": { "one": "1 days", "other": "%{count} days" @@ -48,11 +50,19 @@ "one": "1 seconds", "other": "%{count} seconds" }, + "count_selected_nodes_19bbd632": { + "one": "1 selected-nodes", + "other": "%{count} selected-nodes" + }, "create_network_d229d642": "Create network", "create_new_network_28805f92": "Create new network", "create_session_ad54bdb6": "Create Session", "currently_your_node_has_version_1c26984b": "Currently your node has version:", + "delete_a6efa79d": "Delete", + "delete_nodes_f63ec0d5": "Delete Nodes", "device_95d26d94": "Device", + "disconnected_howmany_10fc7bd5": "Disconnected (%{howMany})", + "disconnected_nodes_88f80d1e": "Disconnected Nodes", "don_t_show_this_message_again_9950c20": "Don't show this message again", "download_c7ffdfb9": "Download", "downloading_1e41f805": "Downloading", @@ -65,6 +75,7 @@ "full_path_metrics_2859608f": "Full path metrics", "go_64ecd1fd": "Go!", "go_to_community_view_d12b8d67": "Go to Community View", + "go_to_delete_nodes_1203128b": "Go to Delete Nodes", "go_to_node_view_26ba929d": "Go to Node View", "ground_routing_12ab04c9": "Ground Routing", "ground_routing_configuration_3f4fa9c1": "Ground Routing configuration", @@ -72,6 +83,7 @@ "hide_console_9bbb309e": "Hide Console", "host_name_d865cef3": "Host name", "i_don_t_know_the_shared_password_336b198": "I don't know the shared password", + "if_some_of_these_nodes_no_longer_belong_to_the_net_a75d316f": "If some of these nodes no longer belong to the network you can delete them from Delete Nodes.", "interface_177dac54": "Interface", "internet_connection_fda60ffa": "Internet connection", "ip_addresses_440ac240": "IP Addresses", @@ -135,6 +147,7 @@ "select_file_71aa4113": "Select file", "select_new_node_5b2e9165": "Select new node", "select_one_b647b384": "Select one", + "select_the_nodes_which_no_longer_belong_to_the_net_8ed27f05": "Select the nodes which no longer belong to the network and delete them from the list of disconnected nodes", "set_network_bcd0ea96": "Set network", "setting_network_21ebac51": "Setting network", "setting_up_new_password_4daf8f1c": "Setting up new password", @@ -147,6 +160,7 @@ "size_b30e1077": "Size", "station_name_7d67417c": "Station name", "status_e7fdbe06": "Status", + "successfully_deleted_23ce0a20": "Successfully deleted", "system_55b0ca91": "System", "the_are_not_mesh_interfaces_available_4055abd7": "The are not mesh interfaces available", "the_download_failed_130e1274": "The download failed", @@ -156,9 +170,12 @@ "the_selected_image_is_not_valid_for_the_target_dev_cea9b494": "The selected image is not valid for the target device", "the_shared_password_has_been_chosen_by_the_communi_f9d30a92": "The shared password has been chosen by the community when the network was created. You can ask other community members for it.", "the_upgrade_should_be_done_d66854": "The upgrade should be done", + "there_are_no_left_discconected_nodes_cd78852e": "There are no left discconected nodes", "there_s_an_active_remote_support_session_4a40a8bb": "There's an active remote support session", "there_s_no_open_session_for_remote_support_click_a_efd0d415": "There's no open session for remote support. Click at Create Session to begin one", "these_are_the_nodes_associated_on_this_radio_3d302167": "These are the nodes associated on this radio", + "these_are_the_nodes_with_which_you_do_not_have_con_ef5cc209": "These are the nodes with which you do not have connectivity, it is possible that they are not turned on or a link to reach them is down.", + "these_are_the_nodes_with_which_you_have_connectivi_ef11819b": "These are the nodes with which you have connectivity, i.e. there is a working path from your node to each of them.", "this_device_does_not_support_secure_rollback_to_pr_1c167a2c": "This device does not support secure rollback to previous version if something goes wrong", "this_device_supports_secure_rollback_to_previous_v_a60ddbcb": "This device supports secure rollback to previous version if something goes wrong", "this_node_is_the_gateway_1e20aaff": "This node is the gateway", diff --git a/i18n/translations/en.json b/i18n/translations/en.json index bb1272ae..0eeaca9a 100644 --- a/i18n/translations/en.json +++ b/i18n/translations/en.json @@ -59,5 +59,9 @@ "zero": "No one has joined yet.", "one": "One person has joined.", "other": "%{count} people have joined." + }, + "count_selected_nodes_19bbd632": { + "one": "node selected", + "other": "nodes selected" } } diff --git a/i18n/translations/es.json b/i18n/translations/es.json index 9012acac..80600133 100644 --- a/i18n/translations/es.json +++ b/i18n/translations/es.json @@ -192,8 +192,25 @@ "visit_a_neighboring_node_4116be4": "Visitar un nodo vecino", "select_new_node_5b2e9165": "Selecciona el nodo", "visit_864b4060": "Visitar", - "go_to_community_view_d12b8d67": "Ir a Vista de Comunidad", "go_to_node_view_26ba929d": "Ir a Vista de Nodo", - "network_nodes_4368eb67": "Nodos de la Red" + "network_nodes_4368eb67": "Nodos de la Red", + "connected_howmany_228826ea": "Conectados (%{howMany})", + "connected_nodes_4b7e6f49": "Nodos Conectados", + "delete_a6efa79d": "Eliminar", + "delete_nodes_f63ec0d5": "Eliminar Nodos", + "disconnected_howmany_10fc7bd5": "Desconectados (%{howMany})", + "disconnected_nodes_88f80d1e": "Nodos Desconectados", + "go_to_community_view_d12b8d67": "Ir a Vista de Comunidad", + "go_to_delete_nodes_1203128b": "Ir a Eliminar Nodos", + "count_selected_nodes_19bbd632": { + "one": "nodo seleccionado", + "other": "nodos seleccionados" + }, + "successfully_deleted_23ce0a20": "Eliminado/s correctamente", + "there_are_no_left_discconected_nodes_cd78852e": "No hay nodos desconectados", + "these_are_the_nodes_with_which_you_do_not_have_con_ef5cc209": "Son los nodos con los que no tienes conectividad, es posible que no estén encendidos o que algún enlace para llegar a ellos esté caído.", + "these_are_the_nodes_with_which_you_have_connectivi_ef11819b": "Son los nodos con los que tienes conectividad, es decir que hay un camino funcionando entre tu nodo y cada uno de ellos.", + "if_some_of_these_nodes_no_longer_belong_to_the_net_a75d316f": "Si alguno de estos nodos no pertence más a la red puedes eliminarlo desde Eliminar Nodos.", + "select_the_nodes_which_no_longer_belong_to_the_net_8ed27f05": "Selecciona los nodos que ya no pertenecen a la red y elimínalos de la lista de nodos desconectados" } diff --git a/plugins/lime-plugin-reachable-nodes/index.js b/plugins/lime-plugin-reachable-nodes/index.js new file mode 100644 index 00000000..6159bd1d --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/index.js @@ -0,0 +1,13 @@ +import Page from './src/networkNodesPage'; +import { NetworkNodesMenu } from './src/networkNodesMenu'; +import DeleteNodesPage from './src/containers/deleteNodesPage'; + +export default { + name: 'NetworkNodes', + page: Page, + menu: NetworkNodesMenu, + menuView: 'community', + additionalProtectedRoutes: [ + ['delete-nodes', DeleteNodesPage] + ] +}; diff --git a/plugins/lime-plugin-reachable-nodes/networkNodes.spec.js b/plugins/lime-plugin-reachable-nodes/networkNodes.spec.js new file mode 100644 index 00000000..cfc9fb3b --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/networkNodes.spec.js @@ -0,0 +1,134 @@ +import { h } from 'preact'; +import { fireEvent, act, screen } from '@testing-library/preact'; +import '@testing-library/jest-dom'; +import waitForExpect from 'wait-for-expect'; + +import NetworkNodesPage from './src/networkNodesPage'; +import DeleteNodesPage from './src/containers/deleteNodesPage'; +import queryCache from 'utils/queryCache'; +import { getNodes, markNodesAsGone } from './src/networkNodesApi'; +import { render } from 'utils/test_utils'; + +jest.mock('./src/networkNodesApi'); + +beforeEach(() => { + getNodes.mockImplementation(async () => [ + { hostname: 'node1', status: 'connected' }, + { hostname: 'node2', status: 'connected' }, + { hostname: 'node3', status: 'connected' }, + { hostname: 'node4', status: 'disconnected' }, + { hostname: 'node5', status: 'disconnected' }, + { hostname: 'node6', status: 'disconnected' }, + { hostname: 'node7', status: 'disconnected' }, + { hostname: 'node8', status: 'gone' }, + { hostname: 'node9', status: 'gone' }, + ]); + markNodesAsGone.mockImplementation(async () => []); +}); + +afterEach(() => { + act(() => queryCache.clear()); +}); + +describe('network nodes screen', () => { + it('shows one tab for connected nodes and one for discconected nodes with length', async () => { + render(); + expect(await screen.findByRole('tab', { name: /^connected \(3\)/i })).toBeVisible(); + expect(await screen.findByRole('tab', { name: /^disconnected \(4\)/i })).toBeVisible(); + }) + + it('shows one row with the hostname for each connect node', async () => { + render(); + expect(await screen.findByText('node1')).toBeVisible(); + expect(await screen.findByText('node2')).toBeVisible(); + expect(await screen.findByText('node3')).toBeVisible(); + }) + + it('shows one row with the hostname for each disconnect node', async () => { + render(); + const tabDisconnected = await screen.findByRole('tab', { name: /^disconnected \(4\)/i }); + fireEvent.click(tabDisconnected); + expect(await screen.findByText('node4')).toBeVisible(); + expect(await screen.findByText('node5')).toBeVisible(); + expect(await screen.findByText('node6')).toBeVisible(); + expect(await screen.findByText('node7')).toBeVisible(); + }) + + it('shows a link to go to delete nodes page', async () => { + render(); + const tabDisconnected = await screen.findByRole('tab', { name: /^disconnected \(4\)/i }); + fireEvent.click(tabDisconnected); + expect(await screen.findByRole('link', { name: /go to delete nodes/i })).toBeVisible(); + }) + + it('does not show a link to go to delete nodes page if there are no disconnected nodes', async () => { + getNodes.mockImplementation(async () => [ + { hostname: 'node1', status: 'connected' }, + { hostname: 'node2', status: 'connected' }, + { hostname: 'node3', status: 'connected' }, + ]); + render(); + const tabDisconnected = await screen.findByRole('tab', { name: /^disconnected \(0\)/i }); + fireEvent.click(tabDisconnected); + expect(screen.queryByRole('link', { name: /go to delete nodes/i })).toBeNull(); + }) + + it('shows help message when clicking on help button', async () => { + render(); + const helpButton = await screen.findByLabelText('help'); + fireEvent.click(helpButton); + expect(await screen.findByText("Connected Nodes")).toBeVisible(); + expect(await screen.findByText("These are the nodes with which you have connectivity, " + + "i.e. there is a working path from your node to each of them.")).toBeVisible(); + expect(await screen.findByText("Disconnected Nodes")).toBeVisible(); + expect(await screen.findByText("These are the nodes with which you do not have connectivity, " + + "it is possible that they are not turned on or a link to reach them is down.")).toBeVisible(); + }) +}); + + +describe('delete nodes page', () => { + it('shows the list of disconnected nodes only', async () => { + render(); + expect(await screen.findByText('node4')).toBeVisible(); + expect(await screen.findByText('node5')).toBeVisible(); + expect(await screen.findByText('node6')).toBeVisible(); + expect(await screen.findByText('node7')).toBeVisible(); + expect(screen.queryByText('node1')).toBeNull(); + expect(screen.queryByText('node2')).toBeNull(); + expect(screen.queryByText('node3')).toBeNull(); + expect(screen.queryByText('node8')).toBeNull(); + expect(screen.queryByText('node9')).toBeNull(); + }) + + it('calls the markNodesAsGone api when deleting', async () => { + markNodesAsGone.mockImplementation(async () => ['node6']); + render(); + fireEvent.click(await screen.findByText('node6')); + fireEvent.click(await screen.findByRole('button', { name: /delete/i })); + await waitForExpect(() => { + expect(markNodesAsGone).toBeCalledWith(['node6']); + }) + }) + + it('hide nodes from the list after deletion', async () => { + markNodesAsGone.mockImplementation(async () => ['node6', 'node7']); + render(); + fireEvent.click(await screen.findByText('node6')); + fireEvent.click(await screen.findByText('node7')); + fireEvent.click(await screen.findByRole('button', { name: /delete/i })); + expect(await screen.findByText('node4')).toBeVisible(); + expect(await screen.queryByText('node5')).toBeVisible(); + expect(await screen.queryByText('node6')).toBeNull(); + expect(await screen.queryByText('node7')).toBeNull(); + }) + + it('show success message after deletion', async () => { + markNodesAsGone.mockImplementation(async () => ['node6', 'node7']); + render(); + fireEvent.click(await screen.findByText('node6')); + fireEvent.click(await screen.findByText('node7')); + fireEvent.click(await screen.findByRole('button', { name: /delete/i })); + expect(await screen.findByText(/successfully deleted/i)).toBeVisible(); + }) +}) \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/networkNodes.stories.js b/plugins/lime-plugin-reachable-nodes/networkNodes.stories.js new file mode 100644 index 00000000..86ada871 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/networkNodes.stories.js @@ -0,0 +1,34 @@ +import { NetworkNodesPage_ } from "./src/networkNodesPage"; +import { DeleteNodesPage_ } from "./src/containers/deleteNodesPage"; + +export default { + title: 'Containers/Network nodes', +}; + +const nodes = [ + { hostname: "ql-czuk", status: "connected" }, + { hostname: "ql-irene", status: "connected" }, + { hostname: "ql-ipem", status: "connected" }, + { hostname: "ql-czuck-bbone", status: "connected" }, + { hostname: "ql-graciela", status: "connected" }, + { hostname: "ql-marisa", status: "connected" }, + { hostname: "ql-anaymarcos", status: "connected" }, + { hostname: "ql-quinteros", status: "connected" }, + { hostname: "ql-guada", status: "connected" }, + { hostname: "ql-refu-bbone", status: "disconnected" }, + { hostname: "si-soniam", status: "disconnected" }, + { hostname: "si-giordano", status: "disconnected" }, + { hostname: "si-mario", status: "disconnected" }, + { hostname: "si-manu", status: "disconnected" }, +]; + +export const networkNodesPage = () => ( + +) + +export const deleteNodesPage = (args) => ( + +) +deleteNodesPage.argTypes = { + onDelete: { action: 'deleted' } +} \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js new file mode 100644 index 00000000..f6171fcc --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js @@ -0,0 +1,86 @@ +import { h } from "preact"; +import { List, ListItem } from 'components/list'; +import Loading from 'components/loading'; +import Toast from 'components/toast'; +import { useEffect, useState } from 'preact/hooks'; +import { useSet } from 'react-use'; +import { useMarkNodesAsGone, useNetworkNodes } from '../../networkNodesQueries' +import style from './style.less'; +import I18n from 'i18n-js'; + +export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) => { + const [selectedNodes, { toggle, has, reset }] = useSet(new Set([])); + const [showSuccess, setshowSuccess] = useState(false); + const disconnectedNodes = nodes.filter(n => n.status === "disconnected"); + + useEffect(() => { + if (isSuccess) { + reset(); + setshowSuccess(true); + setTimeout(() => { + setshowSuccess(false); + }, 2000); + } + }, [isSuccess]) + + return ( +
+
+

{I18n.t("Delete Nodes")}

+ {disconnectedNodes.length > 0 && +

{I18n.t("Select the nodes which no longer belong to the network and " + + "delete them from the list of disconnected nodes")}

+ } + {disconnectedNodes.length === 0 && +

{I18n.t("There are no left discconected nodes")}

+ } + + {disconnectedNodes.map(node => + toggle(node.hostname)} > +
+ + {node.hostname} +
+
+ )} +
+
+ {selectedNodes.size >= 1 && +
+ + {[selectedNodes.size, + I18n.t('selected-nodes', { count: selectedNodes.size }) + ].join(' ')} + + {!isSubmitting && + + } + {isSubmitting && +
+ +
+ } +
+ } + {showSuccess && + + } +
+ ) +}; + +const DeleteNodesPage = () => { + const [deleteNodes, { isSubmitting, isSuccess }] = useMarkNodesAsGone(); + const { data: nodes, isLoading } = useNetworkNodes(); + if (isLoading) { + return
+ } + + return +} + +export default DeleteNodesPage; \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/index.js b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/index.js new file mode 100644 index 00000000..a279dd8c --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/index.js @@ -0,0 +1,2 @@ +export * from './deleteNodesPage'; +export { default } from './deleteNodesPage'; \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/style.less b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/style.less new file mode 100644 index 00000000..6e844dce --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/style.less @@ -0,0 +1,17 @@ +.nodeItem { + font-size: 2rem; + display: flex; + flex: auto; + input { + margin-right: 1em; + } + cursor: pointer; +} + +.bottomAction { + display: flex; + align-items: baseline; + padding: 0.5em 1em; + font-weight: bold; + border-top: 0.05em solid #bdbdbd; +} \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js b/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js new file mode 100644 index 00000000..060496ce --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js @@ -0,0 +1,9 @@ +import api from 'utils/uhttpd.service'; + +export const getNodes = () => + api.call('network-nodes', 'get_nodes', {}).toPromise() + .then(res => res.nodes); + +export const markNodesAsGone = (hostnames) => + api.call('network-nodes', 'mark_nodes_as_gone', { hostnames: hostnames }).toPromise() + .then(() => hostnames); \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.spec.js b/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.spec.js new file mode 100644 index 00000000..bc1e2fef --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.spec.js @@ -0,0 +1,49 @@ +import { of, throwError } from 'rxjs'; +import api from 'utils/uhttpd.service'; +import waitForExpect from 'wait-for-expect'; + +jest.mock('utils/uhttpd.service') + +import { getNodes, markNodesAsGone } from './networkNodesApi'; + + +beforeEach(() => { + api.call.mockClear(); +}) + +describe('getNodes', () => { + it('calls the expected endpoint', async () => { + api.call.mockImplementation(() => of({ status: 'ok' })) + await getNodes(); + expect(api.call).toBeCalledWith('network-nodes', 'get_nodes', {}); + }) + + it('resolves to network nodes', async () => { + const networkNodes = [ + { hostname: 'node1', status: 'connected' }, + { hostname: 'node2', status: 'disconnected' }, + { hostname: 'node3', status: 'gone' }, + ]; + api.call.mockImplementation(() => of( + { + status: 'ok', + nodes: networkNodes, + })); + let nodes = await getNodes(); + expect(nodes).toEqual(networkNodes); + }); +}); + +describe('markNodesAsGone', () => { + it('calls the expected endpoint', async () => { + api.call.mockImplementation(() => of({ status: 'ok' })) + await markNodesAsGone(['node1']); + expect(api.call).toBeCalledWith('network-nodes', 'mark_nodes_as_gone', { hostnames: ['node1'] }) + }) + + it('resolve to hostnames passed as parameters on success', async() => { + api.call.mockImplementation(() => of({status: 'ok'})) + const result = await markNodesAsGone(['node1', 'node2']) + expect(result).toEqual(['node1', 'node2']) + }) +}); diff --git a/plugins/lime-plugin-reachable-nodes/src/networkNodesMenu.js b/plugins/lime-plugin-reachable-nodes/src/networkNodesMenu.js new file mode 100644 index 00000000..ba59c9d4 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/networkNodesMenu.js @@ -0,0 +1,7 @@ +import { h } from 'preact'; + +import I18n from 'i18n-js'; + +export const NetworkNodesMenu = () => ( + {I18n.t('Network Nodes')} +); \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/networkNodesPage.js b/plugins/lime-plugin-reachable-nodes/src/networkNodesPage.js new file mode 100644 index 00000000..55cd73d1 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/networkNodesPage.js @@ -0,0 +1,85 @@ +import { h } from "preact"; +import { useState } from "preact/hooks"; +import style from "./style.less"; +import Tabs from "components/tabs"; +import Loading from "components/loading"; +import { List, ListItem } from "components/list"; +import { useNetworkNodes } from "./networkNodesQueries"; +import Help from "components/help"; +import I18n from 'i18n-js'; + +const DeleteNodesLegend = () => ( +
+
{I18n.t("If some of these nodes no longer belong " + + "to the network you can delete them from Delete Nodes.")}
+ +
+) + +const PageHelp = () => ( +
+

+

{I18n.t("Connected Nodes")}
+ {I18n.t("These are the nodes with which you have connectivity, " + + "i.e. there is a working path from your node to each of them.")} +

+

+

{I18n.t("Disconnected Nodes")}
+ {I18n.t("These are the nodes with which you do not have connectivity, " + + "it is possible that they are not turned on or a link to reach them is down.")} +

+
+); + +const PageTabs = ({ nodes, ...props }) => { + const nConnected = nodes.filter(n => n.status === "connected").length; + const nDisconnected = nodes.filter(n => n.status === "disconnected").length; + const tabs = [ + { key: 'connected', repr: I18n.t('Connected (%{howMany})', { howMany: nConnected }) }, + { key: 'disconnected', repr: I18n.t('Disconnected (%{howMany})', { howMany: nDisconnected }) }, + ]; + return +} + +export const NetworkNodesPage_ = ({ nodes }) => { + const [selectedGroup, setselectedGroup] = useState('connected'); + return ( +
+
+ +
+ +
+
+ + {nodes + .filter(n => n.status === selectedGroup) + .sort((a, b) => a.hostname > b.hostname) + .map( + node => + +
+ {node.hostname} +
+
+ )} + {selectedGroup === "disconnected" && + nodes.filter(n => n.status == "disconnected").length && + + } +
+
+ ) +} + +const NetworkNodesPage = () => { + const { data: nodes, isLoading } = useNetworkNodes(); + + if (isLoading) { + return
+ } + + return +} + +export default NetworkNodesPage \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js b/plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js new file mode 100644 index 00000000..fb9b8b76 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js @@ -0,0 +1,16 @@ +import { useQuery, useMutation } from 'react-query'; +import { getNodes, markNodesAsGone } from './networkNodesApi'; +import queryCache from 'utils/queryCache'; + +export const useNetworkNodes = () => useQuery(['network-nodes', 'get_nodes'], getNodes) + +export const useMarkNodesAsGone = () => useMutation(markNodesAsGone, { + onSuccess: hostnames => queryCache.setQueryData(['network-nodes', 'get_nodes'], + old => { + const result = old.map( + node => hostnames.indexOf(node.hostname) != -1 ? { ...node, status: "gone" } : node + ) + return result; + } + ) +}) \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/style.less b/plugins/lime-plugin-reachable-nodes/src/style.less new file mode 100644 index 00000000..b3ea1445 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/style.less @@ -0,0 +1,15 @@ +.deleteNodesLegend { + text-align: center; + flex: auto; + padding: 0.5em; +} + +.nodeItem { + font-size: 2rem; + display: flex; + flex: auto +} + +.helpWrapper { + padding: 1em; +} \ No newline at end of file