From ce2b23ca933512a5182c63e9dcfa5927264bf237 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Wed, 21 Apr 2021 17:33:08 -0300 Subject: [PATCH 01/19] feat(network nodes): add Network Nodes Plugin --- plugins/lime-plugin-network-nodes/index.js | 9 ++++ .../networkNodes.spec.js | 54 +++++++++++++++++++ .../src/components/expandableNode/index.js | 25 +++++++++ .../src/components/expandableNode/stories.js | 20 +++++++ .../src/components/expandableNode/style.less | 14 +++++ .../src/networkNodesApi.js | 7 +++ .../src/networkNodesApi.spec.js | 34 ++++++++++++ .../src/networkNodesMenu.js | 8 +++ .../src/networkNodesPage.js | 49 +++++++++++++++++ .../src/networkNodesPage.stories.js | 51 ++++++++++++++++++ .../src/networkNodesQueries.js | 5 ++ .../src/networkNodesStyle.less | 5 ++ src/config.js | 2 + 13 files changed, 283 insertions(+) create mode 100644 plugins/lime-plugin-network-nodes/index.js create mode 100644 plugins/lime-plugin-network-nodes/networkNodes.spec.js create mode 100644 plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js create mode 100644 plugins/lime-plugin-network-nodes/src/components/expandableNode/stories.js create mode 100644 plugins/lime-plugin-network-nodes/src/components/expandableNode/style.less create mode 100644 plugins/lime-plugin-network-nodes/src/networkNodesApi.js create mode 100644 plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js create mode 100644 plugins/lime-plugin-network-nodes/src/networkNodesMenu.js create mode 100644 plugins/lime-plugin-network-nodes/src/networkNodesPage.js create mode 100644 plugins/lime-plugin-network-nodes/src/networkNodesPage.stories.js create mode 100644 plugins/lime-plugin-network-nodes/src/networkNodesQueries.js create mode 100644 plugins/lime-plugin-network-nodes/src/networkNodesStyle.less diff --git a/plugins/lime-plugin-network-nodes/index.js b/plugins/lime-plugin-network-nodes/index.js new file mode 100644 index 000000000..602ea57b8 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/index.js @@ -0,0 +1,9 @@ +import Page from './src/networkNodesPage'; +import Menu from './src/networkNodesMenu'; + +export default { + name: 'networkNodes', + page: Page, + menu: Menu, + menuView: 'community' +}; diff --git a/plugins/lime-plugin-network-nodes/networkNodes.spec.js b/plugins/lime-plugin-network-nodes/networkNodes.spec.js new file mode 100644 index 000000000..261b81f59 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/networkNodes.spec.js @@ -0,0 +1,54 @@ +// Here you define tests that closely resemble how your component is used +// Using the testing-library: https://testing-library.com + +import { h } from 'preact'; +import { fireEvent, screen, cleanup, act } from '@testing-library/preact'; +import '@testing-library/jest-dom'; +import { render } from 'utils/test_utils'; +import queryCache from 'utils/queryCache'; + +import NetworkNodes from './src/networkNodesPage'; +import { getNodes } from './src/networkNodesApi'; + +jest.mock('./src/networkNodesApi'); + +describe('networkNodes', () => { + beforeEach(() => { + getNodes.mockImplementation(async () => ({ + "ql-berta": { + ipv4: '10.5.0.16', + ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', + fw_version: 'LibreRouterOS 1.4' + }, + "ql-nelson": { + ipv4: '10.5.0.17', + ipv6: 'fd0d:fe46:8ce8::8bbf:75bf', + board: 'LibreRouter v1', + fw_version: 'LibreRouterOS 1.4' + } + })); + }); + + afterEach(() => { + cleanup(); + act(() => queryCache.clear()); + }); + + it('test that nodes are shown', async () => { + render(); + expect(await screen.findByText('ql-nelson')).toBeInTheDocument(); + expect(await screen.findByText('ql-berta')).toBeInTheDocument(); + }); + + it('test that details are shown on click', async () => { + render(); + const element = await screen.findByText('ql-nelson'); + fireEvent.click(element); + expect(await screen.findByRole('link', { name: '10.5.0.17'})).toBeInTheDocument(); + expect(await screen.findByText('IPv6: fd0d:fe46:8ce8::8bbf:75bf')).toBeInTheDocument(); + expect(await screen.findByText('Device: LibreRouter v1')).toBeInTheDocument(); + expect(await screen.findByText('Firmware: LibreRouterOS 1.4')).toBeInTheDocument(); + }) + +}); diff --git a/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js new file mode 100644 index 000000000..2bc67362b --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js @@ -0,0 +1,25 @@ +import { h } from 'preact'; +import I18n from 'i18n-js'; +import { ListItem } from 'components/list'; +import style from './style.less'; + +export const ExpandableNode = ({ node, showMore, onClick }) => { + const { hostname, ipv4, ipv6, board, fw_version } = node; + return ( + +
+
+
{hostname}
+
+ {showMore && +
+ {ipv4 &&
IPv4: {ipv4}
} + {ipv6 &&
IPv6: {ipv6}
} + {board &&
{I18n.t('Device')}: {board}
} + {fw_version &&
{I18n.t('Firmware')}: {fw_version}
} +
+ } +
+
+ ) +} \ No newline at end of file diff --git a/plugins/lime-plugin-network-nodes/src/components/expandableNode/stories.js b/plugins/lime-plugin-network-nodes/src/components/expandableNode/stories.js new file mode 100644 index 000000000..2abf1859f --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/components/expandableNode/stories.js @@ -0,0 +1,20 @@ +import { ExpandableNode } from './index'; + +export default { + title: 'Containers/NetworkNodes/Components/ExpandableNode', + component: ExpandableNode +}; + +const node = { + hostname: 'ql-flor', + ipv4:'10.5.0.16', + ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', + fw_version: 'LibreRouterOS 1.4' +}; + +export const folded = () => + + +export const unfolded = () => + \ No newline at end of file diff --git a/plugins/lime-plugin-network-nodes/src/components/expandableNode/style.less b/plugins/lime-plugin-network-nodes/src/components/expandableNode/style.less new file mode 100644 index 000000000..5dc98d636 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/components/expandableNode/style.less @@ -0,0 +1,14 @@ +.moreData { + padding-left: 2em; + cursor: text; +} + +.hostname { + font-size: 2em; +} + +.threeDots { + font-size: 1.5em; + font-weight: bold; + cursor: pointer; +} \ No newline at end of file diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesApi.js b/plugins/lime-plugin-network-nodes/src/networkNodesApi.js new file mode 100644 index 000000000..a2d02a0b6 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.js @@ -0,0 +1,7 @@ +import api from 'utils/uhttpd.service'; + +export const getNodes = () => + api.call('network-nodes', 'get_nodes', {}).toPromise() + .then(res => res.nodes); + +export const markNodesAsGone = () => api.call('network-nodes', 'mark_nodes_as_gone', {}).toPromise(); diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js b/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js new file mode 100644 index 000000000..69fef588c --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js @@ -0,0 +1,34 @@ +import { getNodes, markNodesAsGone } from './networkNodesApi' +import api from 'utils/uhttpd.service'; +import { of } from 'rxjs'; +jest.mock('utils/uhttpd.service') + +beforeEach(() => { + api.call.mockImplementation(() => of({ status: 'ok' })) +}) + +describe('getNodes', () => { + it('hits the expected endpoint', async () => { + getNodes(); + expect(api.call).toBeCalledWith('network-nodes', 'get_nodes', {}); + }); + + it('test resolves to nodes data', async () => { + const nodes = { + 'host1': { + ipv4: '10.5.0.16', + ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', + fw_version: 'LibreRouterOS 1.4' + }, + 'host2': { + ipv4: '10.5.0.17', + ipv6: 'fd0d:fe46:8ce8::8bbf:75bf', + board: 'TL-WDR3500', + fw_version: 'LibreRouterOS 1.4' + } + }; + api.call.mockImplementation(() => of({ status: 'ok', nodes })); + expect(await getNodes()).toEqual(nodes); + }); +}); diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js b/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js new file mode 100644 index 000000000..0303aa3c6 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js @@ -0,0 +1,8 @@ +import { h } from 'preact'; +import I18n from 'i18n-js'; + +const Menu = () => ( + {I18n.t('Network Nodes')} +); + +export default Menu; diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesPage.js b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js new file mode 100644 index 000000000..9d20e0d4a --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js @@ -0,0 +1,49 @@ +// NetworkNodes will be rendered when navigating to this plugin +import { h } from 'preact'; +import { useNetworkNodes } from './networkNodesQueries'; +import { List } from 'components/list'; +import { Loading } from 'components/loading'; +import { ExpandableNode } from './components/expandableNode'; +import style from './networkNodesStyle.less'; +import { useState } from 'preact/hooks'; +import I18n from 'i18n-js'; + +export const _NetworkNodes = ({ nodes, isLoading, unfoldedNode, onUnfold }) => { + if (isLoading) { + return
+ } + return ( +
+
{I18n.t("Network Nodes")}
+ + {nodes.map((node) => + onUnfold(node.hostname)} /> + )} + +
+ ) +}; + +const NetworkNodes = () => { + const { data: networkNodes, isLoading } = useNetworkNodes(); + const [ unfoldedNode, setunfoldedNode ] = useState(null); + const sortedNodes = (networkNodes && + Object.entries(networkNodes) + .map(([k, v]) => ({ ...v, hostname: k })) + .sort((a, b) => a.hostname > b.hostname ? -1 : 1)); + + function changeUnfolded(hostname) { + if (unfoldedNode == hostname) { + setunfoldedNode(null); + return; + } + setunfoldedNode(hostname); + } + + return <_NetworkNodes nodes={sortedNodes} isLoading={isLoading} + unfoldedNode={unfoldedNode} onUnfold={changeUnfolded}/>; +} + +export default NetworkNodes; diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesPage.stories.js b/plugins/lime-plugin-network-nodes/src/networkNodesPage.stories.js new file mode 100644 index 000000000..45f7c3763 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesPage.stories.js @@ -0,0 +1,51 @@ +import NetworkNodes, {_NetworkNodes} from './networkNodesPage'; + +export default { + title: 'Containers/networkNodes' +} + +const nodes = [ + { + hostname: 'ql-berta', + ipv4:'10.5.0.16', + ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', + fw_version: 'LibreRouterOS 1.4' + }, + { + hostname: 'ql-nelson', + ipv4:'10.5.0.17', + ipv6: 'fd0d:fe46:8ce8::8bbf:75bf', + board: 'LibreRouter v1', + fw_version: 'LibreRouterOS 1.4' + } +]; + +export const networkNodesNonUnfolded = () => + <_NetworkNodes nodes={nodes} /> + +export const networkNodesOneUnfolded = () => + <_NetworkNodes nodes={nodes} unfoldedNode={'ql-berta'} /> + +export const networkNodesLoading = () => + <_NetworkNodes isLoading={true} /> + +const manyNodes = []; +for (let i = 0; i < 15; i++) { + const hostname = `host${i}`; + const node = {...nodes[0]}; + node.hostname = hostname; + manyNodes.push(node); +} + +export const networkNodesManyNodes = () => + <_NetworkNodes nodes={manyNodes} /> + +export const networkNodesInteractive = () => + +networkNodesInteractive.args = { + queries: [ + [['network-nodes', 'get_nodes'], + Object.fromEntries(nodes.map(n => [n.hostname, n]))] + ] +} \ No newline at end of file diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js b/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js new file mode 100644 index 000000000..720909a9b --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js @@ -0,0 +1,5 @@ +import { useQuery } from 'react-query'; +import { getNodes } from './networkNodesApi'; + +export const useNetworkNodes = () => + useQuery(['network-nodes', 'get_nodes'], getNodes); diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less b/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less new file mode 100644 index 000000000..4342d5e2f --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less @@ -0,0 +1,5 @@ +.title { + font-size: 2em; + padding-top: 1rem; + padding-left: 1rem; +} \ No newline at end of file diff --git a/src/config.js b/src/config.js index 99197c6a1..675c2adcd 100644 --- a/src/config.js +++ b/src/config.js @@ -10,6 +10,7 @@ import NetworkAdmin from '../plugins/lime-plugin-network-admin'; import Firmware from '../plugins/lime-plugin-firmware'; import RemoteSupport from '../plugins/lime-plugin-remotesupport'; import Pirania from '../plugins/lime-plugin-pirania'; +import NetworkNodes from '../plugins/lime-plugin-network-nodes'; // REGISTER PLUGINS export const plugins = [ @@ -24,5 +25,6 @@ export const plugins = [ ChangeNode, RemoteSupport, Pirania, + NetworkNodes, Fbw // fbw does not have menu item ]; From 424a78fb3bc0e586689d18c3cce87754df1f4d2b Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Wed, 21 Apr 2021 17:40:44 -0300 Subject: [PATCH 02/19] chore(translations): spanish for network nodes --- i18n/generic.json | 193 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 i18n/generic.json diff --git a/i18n/generic.json b/i18n/generic.json new file mode 100644 index 000000000..7fddcae1a --- /dev/null +++ b/i18n/generic.json @@ -0,0 +1,193 @@ +{ + "en": { + "a_new_firmware_version_has_been_released_17f89266": "A new firmware version has been released", + "align_11050992": "Align", + "an_error_occurred_a4e1cda4": "An error occurred", + "applying_changes_23ae34f2": "Applying changes.", + "ask_for_remote_support_7e7eaab0": "Ask for remote support", + "at_least_one_alphanumeric_character_357817ee": "At least one alphanumeric character", + "at_least_one_number_bf8434bb": "At least one number", + "best_signal_913e3460": "Best signal", + "cancel_caeb1e68": "Cancel", + "cancel_cd471b5e": "cancel", + "cannot_connect_to_the_remote_support_server_6a95528b": "Cannot connect to the remote support server", + "cannot_load_map_check_your_internet_connection_d24f5daf": "Cannot load map, check your internet connection", + "change_dcaa253a": "Change", + "change_shared_password_58dc580c": "Change Shared Password", + "checking_connection_863b319e": "Checking connection", + "choose_a_name_for_this_node_f491aa8d": "Choose a name for this node", + "choose_a_name_for_your_network_69df41e3": "Choose a name for your network", + "choose_a_shared_password_for_network_administratio_4c98ee0f": "Choose a shared password for network administration", + "click_at_close_session_to_end_the_remote_support_s_1af24bb9": "Click at Close Session to end the remote support session. No one will be able to access your node with this token again", + "click_at_show_console_to_follow_the_remote_support_8b39ccbc": "Click at Show Console to follow the remote support session.", + "close_session_89aadfa2": "Close Session", + "cofirm_upgrade_before_seconds_seconds_or_it_will_b_48b600b6": "Cofirm upgrade before %{seconds} seconds or it will be reverted", + "community_name_115a617": "Community name", + "configure_your_network_7736471d": "Configure your network", + "configure_your_new_community_network_a6daad12": "Configure your new community network", + "confirm_6556b3a6": "Confirm", + "confirm_location_2fe5ae11": "confirm location", + "congratulations_ffe43bf9": "Congratulations", + "count_days_de0c6a32": { + "one": "1 days", + "other": "%{count} days" + }, + "count_hours_1bd03883": { + "one": "1 hours", + "other": "%{count} hours" + }, + "count_minutes_a6eeeacb": { + "one": "1 minutes", + "other": "%{count} minutes" + }, + "count_people_join_sessions_c24dac9c": { + "one": "1 people-join-session", + "other": "%{count} people-join-sessions" + }, + "count_seconds_2953a98e": { + "one": "1 seconds", + "other": "%{count} seconds" + }, + "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:", + "device_95d26d94": "Device", + "don_t_show_this_message_again_9950c20": "Don't show this message again", + "download_c7ffdfb9": "Download", + "downloading_1e41f805": "Downloading", + "edit_location_420eadc4": "edit location", + "error_98e81528": "Error", + "fetching_name_96831fa4": "Fetching name", + "filename_83eeb4ac": "Filename", + "firmware_6a098a0d": "Firmware", + "from_fdd4956d": "From", + "full_path_metrics_2859608f": "Full path metrics", + "go_64ecd1fd": "Go!", + "go_to_community_view_d12b8d67": "Go to Community View", + "go_to_node_view_26ba929d": "Go to Node View", + "ground_routing_12ab04c9": "Ground Routing", + "ground_routing_configuration_3f4fa9c1": "Ground Routing configuration", + "hide_community_773b3f33": "hide community", + "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", + "interface_177dac54": "Interface", + "internet_connection_fda60ffa": "Internet connection", + "ip_addresses_440ac240": "IP Addresses", + "join_the_mesh_653219c6": "Join the mesh", + "last_known_internet_path_45f31c9a": "last_known_internet_path", + "last_packet_82ee8e9d": "Last packet", + "load_last_known_internet_path_677f6229": "load_last_known_internet_path", + "loading_node_status_547ed318": "Loading node status...", + "locate_my_node_b91489b": "locate my node", + "logging_in_1164a773": "Logging in", + "login_6f3d6249": "Login", + "map_eb9418c7": "Map", + "metrics_c80fba05": "Metrics", + "metrics_status_gateway_2a77a113": "metrics_status_gateway", + "metrics_status_path_905a8d22": "metrics_status_path", + "metrics_status_stations_464641e8": "metrics_status_stations", + "more_details_on_the_release_can_be_found_at_dfc8f165": "More details on the release can be found at:", + "more_info_at_117c8533": "More info at:", + "more_than_10_characters_15a6e3bf": "More than 10 characters", + "more_than_a_minute_ago_a2a28531": "more than a minute ago", + "most_active_2d5a3cae": "Most Active", + "must_select_a_network_and_a_valid_hostname_ea82e72c": "Must select a network and a valid hostname", + "network_configuration_ea7f4215": "Network Configuration", + "network_nodes_4368eb67": "Network Nodes", + "no_network_found_try_realigning_your_node_and_resc_176a9b3e": "No network found, try realigning your node and rescanning.", + "node_configuration_7342e6f5": "Node Configuration", + "notes_c42e0fd5": "Notes", + "notes_of_a44a4158": "Notes of", + "ok_ff1b646a": "Ok", + "on_its_radio_radio_f32d79ce": "On its radio %{radio}", + "only_gateway_727b1656": "Only gateway", + "or_choose_a_firmware_image_from_your_device_d56be2d8": "Or choose a firmware image from your device", + "or_upgrade_to_latest_release_e062ddee": "Or upgrade to latest release", + "packet_loss_1afe48a8": "Packet loss", + "password_8a271b1c": "Password", + "please_configure_your_network_d6eb8b76": "Please configure your network", + "please_select_a_file_b49d6bf4": "Please select a file", + "please_select_a_sh_or_bin_file_4004723": "Please select a .sh or .bin file", + "please_verify_your_internet_connection_92ecd88c": "Please verify your internet connection", + "please_wait_62914c7c": "Please wait", + "please_wait_patiently_for_seconds_seconds_and_do_n_b98cfb66": "Please wait patiently for %{seconds} seconds and do not disconnect the device.", + "please_wait_while_the_device_reboots_and_reload_th_67bd290d": "Please wait while the device reboots, and reload the app", + "radio_2573b256": "Radio", + "re_enter_password_49757ed": "Re-enter Password", + "re_enter_the_shared_password_20f09406": "Re-enter the shared password", + "reload_3e45154f": "Reload", + "reload_page_2d381199": "Reload page", + "remote_support_9ba7a3a7": "Remote Support", + "rescan_dff042fc": "Rescan", + "retry_ebd5f8ba": "Retry", + "revert_702e7694": "Revert", + "reverting_to_previous_version_e6e43529": "Reverting to previous version", + "save_notes_616850ea": "Save notes", + "scan_for_existing_networks_f7f485c": "Scan for existing networks", + "scanning_for_existing_networks_195ddb9b": "Scanning for existing networks", + "seconds_aee2098": "seconds", + "seconds_seconds_ago_699b6316": "%{seconds} seconds ago", + "see_more_b24a4422": "See More", + "select_a_network_to_join_b7040672": "Select a network to join", + "select_another_node_and_use_the_limeapp_as_you_wer_d189728": "Select another node and use the LimeApp as you were there", + "select_file_71aa4113": "Select file", + "select_new_node_5b2e9165": "Select new node", + "select_one_b647b384": "Select one", + "set_network_bcd0ea96": "Set network", + "setting_network_21ebac51": "Setting network", + "setting_up_new_password_4daf8f1c": "Setting up new password", + "share_the_following_command_with_whoever_you_want__6fd30335": "Share the following command with whoever you want to give them access to your node", + "shared_password_changed_successfully_b2820acc": "Shared Password changed successfully", + "shared_password_dac7c19d": "Shared Password", + "show_community_42f3833": "show community", + "show_console_5d6937ac": "Show Console", + "signal_lost_690073": "Signal lost", + "size_b30e1077": "Size", + "station_name_7d67417c": "Station name", + "status_e7fdbe06": "Status", + "system_55b0ca91": "System", + "the_are_not_mesh_interfaces_available_4055abd7": "The are not mesh interfaces available", + "the_download_failed_130e1274": "The download failed", + "the_firmware_is_being_upgraded_f3881802": "The firmware is being upgraded...", + "the_password_should_have_b9f88155": "The password should have:", + "the_passwords_do_not_match_62d77c67": "The passwords do not match!", + "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_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", + "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", + "this_radio_is_not_associated_with_other_nodes_6722a471": "This radio is not associated with other nodes", + "to_internet_494eb85c": "To Internet", + "to_keep_the_current_configuration_or_ab76f6d1": "to keep the current configuration. Or ...", + "to_the_previous_configuration_bf087867": "to the previous configuration", + "traffic_bfe536d2": "Traffic", + "try_reloading_the_app_4e4c3a66": "Try reloading the app", + "upgrade_5de364f8": "Upgrade", + "upgrade_now_f300d697": "Upgrade Now", + "upgrade_to_lastest_firmware_version_9b159910": "Upgrade to lastest firmware version", + "upgrade_to_versionname_621a0b6a": "Upgrade to %{versionName}", + "upload_firmware_image_from_your_device_57327bee": "Upload firmware image from your device", + "uptime_c1d2415d": "Uptime", + "versionname_is_now_available_a6fbbb63": "%{versionName} is now available", + "visit_864b4060": "Visit", + "visit_a_neighboring_node_4116be4": "Visit a neighboring node", + "when_reloading_the_app_you_will_be_asked_to_confir_f9ecb33e": "When reloading the app you will be asked to confirm the upgrade, otherwise it will be reverted", + "with_radio_radio_alignin_with_531510d": "With radio %{radio} alignin with", + "wrong_password_try_again_3100aecf": "Wrong password, try again", + "you_are_connected_to_another_node_in_the_network_t_a423710a": "You are connected to another node in the network, try connecting to", + "you_are_now_part_of_90f2585a": "You are now part of ", + "you_can_search_for_mesh_networks_around_you_to_add_e6fbf1c5": "You can search for mesh networks around you to add or to create a new one.", + "you_can_upgrade_to_7af1ea19": "You can upgrade to:", + "you_don_t_go_through_any_paths_to_get_here_25203ed3": "You don't go through any paths to get here.", + "you_have_successfuly_connected_to_ddb8c613": "You have successfuly connected to", + "you_need_to_know_the_shared_password_to_enter_this_4b0c4ec1": "You need to know the shared password to enter this page", + "you_should_try_to_connect_to_the_network_network_8d7f515e": "You should try to connect to the network %{network}.", + "your_router_has_not_yet_been_configured_you_can_us_27c91373": "Your router has not yet been configured, \n\t\t\tyou can use our wizard to incorporate it into an existing network or create a new one.\n\t\t\tIf you ignore this message it will continue to work with the default configuration." + } +} \ No newline at end of file From dc1e9a2667e1a7506538eefe693fa3e418103ce5 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Wed, 21 Apr 2021 18:31:57 -0300 Subject: [PATCH 03/19] improvement(network-node): center title --- plugins/lime-plugin-network-nodes/src/networkNodesStyle.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less b/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less index 4342d5e2f..81dacaf66 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less +++ b/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less @@ -1,5 +1,5 @@ .title { + text-align: center; font-size: 2em; padding-top: 1rem; - padding-left: 1rem; } \ No newline at end of file From 0c045cabb76b3b94695530b0a6f48ad7f02222e9 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Wed, 21 Apr 2021 18:32:52 -0300 Subject: [PATCH 04/19] improvement(network-nodes): filter gones and sort --- bkp/src/networkFirmwaresStyle.less | 1 + .../networkNodes.spec.js | 21 ++++++++++++++++--- .../src/networkNodesPage.js | 3 ++- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 bkp/src/networkFirmwaresStyle.less diff --git a/bkp/src/networkFirmwaresStyle.less b/bkp/src/networkFirmwaresStyle.less new file mode 100644 index 000000000..53a06c08a --- /dev/null +++ b/bkp/src/networkFirmwaresStyle.less @@ -0,0 +1 @@ +// Here you define the css for this plugin \ No newline at end of file diff --git a/plugins/lime-plugin-network-nodes/networkNodes.spec.js b/plugins/lime-plugin-network-nodes/networkNodes.spec.js index 261b81f59..8cfbf48c7 100644 --- a/plugins/lime-plugin-network-nodes/networkNodes.spec.js +++ b/plugins/lime-plugin-network-nodes/networkNodes.spec.js @@ -19,13 +19,22 @@ describe('networkNodes', () => { ipv4: '10.5.0.16', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', board: 'LibreRouter v1', - fw_version: 'LibreRouterOS 1.4' + fw_version: 'LibreRouterOS 1.4', + status: 'recently_connected' }, "ql-nelson": { ipv4: '10.5.0.17', ipv6: 'fd0d:fe46:8ce8::8bbf:75bf', board: 'LibreRouter v1', - fw_version: 'LibreRouterOS 1.4' + fw_version: 'LibreRouterOS 1.4', + status: 'disconnected' + }, + "ql-gone-node": { + ipv4: '10.5.0.18', + ipv6: 'fd0d:fe46:8ce8::8bbf:75be', + board: 'LibreRouter v1', + fw_version: 'LibreRouterOS 1.4', + status: 'gone' } })); }); @@ -35,7 +44,7 @@ describe('networkNodes', () => { act(() => queryCache.clear()); }); - it('test that nodes are shown', async () => { + it('test that nodes recently_connected and connected nodes are shown', async () => { render(); expect(await screen.findByText('ql-nelson')).toBeInTheDocument(); expect(await screen.findByText('ql-berta')).toBeInTheDocument(); @@ -49,6 +58,12 @@ describe('networkNodes', () => { expect(await screen.findByText('IPv6: fd0d:fe46:8ce8::8bbf:75bf')).toBeInTheDocument(); expect(await screen.findByText('Device: LibreRouter v1')).toBeInTheDocument(); expect(await screen.findByText('Firmware: LibreRouterOS 1.4')).toBeInTheDocument(); + }); + + it('test that gone nodes are not shown', async () => { + render(); + await screen.findByText('ql-nelson'); + expect(screen.queryByText('ql-gone-node')).toBeNull(); }) }); diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesPage.js b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js index 9d20e0d4a..5a72fcdf6 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesPage.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js @@ -32,7 +32,8 @@ const NetworkNodes = () => { const sortedNodes = (networkNodes && Object.entries(networkNodes) .map(([k, v]) => ({ ...v, hostname: k })) - .sort((a, b) => a.hostname > b.hostname ? -1 : 1)); + .filter(n => n.status !== 'gone') + .sort((a, b) => a.hostname > b.hostname)); function changeUnfolded(hostname) { if (unfoldedNode == hostname) { From 3a501d3c1761d879a3375747e711e479134389de Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Wed, 21 Apr 2021 18:42:30 -0300 Subject: [PATCH 05/19] improvement(expandable-node): let user copypaste details --- .../src/components/expandableNode/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js index 2bc67362b..8624eb5dd 100644 --- a/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js +++ b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js @@ -12,7 +12,7 @@ export const ExpandableNode = ({ node, showMore, onClick }) => {
{hostname}
{showMore && -
+
e.stopPropagation()}> {ipv4 &&
IPv4: {ipv4}
} {ipv6 &&
IPv6: {ipv6}
} {board &&
{I18n.t('Device')}: {board}
} From 764d170d5b7c36badc7ba7845f3e2ef9b95b5204 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Wed, 21 Apr 2021 19:33:21 -0300 Subject: [PATCH 06/19] 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 | 67 +++++++++ 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 ++ 14 files changed, 551 insertions(+) create mode 100644 i18n/translations/en.json 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 7fddcae1a..83695d01b 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 new file mode 100644 index 000000000..0eeaca9ad --- /dev/null +++ b/i18n/translations/en.json @@ -0,0 +1,67 @@ +{ + "align_11050992" :"Align", + "back_to_base_443797cb" :"Back to base", + "base_host_a17d45a4" :"Base Host", + "change_dcaa253a" :"Change", + "connected_host_91e11459": "Connected Host", + "config_4877f466" :"Config", + "current_status_830c5a75" :"Current status", + "from_fdd4956d" :"From", + "full_path_metrics_2859608f" :"Full path metrics", + "interface_177dac54" :"Interface", + "internet_connection_fda60ffa" :"Internet connection", + "ip_addresses_440ac240" :"IP Addresses", + "load_last_known_internet_path_677f6229" :"Load last known Internet path", + "loading_node_status_547ed318" :"Loading node status...", + "locate_5f6685db" :"Locate", + "metrics_c80fba05" :"Metrics", + "metrics_status_gateway_2a77a113" :"Searching gateway", + "metrics_status_path_905a8d22" :"Calculating network path", + "metrics_status_stations_464641e8" :"Measuring links", + "most_active_2d5a3cae" :"Most Active", + "move_to_new_position_eb97c4c3" :"MOVE TO NEW POSITION", + "notes_c42e0fd5" :"Notes", + "notes_of_a44a4158" :"Notes of", + "only_gateway_727b1656" :"Only gateway", + "packet_loss_1afe48a8": "Packet loss", + "save_notes_616850ea" :"Save notes", + "select_new_base_station_3652ae73" :"Select new base station", + "station_75bce853" :"Station", + "stations_18122820" :"Stations", + "status_e7fdbe06" :"Status", + "system_55b0ca91" :"System", + "to_internet_494eb85c" :"To Internet", + "traffic_bfe536d2" :"Traffic", + "connection_fail_57f84354": "Connection to %{meta_ws} fail", + "trying_to_connect_ff82bf9f" :"Trying to connect to %{meta_ws}", + "try_thisnode_info_1ee1bfe2" :"Try thisnode.info", + "uptime_c1d2415d" :"Uptime", + "interfaces_44f8a99c": "Interfaces", + "last_known_internet_path_45f31c9a": "This your last working path to the Internet", + "you_should_try_to_connect_to_the_network_network_8d7f515e": "You should try to connect to the wifi network %{network}.", + "count_days_de0c6a32": { + "one": "day", + "other": "days" + }, + "count_hours_1bd03883": { + "one": "hour", + "other": "hours" + }, + "count_minutes_a6eeeacb": { + "one": "minute", + "other": "minutes" + }, + "count_seconds_2953a98e": { + "one": "second", + "other": "seconds" + }, + "count_people_join_sessions_c24dac9c": { + "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/plugins/lime-plugin-reachable-nodes/index.js b/plugins/lime-plugin-reachable-nodes/index.js new file mode 100644 index 000000000..6159bd1d4 --- /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 000000000..cfc9fb3bf --- /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 000000000..86ada871b --- /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 000000000..f6171fcc4 --- /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 000000000..a279dd8c7 --- /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 000000000..6e844dce3 --- /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 000000000..060496ce6 --- /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 000000000..bc1e2fef9 --- /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 000000000..ba59c9d47 --- /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 000000000..55cd73d1a --- /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 000000000..fb9b8b764 --- /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 000000000..b3ea14459 --- /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 From 6ac60d17a2b8369db9ee31aa1d19a02a54a11a9b Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Thu, 22 Apr 2021 16:34:15 -0300 Subject: [PATCH 07/19] chore(delete-nodes): separate into its own plugin Currently each plugin match one to one with a menu item. We want one menu item for deleting nodes, so a plugin for it is created. In the future it will be usefull to keep related functionality in the same plugin, and allow them to have more than one menu item associated. --- bkp/src/networkFirmwaresStyle.less | 1 - i18n/generic.json | 5 +- .../deleteNodes.spec.js | 78 +++++++++++++++++++ .../deleteNodes.stories.js | 21 +++++ plugins/lime-plugin-delete-nodes/index.js | 9 +++ .../src/deleteNodesMenu.js | 8 ++ .../src}/deleteNodesPage.js | 44 +++++------ .../src/deleteNodesStyle.less} | 0 .../src/networkNodesApi.js | 8 +- .../src/networkNodesApi.spec.js | 14 ++++ .../src/networkNodesQueries.js | 16 +++- .../src/containers/deleteNodesPage/index.js | 2 - 12 files changed, 175 insertions(+), 31 deletions(-) delete mode 100644 bkp/src/networkFirmwaresStyle.less create mode 100644 plugins/lime-plugin-delete-nodes/deleteNodes.spec.js create mode 100644 plugins/lime-plugin-delete-nodes/deleteNodes.stories.js create mode 100644 plugins/lime-plugin-delete-nodes/index.js create mode 100644 plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js rename plugins/{lime-plugin-reachable-nodes/src/containers/deleteNodesPage => lime-plugin-delete-nodes/src}/deleteNodesPage.js (71%) rename plugins/{lime-plugin-reachable-nodes/src/containers/deleteNodesPage/style.less => lime-plugin-delete-nodes/src/deleteNodesStyle.less} (100%) delete mode 100644 plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/index.js diff --git a/bkp/src/networkFirmwaresStyle.less b/bkp/src/networkFirmwaresStyle.less deleted file mode 100644 index 53a06c08a..000000000 --- a/bkp/src/networkFirmwaresStyle.less +++ /dev/null @@ -1 +0,0 @@ -// Here you define the css for this plugin \ No newline at end of file diff --git a/i18n/generic.json b/i18n/generic.json index 83695d01b..e86f61e09 100644 --- a/i18n/generic.json +++ b/i18n/generic.json @@ -132,6 +132,7 @@ "reload_3e45154f": "Reload", "reload_page_2d381199": "Reload page", "remote_support_9ba7a3a7": "Remote Support", + "remove_nodes_2dd26e14": "Remove Nodes", "rescan_dff042fc": "Rescan", "retry_ebd5f8ba": "Retry", "revert_702e7694": "Revert", @@ -147,7 +148,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", + "select_the_nodes_which_no_longer_belong_to_the_net_92f853ef": "Select the nodes which no longer belong to the network and delete them from the list of unreachable nodes", "set_network_bcd0ea96": "Set network", "setting_network_21ebac51": "Setting network", "setting_up_new_password_4daf8f1c": "Setting up new password", @@ -170,7 +171,7 @@ "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_are_no_left_unreachable_nodes_c0bec63d": "There are no left unreachable 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", diff --git a/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js b/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js new file mode 100644 index 000000000..060f10aa3 --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js @@ -0,0 +1,78 @@ +import { h } from 'preact'; +import { fireEvent, act, screen } from '@testing-library/preact'; +import '@testing-library/jest-dom'; +import waitForExpect from 'wait-for-expect'; + +import DeleteNodesPage from './src/deleteNodesPage'; +import queryCache from 'utils/queryCache'; +import { getNodes, markNodesAsGone } from 'plugins/lime-plugin-network-nodes/src/networkNodesApi'; +import { render } from 'utils/test_utils'; + +jest.mock('plugins/lime-plugin-network-nodes/src/networkNodesApi'); + +describe('delete nodes page', () => { + beforeEach(() => { + getNodes.mockImplementation(async () => [ + { hostname: 'node1', status: 'recently_connected' }, + { hostname: 'node2', status: 'recently_connected' }, + { hostname: 'node3', status: 'recently_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()); + getNodes.mockClear(); + markNodesAsGone.mockClear(); + }); + + 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-delete-nodes/deleteNodes.stories.js b/plugins/lime-plugin-delete-nodes/deleteNodes.stories.js new file mode 100644 index 000000000..a8ae2df8c --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/deleteNodes.stories.js @@ -0,0 +1,21 @@ +import { DeleteNodesPage_ } from './src/deleteNodesPage'; + +export default { + title: 'Containers/Remove Nodes' +}; + +const nodes = [ + { hostname: "ql-refu-bbone", status: "unreachable" }, + { hostname: "si-soniam", status: "unreachable" }, + { hostname: "si-giordano", status: "unreachable" }, + { hostname: "si-mario", status: "unreachable" }, + { hostname: "si-manu", status: "unreachable" }, +]; + +export const deleteNodesPage = (args) => ( + +); + +deleteNodesPage.argTypes = { + onDelete: { action: 'deleted' } +}; \ No newline at end of file diff --git a/plugins/lime-plugin-delete-nodes/index.js b/plugins/lime-plugin-delete-nodes/index.js new file mode 100644 index 000000000..353894016 --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/index.js @@ -0,0 +1,9 @@ +import Page from './src/deleteNodesPage'; +import Menu from './src/deleteNodesMenu'; + +export default { + name: 'deleteNodes', + page: Page, + menu: Menu, + isCommunityProtected: false +}; diff --git a/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js b/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js new file mode 100644 index 000000000..e3279673d --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js @@ -0,0 +1,8 @@ +import { h } from 'preact'; +import I18n from 'i18n-js'; + +const Menu = () => ( + {I18n.t('Remove Nodes')} +); + +export default Menu; diff --git a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js similarity index 71% rename from plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js rename to plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js index f6171fcc4..578ef8342 100644 --- a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js @@ -4,7 +4,7 @@ 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 { useMarkNodesAsGone, useNetworkNodes } from 'plugins/lime-plugin-network-nodes/src/networkNodesQueries' import style from './style.less'; import I18n from 'i18n-js'; @@ -29,10 +29,10 @@ export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) =

{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")}

+ + "delete them from the list of unreachable nodes")}

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

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

+

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

} {disconnectedNodes.map(node => @@ -46,25 +46,25 @@ export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) = )}
- {selectedNodes.size >= 1 && -
- - {[selectedNodes.size, - I18n.t('selected-nodes', { count: selectedNodes.size }) - ].join(' ')} - - {!isSubmitting && - - } - {isSubmitting && -
- -
- } -
- } +
+ + {[selectedNodes.size, + I18n.t('selected-nodes', { count: selectedNodes.size }) + ].join(' ')} + + {!isSubmitting && + + } + {isSubmitting && +
+ +
+ } +
{showSuccess && } diff --git a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/style.less b/plugins/lime-plugin-delete-nodes/src/deleteNodesStyle.less similarity index 100% rename from plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/style.less rename to plugins/lime-plugin-delete-nodes/src/deleteNodesStyle.less diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesApi.js b/plugins/lime-plugin-network-nodes/src/networkNodesApi.js index a2d02a0b6..38feb6ce6 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesApi.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.js @@ -2,6 +2,10 @@ import api from 'utils/uhttpd.service'; export const getNodes = () => api.call('network-nodes', 'get_nodes', {}).toPromise() - .then(res => res.nodes); + .then(res => { + return res.nodes; + }); -export const markNodesAsGone = () => api.call('network-nodes', 'mark_nodes_as_gone', {}).toPromise(); +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-network-nodes/src/networkNodesApi.spec.js b/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js index 69fef588c..c5ee29040 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js @@ -32,3 +32,17 @@ describe('getNodes', () => { expect(await getNodes()).toEqual(nodes); }); }); + +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']) + }) +}); \ No newline at end of file diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js b/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js index 720909a9b..0e82bc51b 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js @@ -1,5 +1,17 @@ -import { useQuery } from 'react-query'; -import { getNodes } from './networkNodesApi'; +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/containers/deleteNodesPage/index.js b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/index.js deleted file mode 100644 index a279dd8c7..000000000 --- a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './deleteNodesPage'; -export { default } from './deleteNodesPage'; \ No newline at end of file From 69b237273ee3abcf0136c59b54b51e2491d3e622 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Thu, 22 Apr 2021 16:37:25 -0300 Subject: [PATCH 08/19] chore(reachable-nodes): add to menu --- plugins/lime-plugin-delete-nodes/index.js | 2 +- src/config.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/lime-plugin-delete-nodes/index.js b/plugins/lime-plugin-delete-nodes/index.js index 353894016..d21ee4664 100644 --- a/plugins/lime-plugin-delete-nodes/index.js +++ b/plugins/lime-plugin-delete-nodes/index.js @@ -5,5 +5,5 @@ export default { name: 'deleteNodes', page: Page, menu: Menu, - isCommunityProtected: false + isCommunityProtected: true }; diff --git a/src/config.js b/src/config.js index 675c2adcd..89eb8ea81 100644 --- a/src/config.js +++ b/src/config.js @@ -11,6 +11,7 @@ import Firmware from '../plugins/lime-plugin-firmware'; import RemoteSupport from '../plugins/lime-plugin-remotesupport'; import Pirania from '../plugins/lime-plugin-pirania'; import NetworkNodes from '../plugins/lime-plugin-network-nodes'; +import DeleteNodes from '../plugins/lime-plugin-delete-nodes'; // REGISTER PLUGINS export const plugins = [ From de0781eec60d862ea476d2bebd17b3bbd06d318c Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Thu, 22 Apr 2021 16:56:23 -0300 Subject: [PATCH 09/19] chore(delete-nodes): move to community view --- plugins/lime-plugin-delete-nodes/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/lime-plugin-delete-nodes/index.js b/plugins/lime-plugin-delete-nodes/index.js index d21ee4664..d4a606bb4 100644 --- a/plugins/lime-plugin-delete-nodes/index.js +++ b/plugins/lime-plugin-delete-nodes/index.js @@ -5,5 +5,6 @@ export default { name: 'deleteNodes', page: Page, menu: Menu, - isCommunityProtected: true + isCommunityProtected: true, + menuView: 'community' }; From d257181f3fc821e421b462fa06dc54f25b840cd6 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Thu, 22 Apr 2021 17:39:59 -0300 Subject: [PATCH 10/19] chore(delete-nodes): rename to [un]reachable --- .../deleteNodes.spec.js | 18 +++++++++--------- .../src/deleteNodesPage.js | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js b/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js index 060f10aa3..9dfe24fcf 100644 --- a/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js +++ b/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js @@ -13,13 +13,13 @@ jest.mock('plugins/lime-plugin-network-nodes/src/networkNodesApi'); describe('delete nodes page', () => { beforeEach(() => { getNodes.mockImplementation(async () => [ - { hostname: 'node1', status: 'recently_connected' }, - { hostname: 'node2', status: 'recently_connected' }, - { hostname: 'node3', status: 'recently_connected' }, - { hostname: 'node4', status: 'disconnected' }, - { hostname: 'node5', status: 'disconnected' }, - { hostname: 'node6', status: 'disconnected' }, - { hostname: 'node7', status: 'disconnected' }, + { hostname: 'node1', status: 'recently_reachable' }, + { hostname: 'node2', status: 'recently_reachable' }, + { hostname: 'node3', status: 'recently_reachable' }, + { hostname: 'node4', status: 'unreachable' }, + { hostname: 'node5', status: 'unreachable' }, + { hostname: 'node6', status: 'unreachable' }, + { hostname: 'node7', status: 'unreachable' }, { hostname: 'node8', status: 'gone' }, { hostname: 'node9', status: 'gone' }, ]); @@ -32,7 +32,7 @@ describe('delete nodes page', () => { markNodesAsGone.mockClear(); }); - it('shows the list of disconnected nodes only', async () => { + it('shows the list of unreachable nodes only', async () => { render(); expect(await screen.findByText('node4')).toBeVisible(); expect(await screen.findByText('node5')).toBeVisible(); @@ -75,4 +75,4 @@ describe('delete nodes page', () => { 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-delete-nodes/src/deleteNodesPage.js b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js index 578ef8342..f0417d7dd 100644 --- a/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js @@ -5,13 +5,13 @@ import Toast from 'components/toast'; import { useEffect, useState } from 'preact/hooks'; import { useSet } from 'react-use'; import { useMarkNodesAsGone, useNetworkNodes } from 'plugins/lime-plugin-network-nodes/src/networkNodesQueries' -import style from './style.less'; +import style from './deleteNodesStyle.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"); + const unreachableNodes = nodes.filter(n => n.status === "unreachable"); useEffect(() => { if (isSuccess) { @@ -27,15 +27,15 @@ export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) =

{I18n.t("Delete Nodes")}

- {disconnectedNodes.length > 0 && + {unreachableNodes.length > 0 &&

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

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

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

} - {disconnectedNodes.map(node => + {unreachableNodes.map(node => toggle(node.hostname)} >
{ isSubmitting={isSubmitting} isSuccess={isSuccess} /> } -export default DeleteNodesPage; \ No newline at end of file +export default DeleteNodesPage; From 86a725b36d5e2fee2996170ad2378c06f6572e91 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Thu, 22 Apr 2021 17:41:47 -0300 Subject: [PATCH 11/19] chore(delete-nodes): add to menu --- src/config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.js b/src/config.js index 89eb8ea81..8f371914e 100644 --- a/src/config.js +++ b/src/config.js @@ -27,5 +27,6 @@ export const plugins = [ RemoteSupport, Pirania, NetworkNodes, + DeleteNodes, Fbw // fbw does not have menu item ]; From 65621b5af09ea662bda236c84e7eba38391e39aa Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Thu, 22 Apr 2021 18:20:55 -0300 Subject: [PATCH 12/19] feat(reachable-nodes): add screen for reachable/unreachable nodes And complete translations --- i18n/generic.json | 16 +-- .../src/deleteNodesMenu.js | 2 +- plugins/lime-plugin-reachable-nodes/index.js | 10 +- .../networkNodes.spec.js | 134 ------------------ .../networkNodes.stories.js | 34 ----- .../reachableNodes.spec.js | 68 +++++++++ .../reachableNodes.stories.js | 29 ++++ .../src/networkNodesApi.js | 9 -- .../src/networkNodesApi.spec.js | 49 ------- .../src/networkNodesMenu.js | 7 - .../src/networkNodesPage.js | 85 ----------- .../src/networkNodesQueries.js | 16 --- .../src/reachableNodesMenu.js | 7 + .../src/reachableNodesPage.js | 83 +++++++++++ .../src/style.less | 15 -- src/components/help/style.less | 2 +- src/config.js | 1 + 17 files changed, 200 insertions(+), 367 deletions(-) delete mode 100644 plugins/lime-plugin-reachable-nodes/networkNodes.spec.js delete mode 100644 plugins/lime-plugin-reachable-nodes/networkNodes.stories.js create mode 100644 plugins/lime-plugin-reachable-nodes/reachableNodes.spec.js create mode 100644 plugins/lime-plugin-reachable-nodes/reachableNodes.stories.js delete mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js delete mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesApi.spec.js delete mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesMenu.js delete mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesPage.js delete mode 100644 plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/reachableNodesMenu.js create mode 100644 plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js delete mode 100644 plugins/lime-plugin-reachable-nodes/src/style.less diff --git a/i18n/generic.json b/i18n/generic.json index e86f61e09..8497de204 100644 --- a/i18n/generic.json +++ b/i18n/generic.json @@ -28,8 +28,6 @@ "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" @@ -61,8 +59,6 @@ "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", @@ -75,7 +71,6 @@ "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", @@ -83,7 +78,6 @@ "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", @@ -129,10 +123,11 @@ "radio_2573b256": "Radio", "re_enter_password_49757ed": "Re-enter Password", "re_enter_the_shared_password_20f09406": "Re-enter the shared password", + "reachable_howmany_6f891e31": "Reachable (%{howMany})", + "reachable_nodes_748c93f0": "Reachable Nodes", "reload_3e45154f": "Reload", "reload_page_2d381199": "Reload page", "remote_support_9ba7a3a7": "Remote Support", - "remove_nodes_2dd26e14": "Remove Nodes", "rescan_dff042fc": "Rescan", "retry_ebd5f8ba": "Retry", "revert_702e7694": "Revert", @@ -175,10 +170,11 @@ "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.", + "these_are_the_nodes_that_can_be_reached_from_your__4c524abe": "These are the nodes that can be reached from your node, i.e. there is a working path from your node to each of them.", + "these_are_the_nodes_that_can_t_be_reached_from_you_dbbf9032": "These are the nodes that can't be reached from your node, it is possible that they are not turned on or a link to reach them is down.", "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_information_is_synced_periodically_and_can_be_8b74cb8c": "This information is synced periodically and can be outdated by some minutes", "this_node_is_the_gateway_1e20aaff": "This node is the gateway", "this_radio_is_not_associated_with_other_nodes_6722a471": "This radio is not associated with other nodes", "to_internet_494eb85c": "To Internet", @@ -186,6 +182,8 @@ "to_the_previous_configuration_bf087867": "to the previous configuration", "traffic_bfe536d2": "Traffic", "try_reloading_the_app_4e4c3a66": "Try reloading the app", + "unreachable_howmany_e5c8f844": "Unreachable (%{howMany})", + "unreachable_nodes_e6785f10": "Unreachable Nodes", "upgrade_5de364f8": "Upgrade", "upgrade_now_f300d697": "Upgrade Now", "upgrade_to_lastest_firmware_version_9b159910": "Upgrade to lastest firmware version", diff --git a/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js b/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js index e3279673d..091be2c84 100644 --- a/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js @@ -2,7 +2,7 @@ import { h } from 'preact'; import I18n from 'i18n-js'; const Menu = () => ( - {I18n.t('Remove Nodes')} + {I18n.t('Delete Nodes')} ); export default Menu; diff --git a/plugins/lime-plugin-reachable-nodes/index.js b/plugins/lime-plugin-reachable-nodes/index.js index 6159bd1d4..f378f77b3 100644 --- a/plugins/lime-plugin-reachable-nodes/index.js +++ b/plugins/lime-plugin-reachable-nodes/index.js @@ -1,13 +1,9 @@ -import Page from './src/networkNodesPage'; -import { NetworkNodesMenu } from './src/networkNodesMenu'; -import DeleteNodesPage from './src/containers/deleteNodesPage'; +import Page from './src/reachableNodesPage'; +import { ReachableNodesMenu } from './src/reachableNodesMenu'; export default { name: 'NetworkNodes', page: Page, - menu: NetworkNodesMenu, + menu: ReachableNodesMenu, 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 deleted file mode 100644 index cfc9fb3bf..000000000 --- a/plugins/lime-plugin-reachable-nodes/networkNodes.spec.js +++ /dev/null @@ -1,134 +0,0 @@ -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 deleted file mode 100644 index 86ada871b..000000000 --- a/plugins/lime-plugin-reachable-nodes/networkNodes.stories.js +++ /dev/null @@ -1,34 +0,0 @@ -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/reachableNodes.spec.js b/plugins/lime-plugin-reachable-nodes/reachableNodes.spec.js new file mode 100644 index 000000000..be0d33712 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/reachableNodes.spec.js @@ -0,0 +1,68 @@ +import { h } from 'preact'; +import { fireEvent, act, screen } from '@testing-library/preact'; +import '@testing-library/jest-dom'; + +import ReachableNodesPage from './src/reachableNodesPage'; +import queryCache from 'utils/queryCache'; +import { getNodes, markNodesAsGone } from 'plugins/lime-plugin-network-nodes/src/networkNodesApi'; +import { render } from 'utils/test_utils'; + +jest.mock('plugins/lime-plugin-network-nodes/src/networkNodesApi'); + +beforeEach(() => { + getNodes.mockImplementation(async () => [ + { hostname: 'node1', status: 'recently_reachable' }, + { hostname: 'node2', status: 'recently_reachable' }, + { hostname: 'node3', status: 'recently_reachable' }, + { hostname: 'node4', status: 'unreachable' }, + { hostname: 'node5', status: 'unreachable' }, + { hostname: 'node6', status: 'unreachable' }, + { hostname: 'node7', status: 'unreachable' }, + { hostname: 'node8', status: 'gone' }, + { hostname: 'node9', status: 'gone' }, + ]); + markNodesAsGone.mockImplementation(async () => []); +}); + +afterEach(() => { + act(() => queryCache.clear()); +}); + +describe('network nodes screen', () => { + it('shows one tab for reachable nodes and one for unreachable nodes with length', async () => { + render(); + expect(await screen.findByRole('tab', { name: /^reachable \(3\)/i })).toBeVisible(); + expect(await screen.findByRole('tab', { name: /^unreachable \(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: /^unreachable \(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 help message when clicking on help button', async () => { + render(); + const helpButton = await screen.findByLabelText('help'); + fireEvent.click(helpButton); + expect(await screen.findByText("Reachable Nodes")).toBeVisible(); + expect(await screen.findByText("These are the nodes that can be reached from your node, " + + "i.e. there is a working path from your node to each of them." + + "This information is synced periodically " + + "and can be outdated by some minutes")).toBeVisible(); + expect(await screen.findByText("Unreachable Nodes")).toBeVisible(); + expect(await screen.findByText("These are the nodes that can't be reached from your node, " + + "it is possible that they are not turned on or a link to reach them is down.")).toBeVisible(); + }) +}); diff --git a/plugins/lime-plugin-reachable-nodes/reachableNodes.stories.js b/plugins/lime-plugin-reachable-nodes/reachableNodes.stories.js new file mode 100644 index 000000000..195b32265 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/reachableNodes.stories.js @@ -0,0 +1,29 @@ +import { ReachableNodesPage_ } from "./src/reachableNodesPage"; + +export default { + title: 'Containers/ReachableNodes', +}; + +const nodes = [ + { hostname: "ql-czuk", status: "recently_reachable", + ipv4:'10.5.0.3', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.4' + }, + { hostname: "ql-irene", status: "recently_reachable" }, + { hostname: "ql-ipem", status: "recently_reachable" }, + { hostname: "ql-czuck-bbone", status: "recently_reachable" }, + { hostname: "ql-graciela", status: "recently_reachable" }, + { hostname: "ql-marisa", status: "recently_reachable" }, + { hostname: "ql-anaymarcos", status: "recently_reachable" }, + { hostname: "ql-quinteros", status: "recently_reachable" }, + { hostname: "ql-guada", status: "recently_reachable" }, + { hostname: "ql-refu-bbone", status: "unreachable" }, + { hostname: "si-soniam", status: "unreachable" }, + { hostname: "si-giordano", status: "unreachable" }, + { hostname: "si-mario", status: "unreachable" }, + { hostname: "si-manu", status: "unreachable" }, +]; + +export const reachableNodesPage = () => ( + +); diff --git a/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js b/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js deleted file mode 100644 index 060496ce6..000000000 --- a/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.js +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index bc1e2fef9..000000000 --- a/plugins/lime-plugin-reachable-nodes/src/networkNodesApi.spec.js +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index ba59c9d47..000000000 --- a/plugins/lime-plugin-reachable-nodes/src/networkNodesMenu.js +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 55cd73d1a..000000000 --- a/plugins/lime-plugin-reachable-nodes/src/networkNodesPage.js +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index fb9b8b764..000000000 --- a/plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js +++ /dev/null @@ -1,16 +0,0 @@ -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/reachableNodesMenu.js b/plugins/lime-plugin-reachable-nodes/src/reachableNodesMenu.js new file mode 100644 index 000000000..55b9138be --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/reachableNodesMenu.js @@ -0,0 +1,7 @@ +import { h } from 'preact'; + +import I18n from 'i18n-js'; + +export const ReachableNodesMenu = () => ( + {I18n.t('Reachable Nodes')} +); \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js b/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js new file mode 100644 index 000000000..92172c6b7 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js @@ -0,0 +1,83 @@ +import { h } from "preact"; +import { useState } from "preact/hooks"; +import Tabs from "components/tabs"; +import Loading from "components/loading"; +import { List } from "components/list"; +import { ExpandableNode } from "plugins/lime-plugin-network-nodes/src/components/expandableNode"; +import { useNetworkNodes } from "plugins/lime-plugin-network-nodes/src/networkNodesQueries"; +import Help from "components/help"; +import I18n from 'i18n-js'; + +const PageHelp = () => ( +
+

+

{I18n.t("Reachable Nodes")}
+ {I18n.t("These are the nodes that can be reached from your node, " + + "i.e. there is a working path from your node to each of them.")} +
+ {I18n.t("This information is synced periodically " + + "and can be outdated by some minutes")} +

+

+

{I18n.t("Unreachable Nodes")}
+ {I18n.t("These are the nodes that can't be reached from your node, " + + "it is possible that they are not turned on or a link to reach them is down.")} +

+
+); + +const PageTabs = ({ nodes, ...props }) => { + const nReachable = nodes.filter(n => n.status === "recently_reachable").length; + const nUnreachable = nodes.filter(n => n.status === "unreachable").length; + const tabs = [ + { key: 'recently_reachable', repr: I18n.t('Reachable (%{howMany})', { howMany: nReachable }) }, + { key: 'unreachable', repr: I18n.t('Unreachable (%{howMany})', { howMany: nUnreachable }) }, + ]; + return +} + +export const ReachableNodesPage_ = ({ nodes }) => { + const [ selectedGroup, setselectedGroup ] = useState('recently_reachable'); + const [ unfoldedNode, setunfoldedNode ] = useState(null); + + function changeUnfolded(hostname) { + if (unfoldedNode == hostname) { + setunfoldedNode(null); + return; + } + setunfoldedNode(hostname); + } + return ( +
+
+ +
+ +
+
+ + {nodes + .filter(n => n.status === selectedGroup) + .sort((a, b) => a.hostname > b.hostname) + .map( + node => + changeUnfolded(node.hostname)}/> + )} + +
+ ) +} + +const ReachableNodesPage = () => { + const { data: nodes, isLoading } = useNetworkNodes(); + + if (isLoading) { + return
+ } + + return +} + +export default ReachableNodesPage diff --git a/plugins/lime-plugin-reachable-nodes/src/style.less b/plugins/lime-plugin-reachable-nodes/src/style.less deleted file mode 100644 index b3ea14459..000000000 --- a/plugins/lime-plugin-reachable-nodes/src/style.less +++ /dev/null @@ -1,15 +0,0 @@ -.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 diff --git a/src/components/help/style.less b/src/components/help/style.less index fa98012f0..1cb7fd193 100644 --- a/src/components/help/style.less +++ b/src/components/help/style.less @@ -13,7 +13,7 @@ background: #fff; border: 0.1em solid #F39100; border-radius: 1em; - padding: 2em; + padding: 1.5rem; cursor:auto; } diff --git a/src/config.js b/src/config.js index 8f371914e..8459f73bb 100644 --- a/src/config.js +++ b/src/config.js @@ -12,6 +12,7 @@ import RemoteSupport from '../plugins/lime-plugin-remotesupport'; import Pirania from '../plugins/lime-plugin-pirania'; import NetworkNodes from '../plugins/lime-plugin-network-nodes'; import DeleteNodes from '../plugins/lime-plugin-delete-nodes'; +import ReachableNodes from '../plugins/lime-plugin-reachable-nodes'; // REGISTER PLUGINS export const plugins = [ From 2e8e33694d9abcaaf914c4dc5bf795e2e6211459 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Thu, 22 Apr 2021 19:26:44 -0300 Subject: [PATCH 13/19] feat(upgraded nodes): add screen for upgraded not upgraded nodes --- plugins/lime-plugin-upgradedNodes/index.js | 10 ++ .../src/upgradedNodesMenu.js | 8 ++ .../src/upgradedNodesPage.js | 93 +++++++++++++++++++ .../upgradedNodes.spec.js | 55 +++++++++++ .../upgradedNodes.stories.js | 9 ++ src/config.js | 3 + 6 files changed, 178 insertions(+) create mode 100644 plugins/lime-plugin-upgradedNodes/index.js create mode 100644 plugins/lime-plugin-upgradedNodes/src/upgradedNodesMenu.js create mode 100644 plugins/lime-plugin-upgradedNodes/src/upgradedNodesPage.js create mode 100644 plugins/lime-plugin-upgradedNodes/upgradedNodes.spec.js create mode 100644 plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js diff --git a/plugins/lime-plugin-upgradedNodes/index.js b/plugins/lime-plugin-upgradedNodes/index.js new file mode 100644 index 000000000..70f42889b --- /dev/null +++ b/plugins/lime-plugin-upgradedNodes/index.js @@ -0,0 +1,10 @@ +import Page from './src/upgradedNodesPage'; +import Menu from './src/upgradedNodesMenu'; + +export default { + name: 'upgradedNodes', + page: Page, + menu: Menu, + menuView: 'community', + isCommunityProtected: false +}; diff --git a/plugins/lime-plugin-upgradedNodes/src/upgradedNodesMenu.js b/plugins/lime-plugin-upgradedNodes/src/upgradedNodesMenu.js new file mode 100644 index 000000000..e9a01c90f --- /dev/null +++ b/plugins/lime-plugin-upgradedNodes/src/upgradedNodesMenu.js @@ -0,0 +1,8 @@ +import { h } from 'preact'; +import I18n from 'i18n-js'; + +const Menu = () => ( + {I18n.t('Upgraded Nodes')} +); + +export default Menu; diff --git a/plugins/lime-plugin-upgradedNodes/src/upgradedNodesPage.js b/plugins/lime-plugin-upgradedNodes/src/upgradedNodesPage.js new file mode 100644 index 000000000..ed3e95dab --- /dev/null +++ b/plugins/lime-plugin-upgradedNodes/src/upgradedNodesPage.js @@ -0,0 +1,93 @@ +import { h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import Tabs from "components/tabs"; +import Loading from "components/loading"; +import { List } from "components/list"; +import { ExpandableNode } from "plugins/lime-plugin-network-nodes/src/components/expandableNode"; +import { useNetworkNodes } from "plugins/lime-plugin-network-nodes/src/networkNodesQueries"; +import { useNewVersion } from "plugins/lime-plugin-firmware/src/firmwareQueries"; +import Help from "components/help"; +import I18n from 'i18n-js'; + +const PageHelp = () => ( +
+

+

{I18n.t("Upgraded Nodes")}
+ {I18n.t("These are the nodes running the last version of the Firmware")} +

+

+

{I18n.t("Not Upgraded Nodes")}
+ {I18n.t("These are the nodes that need to be upgraded to the last version of the Firmware")} +

+
+); + +const PageTabs = ({ nodes, ...props }) => { + const nUpgraded = nodes.filter(n => n.group === "upgraded").length; + const nNotUpgraded = nodes.filter(n => n.group === "not_upgraded").length; + const tabs = [ + { key: 'upgraded', repr: I18n.t('Upgraded (%{howMany})', { howMany: nUpgraded }) }, + { key: 'not_upgraded', repr: I18n.t('Not Upgraded (%{howMany})', { howMany: nNotUpgraded }) }, + ]; + return +} + +export const UpgradedNodesPage_ = ({ nodes }) => { + const [ selectedGroup, setselectedGroup ] = useState('upgraded'); + const [ unfoldedNode, setunfoldedNode ] = useState(null); + + function changeUnfolded(hostname) { + if (unfoldedNode == hostname) { + setunfoldedNode(null); + return; + } + setunfoldedNode(hostname); + } + + return ( +
+
+ +
+ +
+
+ + {nodes + .filter(n => n.status !== 'gone') + .filter(n => n.group === selectedGroup) + .sort((a, b) => a.hostname > b.hostname) + .map( + node => + changeUnfolded(node.hostname)}/> + )} + +
+ ) +} + +const UpgradedNodesPage = () => { + const { data: nodes, isLoading: isLoadingNodes } = useNetworkNodes(); + const { data: newVersion, isLoading: isLoadingVersion} = useNewVersion(); + const [taggedNodes, setTaggedNodes] = useState(undefined); + + useEffect(() => { + if (nodes && newVersion){ + const taggedNodes = [...nodes].map( + n => ({ ...n, + group: n.fw_version === newVersion.version ? 'upgraded' : 'not_upgraded' + })); + setTaggedNodes(taggedNodes); + } + }, [nodes, newVersion]) + + if (isLoadingNodes || isLoadingVersion || taggedNodes === undefined) { + return
+ } + + return +} + +export default UpgradedNodesPage diff --git a/plugins/lime-plugin-upgradedNodes/upgradedNodes.spec.js b/plugins/lime-plugin-upgradedNodes/upgradedNodes.spec.js new file mode 100644 index 000000000..a4da44890 --- /dev/null +++ b/plugins/lime-plugin-upgradedNodes/upgradedNodes.spec.js @@ -0,0 +1,55 @@ +// Here you define tests that closely resemble how your component is used +// Using the testing-library: https://testing-library.com + +import { h } from 'preact'; +import { fireEvent, screen, cleanup, act } from '@testing-library/preact'; +import '@testing-library/jest-dom'; +import { render } from 'utils/test_utils'; +import queryCache from 'utils/queryCache'; + +import UpgradedNodesPage from './src/upgradedNodesPage'; +import { getNodes } from 'plugins/lime-plugin-network-nodes/src/networkNodesApi'; +import { getNewVersion } from 'plugins/lime-plugin-firmware/src/firmwareApi'; + +jest.mock('plugins/lime-plugin-network-nodes/src/networkNodesApi'); +jest.mock('plugins/lime-plugin-firmware/src/firmwareApi'); + + +describe('upgradedNodes', () => { + beforeEach(() => { + getNodes.mockImplementation(async () => [ + { hostname: 'node1', status: 'unreachable', fw_version: 'LibreRouterOS 1.4' }, + { hostname: 'node2', status: 'recently_reachable', fw_version: 'LibreRouterOS 1.4' }, + { hostname: 'node3', status: 'unreachable', fw_version: 'LibreRouterOS 1.3' }, + { hostname: 'node4', status: 'recently_reachable', fw_version: 'LibreRouterOS 1.3' }, + ]); + getNewVersion.mockImplementation(async () => ({ + version: 'LibreRouterOS 1.4' + })) + }); + + afterEach(() => { + cleanup(); + act(() => queryCache.clear()); + }); + + it('shows one tab for upgraded nodes and one for not upgradaded nodes with length', async () => { + render(); + expect(await screen.findByRole('tab', { name: /^Upgraded \(2\)/i })).toBeVisible(); + expect(await screen.findByRole('tab', { name: /^Not Upgraded \(2\)/i })).toBeVisible(); + }) + + it('shows one row with the hostname for each upgraded node', async () => { + render(); + expect(await screen.findByText('node1')).toBeVisible(); + expect(await screen.findByText('node2')).toBeVisible(); + }) + + it('shows one row with the hostname for each not upgraded node', async () => { + render(); + const tabNotUpgraded = await screen.findByRole('tab', { name: /^Not Upgraded \(2\)/i }); + fireEvent.click(tabNotUpgraded); + expect(await screen.findByText('node3')).toBeVisible(); + expect(await screen.findByText('node4')).toBeVisible(); + }) +}); diff --git a/plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js b/plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js new file mode 100644 index 000000000..782012759 --- /dev/null +++ b/plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js @@ -0,0 +1,9 @@ +//Here you define the StoryBook stories for this plugin + +import UpgradedNodes from './src/upgradedNodesPage'; + +export default { + title: 'Containers/upgradedNodes' +} + +export const myStory = () => diff --git a/src/config.js b/src/config.js index 8459f73bb..f9ab9b778 100644 --- a/src/config.js +++ b/src/config.js @@ -13,6 +13,7 @@ import Pirania from '../plugins/lime-plugin-pirania'; import NetworkNodes from '../plugins/lime-plugin-network-nodes'; import DeleteNodes from '../plugins/lime-plugin-delete-nodes'; import ReachableNodes from '../plugins/lime-plugin-reachable-nodes'; +import UpgradedNodes from '../plugins/lime-plugin-upgraded-nodes'; // REGISTER PLUGINS export const plugins = [ @@ -28,6 +29,8 @@ export const plugins = [ RemoteSupport, Pirania, NetworkNodes, + ReachableNodes, + UpgradedNodes, DeleteNodes, Fbw // fbw does not have menu item ]; From 7498ca24a62b2412c7e9448525d4f7d392306aad Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Mon, 3 May 2021 10:12:42 -0300 Subject: [PATCH 14/19] improvement(upgraded-nodes): show subheaders for each version --- i18n/generic.json | 6 ++ .../index.js | 0 .../lime-plugin-upgraded-nodes/src/style.less | 8 +++ .../src/upgradedNodesMenu.js | 0 .../src/upgradedNodesPage.js | 65 ++++++++++++++----- .../upgradedNodes.spec.js | 3 - .../upgradedNodes.stories.js | 55 ++++++++++++++++ .../upgradedNodes.stories.js | 9 --- 8 files changed, 116 insertions(+), 30 deletions(-) rename plugins/{lime-plugin-upgradedNodes => lime-plugin-upgraded-nodes}/index.js (100%) create mode 100644 plugins/lime-plugin-upgraded-nodes/src/style.less rename plugins/{lime-plugin-upgradedNodes => lime-plugin-upgraded-nodes}/src/upgradedNodesMenu.js (100%) rename plugins/{lime-plugin-upgradedNodes => lime-plugin-upgraded-nodes}/src/upgradedNodesPage.js (57%) rename plugins/{lime-plugin-upgradedNodes => lime-plugin-upgraded-nodes}/upgradedNodes.spec.js (94%) create mode 100644 plugins/lime-plugin-upgraded-nodes/upgradedNodes.stories.js delete mode 100644 plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js diff --git a/i18n/generic.json b/i18n/generic.json index 8497de204..c194666e9 100644 --- a/i18n/generic.json +++ b/i18n/generic.json @@ -104,6 +104,8 @@ "network_nodes_4368eb67": "Network Nodes", "no_network_found_try_realigning_your_node_and_resc_176a9b3e": "No network found, try realigning your node and rescanning.", "node_configuration_7342e6f5": "Node Configuration", + "not_upgraded_howmany_5ed230c3": "Not Upgraded (%{howMany})", + "not_upgraded_nodes_9e67db38": "Not Upgraded Nodes", "notes_c42e0fd5": "Notes", "notes_of_a44a4158": "Notes of", "ok_ff1b646a": "Ok", @@ -170,8 +172,10 @@ "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_running_the_last_version_of_th_5165bdfe": "These are the nodes running the last version of the Firmware", "these_are_the_nodes_that_can_be_reached_from_your__4c524abe": "These are the nodes that can be reached from your node, i.e. there is a working path from your node to each of them.", "these_are_the_nodes_that_can_t_be_reached_from_you_dbbf9032": "These are the nodes that can't be reached from your node, it is possible that they are not turned on or a link to reach them is down.", + "these_are_the_nodes_that_need_to_be_upgraded_to_th_d09d104": "These are the nodes that need to be upgraded to the last version of the Firmware", "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_information_is_synced_periodically_and_can_be_8b74cb8c": "This information is synced periodically and can be outdated by some minutes", @@ -188,6 +192,8 @@ "upgrade_now_f300d697": "Upgrade Now", "upgrade_to_lastest_firmware_version_9b159910": "Upgrade to lastest firmware version", "upgrade_to_versionname_621a0b6a": "Upgrade to %{versionName}", + "upgraded_howmany_e439d4b1": "Upgraded (%{howMany})", + "upgraded_nodes_dfc85207": "Upgraded Nodes", "upload_firmware_image_from_your_device_57327bee": "Upload firmware image from your device", "uptime_c1d2415d": "Uptime", "versionname_is_now_available_a6fbbb63": "%{versionName} is now available", diff --git a/plugins/lime-plugin-upgradedNodes/index.js b/plugins/lime-plugin-upgraded-nodes/index.js similarity index 100% rename from plugins/lime-plugin-upgradedNodes/index.js rename to plugins/lime-plugin-upgraded-nodes/index.js diff --git a/plugins/lime-plugin-upgraded-nodes/src/style.less b/plugins/lime-plugin-upgraded-nodes/src/style.less new file mode 100644 index 000000000..0b786cbee --- /dev/null +++ b/plugins/lime-plugin-upgraded-nodes/src/style.less @@ -0,0 +1,8 @@ +.stickySubheader { + position: sticky; + top: 0; + background-color: #325850; + padding: 0.2rem; + text-align: center; + color:white; +} diff --git a/plugins/lime-plugin-upgradedNodes/src/upgradedNodesMenu.js b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js similarity index 100% rename from plugins/lime-plugin-upgradedNodes/src/upgradedNodesMenu.js rename to plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js diff --git a/plugins/lime-plugin-upgradedNodes/src/upgradedNodesPage.js b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js similarity index 57% rename from plugins/lime-plugin-upgradedNodes/src/upgradedNodesPage.js rename to plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js index ed3e95dab..0e1ba5a98 100644 --- a/plugins/lime-plugin-upgradedNodes/src/upgradedNodesPage.js +++ b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js @@ -1,4 +1,5 @@ -import { h } from "preact"; +import { h, Fragment } from "preact"; +import style from "./style.less"; import { useEffect, useState } from "preact/hooks"; import Tabs from "components/tabs"; import Loading from "components/loading"; @@ -9,6 +10,19 @@ import { useNewVersion } from "plugins/lime-plugin-firmware/src/firmwareQueries" import Help from "components/help"; import I18n from 'i18n-js'; +function groupBy(xs, key) { + return xs.reduce(function (rv, x) { + let v = key instanceof Function ? key(x) : x[key]; + let el = rv.find((r) => r && r.key === v); + if (el) { + el.values.push(x); + } else { + rv.push({ key: v, values: [x] }); + } + return rv; + }, []); +}; + const PageHelp = () => (

@@ -33,8 +47,8 @@ const PageTabs = ({ nodes, ...props }) => { } export const UpgradedNodesPage_ = ({ nodes }) => { - const [ selectedGroup, setselectedGroup ] = useState('upgraded'); - const [ unfoldedNode, setunfoldedNode ] = useState(null); + const [selectedGroup, setselectedGroup] = useState('upgraded'); + const [unfoldedNode, setunfoldedNode] = useState(null); function changeUnfolded(hostname) { if (unfoldedNode == hostname) { @@ -44,6 +58,13 @@ export const UpgradedNodesPage_ = ({ nodes }) => { setunfoldedNode(hostname); } + const groupNodes = nodes + .filter(n => n.status !== 'gone') + .filter(n => n.group === selectedGroup); + + const grouppedByVersion = groupBy(groupNodes, 'fw_version') + .sort((a, b) => a.key > b.key); + return (

@@ -53,16 +74,23 @@ export const UpgradedNodesPage_ = ({ nodes }) => {
- {nodes - .filter(n => n.status !== 'gone') - .filter(n => n.group === selectedGroup) - .sort((a, b) => a.hostname > b.hostname) - .map( - node => - changeUnfolded(node.hostname)}/> - )} + {grouppedByVersion + .map((versionGroup, index) => ( + + {selectedGroup === 'not_upgraded' && +
+ {versionGroup.key} +
+ } + {versionGroup.values + .map(node => + changeUnfolded(node.hostname)} /> + )} +
+ )) + }
) @@ -70,15 +98,16 @@ export const UpgradedNodesPage_ = ({ nodes }) => { const UpgradedNodesPage = () => { const { data: nodes, isLoading: isLoadingNodes } = useNetworkNodes(); - const { data: newVersion, isLoading: isLoadingVersion} = useNewVersion(); + const { data: newVersion, isLoading: isLoadingVersion } = useNewVersion(); const [taggedNodes, setTaggedNodes] = useState(undefined); useEffect(() => { - if (nodes && newVersion){ + if (nodes && newVersion) { const taggedNodes = [...nodes].map( - n => ({ ...n, - group: n.fw_version === newVersion.version ? 'upgraded' : 'not_upgraded' - })); + n => ({ + ...n, + group: n.fw_version === newVersion.version ? 'upgraded' : 'not_upgraded' + })); setTaggedNodes(taggedNodes); } }, [nodes, newVersion]) diff --git a/plugins/lime-plugin-upgradedNodes/upgradedNodes.spec.js b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js similarity index 94% rename from plugins/lime-plugin-upgradedNodes/upgradedNodes.spec.js rename to plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js index a4da44890..16d4c800d 100644 --- a/plugins/lime-plugin-upgradedNodes/upgradedNodes.spec.js +++ b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js @@ -1,6 +1,3 @@ -// Here you define tests that closely resemble how your component is used -// Using the testing-library: https://testing-library.com - import { h } from 'preact'; import { fireEvent, screen, cleanup, act } from '@testing-library/preact'; import '@testing-library/jest-dom'; diff --git a/plugins/lime-plugin-upgraded-nodes/upgradedNodes.stories.js b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.stories.js new file mode 100644 index 000000000..3f3dbe585 --- /dev/null +++ b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.stories.js @@ -0,0 +1,55 @@ +import { UpgradedNodesPage_ } from './src/upgradedNodesPage'; + +export default { + title: 'Containers/Upgraded Nodes' +} + +const nodes = [ + { hostname: "ql-czuk", status: "recently_reachable", + ipv4:'10.5.0.3', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.3', + group: 'not_upgraded' + }, + { hostname: "ql-czuk-bbone", status: "recently_reachable", + ipv4:'10.5.0.9', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.3', + group: 'not_upgraded' + }, + { hostname: "si-soniam", status: "recently_reachable", + ipv4:'10.5.0.16', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.4', + group: 'upgraded' + }, + { hostname: "ql-berta", status: "recently_reachable", + ipv4:'10.5.0.9', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.4', + group: 'upgraded' + }, + { hostname: "ql-nelson", status: "recently_reachable", + ipv4:'10.5.0.9', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.3', + group: 'not_upgraded' + }, + { hostname: "ql-irene", status: "recently_reachable", + ipv4:'10.5.0.9', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.3', + group: 'not_upgraded' + }, + { hostname: "ql-guillermina", status: "recently_reachable", + ipv4:'10.5.0.9', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.3', + group: 'not_upgraded' + }, + { hostname: "ql-silviak", status: "recently_reachable", + ipv4:'10.5.0.9', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.3', + group: 'not_upgraded' + }, + { hostname: "ql-oncelotes", status: "recently_reachable", + ipv4:'10.5.0.9', ipv6: 'fd0d:fe46:8ce8::8bbf:7500', + board: 'LibreRouter v1', fw_version: 'LibreRouterOS 1.2', + group: 'not_upgraded' + }, +]; + +export const upgradedNodesPage = () => diff --git a/plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js b/plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js deleted file mode 100644 index 782012759..000000000 --- a/plugins/lime-plugin-upgradedNodes/upgradedNodes.stories.js +++ /dev/null @@ -1,9 +0,0 @@ -//Here you define the StoryBook stories for this plugin - -import UpgradedNodes from './src/upgradedNodesPage'; - -export default { - title: 'Containers/upgradedNodes' -} - -export const myStory = () => From 3ed6c7f659f1bcfac0edccb4a5c35370e7e23d62 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Fri, 17 Dec 2021 11:27:42 -0300 Subject: [PATCH 15/19] refactor(networknodes): adapt to promise based api --- .../lime-plugin-network-nodes/src/networkNodesApi.js | 6 +++--- .../src/networkNodesApi.spec.js | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesApi.js b/plugins/lime-plugin-network-nodes/src/networkNodesApi.js index 38feb6ce6..fc588626d 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesApi.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.js @@ -1,11 +1,11 @@ import api from 'utils/uhttpd.service'; export const getNodes = () => - api.call('network-nodes', 'get_nodes', {}).toPromise() + api.call('network-nodes', 'get_nodes', {}) .then(res => { return 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 + api.call('network-nodes', 'mark_nodes_as_gone', { hostnames: hostnames }) + .then(() => hostnames); diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js b/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js index c5ee29040..c9c83a9d3 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js @@ -1,10 +1,9 @@ import { getNodes, markNodesAsGone } from './networkNodesApi' import api from 'utils/uhttpd.service'; -import { of } from 'rxjs'; jest.mock('utils/uhttpd.service') beforeEach(() => { - api.call.mockImplementation(() => of({ status: 'ok' })) + api.call.mockImplementation(async () => ({ status: 'ok' })) }) describe('getNodes', () => { @@ -28,21 +27,21 @@ describe('getNodes', () => { fw_version: 'LibreRouterOS 1.4' } }; - api.call.mockImplementation(() => of({ status: 'ok', nodes })); + api.call.mockImplementation(async () => ({ status: 'ok', nodes })); expect(await getNodes()).toEqual(nodes); }); }); describe('markNodesAsGone', () => { it('calls the expected endpoint', async () => { - api.call.mockImplementation(() => of({ status: 'ok' })) + api.call.mockImplementation(async () => ({ 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'})) + api.call.mockImplementation(async () => ({status: 'ok'})); const result = await markNodesAsGone(['node1', 'node2']) expect(result).toEqual(['node1', 'node2']) }) -}); \ No newline at end of file +}); From f29f46d42346fb70be5355e0d1ee5107f769149e Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Fri, 17 Dec 2021 11:32:57 -0300 Subject: [PATCH 16/19] improvement(networknodes): show ipv6 as link aswell --- plugins/lime-plugin-network-nodes/networkNodes.spec.js | 2 +- .../src/components/expandableNode/index.js | 4 ++-- plugins/lime-plugin-network-nodes/src/networkNodesPage.js | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/lime-plugin-network-nodes/networkNodes.spec.js b/plugins/lime-plugin-network-nodes/networkNodes.spec.js index 8cfbf48c7..009ce5c6b 100644 --- a/plugins/lime-plugin-network-nodes/networkNodes.spec.js +++ b/plugins/lime-plugin-network-nodes/networkNodes.spec.js @@ -55,7 +55,7 @@ describe('networkNodes', () => { const element = await screen.findByText('ql-nelson'); fireEvent.click(element); expect(await screen.findByRole('link', { name: '10.5.0.17'})).toBeInTheDocument(); - expect(await screen.findByText('IPv6: fd0d:fe46:8ce8::8bbf:75bf')).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'fd0d:fe46:8ce8::8bbf:75bf'})).toBeInTheDocument(); expect(await screen.findByText('Device: LibreRouter v1')).toBeInTheDocument(); expect(await screen.findByText('Firmware: LibreRouterOS 1.4')).toBeInTheDocument(); }); diff --git a/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js index 8624eb5dd..e3ebeaf4a 100644 --- a/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js +++ b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js @@ -14,7 +14,7 @@ export const ExpandableNode = ({ node, showMore, onClick }) => { {showMore &&
e.stopPropagation()}> {ipv4 &&
IPv4: {ipv4}
} - {ipv6 &&
IPv6: {ipv6}
} + {ipv6 &&
IPv6: {ipv6}
} {board &&
{I18n.t('Device')}: {board}
} {fw_version &&
{I18n.t('Firmware')}: {fw_version}
}
@@ -22,4 +22,4 @@ export const ExpandableNode = ({ node, showMore, onClick }) => {
) -} \ No newline at end of file +} diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesPage.js b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js index 5a72fcdf6..539c126f5 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesPage.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js @@ -1,4 +1,3 @@ -// NetworkNodes will be rendered when navigating to this plugin import { h } from 'preact'; import { useNetworkNodes } from './networkNodesQueries'; import { List } from 'components/list'; From 2e7bf51ed861b1b0614196e8b24d0edb30c74a3c Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Fri, 17 Dec 2021 12:01:33 -0300 Subject: [PATCH 17/19] refactor(networknodes): fix api format --- .../deleteNodes.spec.js | 22 +++++++++---------- .../src/deleteNodesPage.js | 2 +- .../src/networkNodesQueries.js | 9 ++++---- .../reachableNodes.spec.js | 22 +++++++++---------- .../src/reachableNodesPage.js | 7 +++--- .../src/upgradedNodesPage.js | 2 +- .../upgradedNodes.spec.js | 12 +++++----- 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js b/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js index 9dfe24fcf..8ec7143dc 100644 --- a/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js +++ b/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js @@ -12,17 +12,17 @@ jest.mock('plugins/lime-plugin-network-nodes/src/networkNodesApi'); describe('delete nodes page', () => { beforeEach(() => { - getNodes.mockImplementation(async () => [ - { hostname: 'node1', status: 'recently_reachable' }, - { hostname: 'node2', status: 'recently_reachable' }, - { hostname: 'node3', status: 'recently_reachable' }, - { hostname: 'node4', status: 'unreachable' }, - { hostname: 'node5', status: 'unreachable' }, - { hostname: 'node6', status: 'unreachable' }, - { hostname: 'node7', status: 'unreachable' }, - { hostname: 'node8', status: 'gone' }, - { hostname: 'node9', status: 'gone' }, - ]); + getNodes.mockImplementation(async () => ({ + 'node1': { hostname: 'node1', status: 'recently_reachable' }, + 'node2': { hostname: 'node2', status: 'recently_reachable' }, + 'node3': { hostname: 'node3', status: 'recently_reachable' }, + 'node4': { hostname: 'node4', status: 'unreachable' }, + 'node5': { hostname: 'node5', status: 'unreachable' }, + 'node6': { hostname: 'node6', status: 'unreachable' }, + 'node7': { hostname: 'node7', status: 'unreachable' }, + 'node8': { hostname: 'node8', status: 'gone' }, + 'node9': { hostname: 'node9', status: 'gone' }, + })); markNodesAsGone.mockImplementation(async () => []); }); diff --git a/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js index f0417d7dd..418a762ff 100644 --- a/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js @@ -11,7 +11,7 @@ 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 unreachableNodes = nodes.filter(n => n.status === "unreachable"); + const unreachableNodes = Object.values(nodes).filter(n => n.status === "unreachable"); useEffect(() => { if (isSuccess) { diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js b/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js index 0e82bc51b..ad8a93569 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js @@ -8,10 +8,11 @@ export const useNetworkNodes = () => 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 - ) + const result = old; + hostnames.forEach(hostname => { + result[hostname] = {...old[hostname], status: "gone"} + }); return result; } ) -}) \ No newline at end of file +}) diff --git a/plugins/lime-plugin-reachable-nodes/reachableNodes.spec.js b/plugins/lime-plugin-reachable-nodes/reachableNodes.spec.js index be0d33712..e490f6446 100644 --- a/plugins/lime-plugin-reachable-nodes/reachableNodes.spec.js +++ b/plugins/lime-plugin-reachable-nodes/reachableNodes.spec.js @@ -10,17 +10,17 @@ import { render } from 'utils/test_utils'; jest.mock('plugins/lime-plugin-network-nodes/src/networkNodesApi'); beforeEach(() => { - getNodes.mockImplementation(async () => [ - { hostname: 'node1', status: 'recently_reachable' }, - { hostname: 'node2', status: 'recently_reachable' }, - { hostname: 'node3', status: 'recently_reachable' }, - { hostname: 'node4', status: 'unreachable' }, - { hostname: 'node5', status: 'unreachable' }, - { hostname: 'node6', status: 'unreachable' }, - { hostname: 'node7', status: 'unreachable' }, - { hostname: 'node8', status: 'gone' }, - { hostname: 'node9', status: 'gone' }, - ]); + getNodes.mockImplementation(async () => ({ + 'node1': { hostname: 'node1', status: 'recently_reachable' }, + 'node2': { hostname: 'node2', status: 'recently_reachable' }, + 'node3': { hostname: 'node3', status: 'recently_reachable' }, + 'node4': { hostname: 'node4', status: 'unreachable' }, + 'node5': { hostname: 'node5', status: 'unreachable' }, + 'node6': { hostname: 'node6', status: 'unreachable' }, + 'node7': { hostname: 'node7', status: 'unreachable' }, + 'node8': { hostname: 'node8', status: 'gone' }, + 'node9': { hostname: 'node9', status: 'gone' }, + })); markNodesAsGone.mockImplementation(async () => []); }); diff --git a/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js b/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js index 92172c6b7..bc02bd4fe 100644 --- a/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js +++ b/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js @@ -27,8 +27,8 @@ const PageHelp = () => ( ); const PageTabs = ({ nodes, ...props }) => { - const nReachable = nodes.filter(n => n.status === "recently_reachable").length; - const nUnreachable = nodes.filter(n => n.status === "unreachable").length; + const nReachable = Object.values(nodes).filter(n => n.status === "recently_reachable").length; + const nUnreachable = Object.values(nodes).filter(n => n.status === "unreachable").length; const tabs = [ { key: 'recently_reachable', repr: I18n.t('Reachable (%{howMany})', { howMany: nReachable }) }, { key: 'unreachable', repr: I18n.t('Unreachable (%{howMany})', { howMany: nUnreachable }) }, @@ -39,7 +39,6 @@ const PageTabs = ({ nodes, ...props }) => { export const ReachableNodesPage_ = ({ nodes }) => { const [ selectedGroup, setselectedGroup ] = useState('recently_reachable'); const [ unfoldedNode, setunfoldedNode ] = useState(null); - function changeUnfolded(hostname) { if (unfoldedNode == hostname) { setunfoldedNode(null); @@ -56,7 +55,7 @@ export const ReachableNodesPage_ = ({ nodes }) => {
- {nodes + {Object.values(nodes) .filter(n => n.status === selectedGroup) .sort((a, b) => a.hostname > b.hostname) .map( diff --git a/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js index 0e1ba5a98..99b5c2031 100644 --- a/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js +++ b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js @@ -103,7 +103,7 @@ const UpgradedNodesPage = () => { useEffect(() => { if (nodes && newVersion) { - const taggedNodes = [...nodes].map( + const taggedNodes = [...Object.values(nodes)].map( n => ({ ...n, group: n.fw_version === newVersion.version ? 'upgraded' : 'not_upgraded' diff --git a/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js index 16d4c800d..aa3d68bef 100644 --- a/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js +++ b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js @@ -14,12 +14,12 @@ jest.mock('plugins/lime-plugin-firmware/src/firmwareApi'); describe('upgradedNodes', () => { beforeEach(() => { - getNodes.mockImplementation(async () => [ - { hostname: 'node1', status: 'unreachable', fw_version: 'LibreRouterOS 1.4' }, - { hostname: 'node2', status: 'recently_reachable', fw_version: 'LibreRouterOS 1.4' }, - { hostname: 'node3', status: 'unreachable', fw_version: 'LibreRouterOS 1.3' }, - { hostname: 'node4', status: 'recently_reachable', fw_version: 'LibreRouterOS 1.3' }, - ]); + getNodes.mockImplementation(async () => ({ + 'node1': { hostname: 'node1', status: 'unreachable', fw_version: 'LibreRouterOS 1.4' }, + 'node2': { hostname: 'node2', status: 'recently_reachable', fw_version: 'LibreRouterOS 1.4' }, + 'node3': { hostname: 'node3', status: 'unreachable', fw_version: 'LibreRouterOS 1.3' }, + 'node4': { hostname: 'node4', status: 'recently_reachable', fw_version: 'LibreRouterOS 1.3' }, + })); getNewVersion.mockImplementation(async () => ({ version: 'LibreRouterOS 1.4' })) From c5b514659ba7783566cdede51dec619c08164db6 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Fri, 17 Dec 2021 12:01:53 -0300 Subject: [PATCH 18/19] refactor(networknodes): fix plugin name --- plugins/lime-plugin-reachable-nodes/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lime-plugin-reachable-nodes/index.js b/plugins/lime-plugin-reachable-nodes/index.js index f378f77b3..6d6014503 100644 --- a/plugins/lime-plugin-reachable-nodes/index.js +++ b/plugins/lime-plugin-reachable-nodes/index.js @@ -2,7 +2,7 @@ import Page from './src/reachableNodesPage'; import { ReachableNodesMenu } from './src/reachableNodesMenu'; export default { - name: 'NetworkNodes', + name: 'ReachableNodes', page: Page, menu: ReachableNodesMenu, menuView: 'community', From fd94bad67bef5f98d21afb89760a17bf5c37c193 Mon Sep 17 00:00:00 2001 From: German Ferrero Date: Fri, 17 Dec 2021 13:43:07 -0300 Subject: [PATCH 19/19] refactor(networknodes): adapt to lingui i18n --- package.json | 1 - .../src/deleteNodesMenu.js | 4 +-- .../src/deleteNodesPage.js | 25 ++++++++++------- .../src/components/expandableNode/index.js | 6 ++-- .../src/networkNodesMenu.js | 4 +-- .../src/networkNodesPage.js | 4 +-- .../src/reachableNodesMenu.js | 6 ++-- .../src/reachableNodesPage.js | 28 +++++++++++-------- .../src/upgradedNodesMenu.js | 4 +-- .../src/upgradedNodesPage.js | 14 +++++----- 10 files changed, 53 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index d7e9ed585..c455134bb 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,6 @@ "leaflet.gridlayer.googlemutant": "0.13.4", "preact": "10.5.7", "preact-cli": "3.0.0", - "preact-i18nline": "2.0.0", "preact-router": "3.2.1", "react-hook-form": "6.9.2", "react-query": "2.23.1", diff --git a/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js b/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js index 091be2c84..5f7be2277 100644 --- a/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js @@ -1,8 +1,8 @@ import { h } from 'preact'; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; const Menu = () => ( - {I18n.t('Delete Nodes')} + Delete Nodes ); export default Menu; diff --git a/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js index 418a762ff..b33a0311b 100644 --- a/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js @@ -6,7 +6,7 @@ import { useEffect, useState } from 'preact/hooks'; import { useSet } from 'react-use'; import { useMarkNodesAsGone, useNetworkNodes } from 'plugins/lime-plugin-network-nodes/src/networkNodesQueries' import style from './deleteNodesStyle.less'; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) => { const [selectedNodes, { toggle, has, reset }] = useSet(new Set([])); @@ -26,13 +26,17 @@ export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) = return (
-

{I18n.t("Delete Nodes")}

+

Delete Nodes

{unreachableNodes.length > 0 && -

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

+

+ + Select the nodes which no longer belong to the network and + delete them from the list of unreachable nodes + +

} {unreachableNodes.length === 0 && -

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

+

There are no left unreachable nodes

} {unreachableNodes.map(node => @@ -48,15 +52,16 @@ export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) =
- {[selectedNodes.size, - I18n.t('selected-nodes', { count: selectedNodes.size }) - ].join(' ')} + {!isSubmitting && } {isSubmitting && @@ -66,7 +71,7 @@ export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) = }
{showSuccess && - + Successfully deleted} /> }
) diff --git a/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js index e3ebeaf4a..05186ad9d 100644 --- a/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js +++ b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js @@ -1,5 +1,5 @@ import { h } from 'preact'; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; import { ListItem } from 'components/list'; import style from './style.less'; @@ -15,8 +15,8 @@ export const ExpandableNode = ({ node, showMore, onClick }) => {
e.stopPropagation()}> {ipv4 &&
IPv4: {ipv4}
} {ipv6 &&
IPv6: {ipv6}
} - {board &&
{I18n.t('Device')}: {board}
} - {fw_version &&
{I18n.t('Firmware')}: {fw_version}
} + {board &&
Device: {board}
} + {fw_version &&
Firmware: {fw_version}
}
}
diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js b/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js index 0303aa3c6..9f2019d78 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js @@ -1,8 +1,8 @@ import { h } from 'preact'; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; const Menu = () => ( - {I18n.t('Network Nodes')} + Network Nodes ); export default Menu; diff --git a/plugins/lime-plugin-network-nodes/src/networkNodesPage.js b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js index 539c126f5..f256d8b09 100644 --- a/plugins/lime-plugin-network-nodes/src/networkNodesPage.js +++ b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js @@ -5,7 +5,7 @@ import { Loading } from 'components/loading'; import { ExpandableNode } from './components/expandableNode'; import style from './networkNodesStyle.less'; import { useState } from 'preact/hooks'; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; export const _NetworkNodes = ({ nodes, isLoading, unfoldedNode, onUnfold }) => { if (isLoading) { @@ -13,7 +13,7 @@ export const _NetworkNodes = ({ nodes, isLoading, unfoldedNode, onUnfold }) => { } return (
-
{I18n.t("Network Nodes")}
+
Network Nodes
{nodes.map((node) => ( - {I18n.t('Reachable Nodes')} -); \ No newline at end of file + Reachable Nodes +); diff --git a/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js b/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js index bc02bd4fe..798a80b96 100644 --- a/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js +++ b/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js @@ -6,22 +6,28 @@ import { List } from "components/list"; import { ExpandableNode } from "plugins/lime-plugin-network-nodes/src/components/expandableNode"; import { useNetworkNodes } from "plugins/lime-plugin-network-nodes/src/networkNodesQueries"; import Help from "components/help"; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; const PageHelp = () => (

-

{I18n.t("Reachable Nodes")}
- {I18n.t("These are the nodes that can be reached from your node, " + - "i.e. there is a working path from your node to each of them.")} +
Reachable Nodes
+ + These are the nodes that can be reached from your node, + i.e. there is a working path from your node to each of them. +
- {I18n.t("This information is synced periodically " + - "and can be outdated by some minutes")} + + This information is synced periodically + and can be outdated by some minutes. +

-

{I18n.t("Unreachable Nodes")}
- {I18n.t("These are the nodes that can't be reached from your node, " + - "it is possible that they are not turned on or a link to reach them is down.")} +
Unreachable Nodes
+ + These are the nodes that can't be reached from your node, + it is possible that they are not turned on or a link to reach them is down. +

); @@ -30,8 +36,8 @@ const PageTabs = ({ nodes, ...props }) => { const nReachable = Object.values(nodes).filter(n => n.status === "recently_reachable").length; const nUnreachable = Object.values(nodes).filter(n => n.status === "unreachable").length; const tabs = [ - { key: 'recently_reachable', repr: I18n.t('Reachable (%{howMany})', { howMany: nReachable }) }, - { key: 'unreachable', repr: I18n.t('Unreachable (%{howMany})', { howMany: nUnreachable }) }, + { key: 'recently_reachable', repr: Reachable ({nReachable}) }, + { key: 'unreachable', repr: Unreachable ({nUnreachable}) }, ]; return } diff --git a/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js index e9a01c90f..38dcd2f4a 100644 --- a/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js +++ b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js @@ -1,8 +1,8 @@ import { h } from 'preact'; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; const Menu = () => ( - {I18n.t('Upgraded Nodes')} + Upgraded Nodes ); export default Menu; diff --git a/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js index 99b5c2031..b14e582e6 100644 --- a/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js +++ b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js @@ -8,7 +8,7 @@ import { ExpandableNode } from "plugins/lime-plugin-network-nodes/src/components import { useNetworkNodes } from "plugins/lime-plugin-network-nodes/src/networkNodesQueries"; import { useNewVersion } from "plugins/lime-plugin-firmware/src/firmwareQueries"; import Help from "components/help"; -import I18n from 'i18n-js'; +import { Trans } from '@lingui/macro'; function groupBy(xs, key) { return xs.reduce(function (rv, x) { @@ -26,12 +26,12 @@ function groupBy(xs, key) { const PageHelp = () => (

-

{I18n.t("Upgraded Nodes")}
- {I18n.t("These are the nodes running the last version of the Firmware")} +
Upgraded Nodes
+ These are the nodes running the last version of the Firmware

-

{I18n.t("Not Upgraded Nodes")}
- {I18n.t("These are the nodes that need to be upgraded to the last version of the Firmware")} +
Not Upgraded Nodes
+ These are the nodes that need to be upgraded to the last version of the Firmware

); @@ -40,8 +40,8 @@ const PageTabs = ({ nodes, ...props }) => { const nUpgraded = nodes.filter(n => n.group === "upgraded").length; const nNotUpgraded = nodes.filter(n => n.group === "not_upgraded").length; const tabs = [ - { key: 'upgraded', repr: I18n.t('Upgraded (%{howMany})', { howMany: nUpgraded }) }, - { key: 'not_upgraded', repr: I18n.t('Not Upgraded (%{howMany})', { howMany: nNotUpgraded }) }, + { key: 'upgraded', repr: Upgraded ({nUpgraded})}, + { key: 'not_upgraded', repr: Not Upgraded ({nNotUpgraded})}, ]; return }