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 00000000..602ea57b --- /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 00000000..261b81f5 --- /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 00000000..2bc67362 --- /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 00000000..2abf1859 --- /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 00000000..5dc98d63 --- /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 00000000..a2d02a0b --- /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 00000000..69fef588 --- /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 00000000..0303aa3c --- /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 00000000..9d20e0d4 --- /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 00000000..45f7c376 --- /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 00000000..720909a9 --- /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 00000000..4342d5e2 --- /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 99197c6a..675c2adc 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 00000000..7fddcae1 --- /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 4342d5e2..81dacaf6 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 00000000..53a06c08 --- /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 261b81f5..8cfbf48c 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 9d20e0d4..5a72fcdf 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 2bc67362..8624eb5d 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 7fddcae1..83695d01 100644 --- a/i18n/generic.json +++ b/i18n/generic.json @@ -28,6 +28,8 @@ "confirm_6556b3a6": "Confirm", "confirm_location_2fe5ae11": "confirm location", "congratulations_ffe43bf9": "Congratulations", + "connected_howmany_228826ea": "Connected (%{howMany})", + "connected_nodes_4b7e6f49": "Connected Nodes", "count_days_de0c6a32": { "one": "1 days", "other": "%{count} days" @@ -48,11 +50,19 @@ "one": "1 seconds", "other": "%{count} seconds" }, + "count_selected_nodes_19bbd632": { + "one": "1 selected-nodes", + "other": "%{count} selected-nodes" + }, "create_network_d229d642": "Create network", "create_new_network_28805f92": "Create new network", "create_session_ad54bdb6": "Create Session", "currently_your_node_has_version_1c26984b": "Currently your node has version:", + "delete_a6efa79d": "Delete", + "delete_nodes_f63ec0d5": "Delete Nodes", "device_95d26d94": "Device", + "disconnected_howmany_10fc7bd5": "Disconnected (%{howMany})", + "disconnected_nodes_88f80d1e": "Disconnected Nodes", "don_t_show_this_message_again_9950c20": "Don't show this message again", "download_c7ffdfb9": "Download", "downloading_1e41f805": "Downloading", @@ -65,6 +75,7 @@ "full_path_metrics_2859608f": "Full path metrics", "go_64ecd1fd": "Go!", "go_to_community_view_d12b8d67": "Go to Community View", + "go_to_delete_nodes_1203128b": "Go to Delete Nodes", "go_to_node_view_26ba929d": "Go to Node View", "ground_routing_12ab04c9": "Ground Routing", "ground_routing_configuration_3f4fa9c1": "Ground Routing configuration", @@ -72,6 +83,7 @@ "hide_console_9bbb309e": "Hide Console", "host_name_d865cef3": "Host name", "i_don_t_know_the_shared_password_336b198": "I don't know the shared password", + "if_some_of_these_nodes_no_longer_belong_to_the_net_a75d316f": "If some of these nodes no longer belong to the network you can delete them from Delete Nodes.", "interface_177dac54": "Interface", "internet_connection_fda60ffa": "Internet connection", "ip_addresses_440ac240": "IP Addresses", @@ -135,6 +147,7 @@ "select_file_71aa4113": "Select file", "select_new_node_5b2e9165": "Select new node", "select_one_b647b384": "Select one", + "select_the_nodes_which_no_longer_belong_to_the_net_8ed27f05": "Select the nodes which no longer belong to the network and delete them from the list of disconnected nodes", "set_network_bcd0ea96": "Set network", "setting_network_21ebac51": "Setting network", "setting_up_new_password_4daf8f1c": "Setting up new password", @@ -147,6 +160,7 @@ "size_b30e1077": "Size", "station_name_7d67417c": "Station name", "status_e7fdbe06": "Status", + "successfully_deleted_23ce0a20": "Successfully deleted", "system_55b0ca91": "System", "the_are_not_mesh_interfaces_available_4055abd7": "The are not mesh interfaces available", "the_download_failed_130e1274": "The download failed", @@ -156,9 +170,12 @@ "the_selected_image_is_not_valid_for_the_target_dev_cea9b494": "The selected image is not valid for the target device", "the_shared_password_has_been_chosen_by_the_communi_f9d30a92": "The shared password has been chosen by the community when the network was created. You can ask other community members for it.", "the_upgrade_should_be_done_d66854": "The upgrade should be done", + "there_are_no_left_discconected_nodes_cd78852e": "There are no left discconected nodes", "there_s_an_active_remote_support_session_4a40a8bb": "There's an active remote support session", "there_s_no_open_session_for_remote_support_click_a_efd0d415": "There's no open session for remote support. Click at Create Session to begin one", "these_are_the_nodes_associated_on_this_radio_3d302167": "These are the nodes associated on this radio", + "these_are_the_nodes_with_which_you_do_not_have_con_ef5cc209": "These are the nodes with which you do not have connectivity, it is possible that they are not turned on or a link to reach them is down.", + "these_are_the_nodes_with_which_you_have_connectivi_ef11819b": "These are the nodes with which you have connectivity, i.e. there is a working path from your node to each of them.", "this_device_does_not_support_secure_rollback_to_pr_1c167a2c": "This device does not support secure rollback to previous version if something goes wrong", "this_device_supports_secure_rollback_to_previous_v_a60ddbcb": "This device supports secure rollback to previous version if something goes wrong", "this_node_is_the_gateway_1e20aaff": "This node is the gateway", diff --git a/i18n/translations/en.json b/i18n/translations/en.json new file mode 100644 index 00000000..0eeaca9a --- /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 00000000..6159bd1d --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/index.js @@ -0,0 +1,13 @@ +import Page from './src/networkNodesPage'; +import { NetworkNodesMenu } from './src/networkNodesMenu'; +import DeleteNodesPage from './src/containers/deleteNodesPage'; + +export default { + name: 'NetworkNodes', + page: Page, + menu: NetworkNodesMenu, + menuView: 'community', + additionalProtectedRoutes: [ + ['delete-nodes', DeleteNodesPage] + ] +}; diff --git a/plugins/lime-plugin-reachable-nodes/networkNodes.spec.js b/plugins/lime-plugin-reachable-nodes/networkNodes.spec.js new file mode 100644 index 00000000..cfc9fb3b --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/networkNodes.spec.js @@ -0,0 +1,134 @@ +import { h } from 'preact'; +import { fireEvent, act, screen } from '@testing-library/preact'; +import '@testing-library/jest-dom'; +import waitForExpect from 'wait-for-expect'; + +import NetworkNodesPage from './src/networkNodesPage'; +import DeleteNodesPage from './src/containers/deleteNodesPage'; +import queryCache from 'utils/queryCache'; +import { getNodes, markNodesAsGone } from './src/networkNodesApi'; +import { render } from 'utils/test_utils'; + +jest.mock('./src/networkNodesApi'); + +beforeEach(() => { + getNodes.mockImplementation(async () => [ + { hostname: 'node1', status: 'connected' }, + { hostname: 'node2', status: 'connected' }, + { hostname: 'node3', status: 'connected' }, + { hostname: 'node4', status: 'disconnected' }, + { hostname: 'node5', status: 'disconnected' }, + { hostname: 'node6', status: 'disconnected' }, + { hostname: 'node7', status: 'disconnected' }, + { hostname: 'node8', status: 'gone' }, + { hostname: 'node9', status: 'gone' }, + ]); + markNodesAsGone.mockImplementation(async () => []); +}); + +afterEach(() => { + act(() => queryCache.clear()); +}); + +describe('network nodes screen', () => { + it('shows one tab for connected nodes and one for discconected nodes with length', async () => { + render(); + expect(await screen.findByRole('tab', { name: /^connected \(3\)/i })).toBeVisible(); + expect(await screen.findByRole('tab', { name: /^disconnected \(4\)/i })).toBeVisible(); + }) + + it('shows one row with the hostname for each connect node', async () => { + render(); + expect(await screen.findByText('node1')).toBeVisible(); + expect(await screen.findByText('node2')).toBeVisible(); + expect(await screen.findByText('node3')).toBeVisible(); + }) + + it('shows one row with the hostname for each disconnect node', async () => { + render(); + const tabDisconnected = await screen.findByRole('tab', { name: /^disconnected \(4\)/i }); + fireEvent.click(tabDisconnected); + expect(await screen.findByText('node4')).toBeVisible(); + expect(await screen.findByText('node5')).toBeVisible(); + expect(await screen.findByText('node6')).toBeVisible(); + expect(await screen.findByText('node7')).toBeVisible(); + }) + + it('shows a link to go to delete nodes page', async () => { + render(); + const tabDisconnected = await screen.findByRole('tab', { name: /^disconnected \(4\)/i }); + fireEvent.click(tabDisconnected); + expect(await screen.findByRole('link', { name: /go to delete nodes/i })).toBeVisible(); + }) + + it('does not show a link to go to delete nodes page if there are no disconnected nodes', async () => { + getNodes.mockImplementation(async () => [ + { hostname: 'node1', status: 'connected' }, + { hostname: 'node2', status: 'connected' }, + { hostname: 'node3', status: 'connected' }, + ]); + render(); + const tabDisconnected = await screen.findByRole('tab', { name: /^disconnected \(0\)/i }); + fireEvent.click(tabDisconnected); + expect(screen.queryByRole('link', { name: /go to delete nodes/i })).toBeNull(); + }) + + it('shows help message when clicking on help button', async () => { + render(); + const helpButton = await screen.findByLabelText('help'); + fireEvent.click(helpButton); + expect(await screen.findByText("Connected Nodes")).toBeVisible(); + expect(await screen.findByText("These are the nodes with which you have connectivity, " + + "i.e. there is a working path from your node to each of them.")).toBeVisible(); + expect(await screen.findByText("Disconnected Nodes")).toBeVisible(); + expect(await screen.findByText("These are the nodes with which you do not have connectivity, " + + "it is possible that they are not turned on or a link to reach them is down.")).toBeVisible(); + }) +}); + + +describe('delete nodes page', () => { + it('shows the list of disconnected nodes only', async () => { + render(); + expect(await screen.findByText('node4')).toBeVisible(); + expect(await screen.findByText('node5')).toBeVisible(); + expect(await screen.findByText('node6')).toBeVisible(); + expect(await screen.findByText('node7')).toBeVisible(); + expect(screen.queryByText('node1')).toBeNull(); + expect(screen.queryByText('node2')).toBeNull(); + expect(screen.queryByText('node3')).toBeNull(); + expect(screen.queryByText('node8')).toBeNull(); + expect(screen.queryByText('node9')).toBeNull(); + }) + + it('calls the markNodesAsGone api when deleting', async () => { + markNodesAsGone.mockImplementation(async () => ['node6']); + render(); + fireEvent.click(await screen.findByText('node6')); + fireEvent.click(await screen.findByRole('button', { name: /delete/i })); + await waitForExpect(() => { + expect(markNodesAsGone).toBeCalledWith(['node6']); + }) + }) + + it('hide nodes from the list after deletion', async () => { + markNodesAsGone.mockImplementation(async () => ['node6', 'node7']); + render(); + fireEvent.click(await screen.findByText('node6')); + fireEvent.click(await screen.findByText('node7')); + fireEvent.click(await screen.findByRole('button', { name: /delete/i })); + expect(await screen.findByText('node4')).toBeVisible(); + expect(await screen.queryByText('node5')).toBeVisible(); + expect(await screen.queryByText('node6')).toBeNull(); + expect(await screen.queryByText('node7')).toBeNull(); + }) + + it('show success message after deletion', async () => { + markNodesAsGone.mockImplementation(async () => ['node6', 'node7']); + render(); + fireEvent.click(await screen.findByText('node6')); + fireEvent.click(await screen.findByText('node7')); + fireEvent.click(await screen.findByRole('button', { name: /delete/i })); + expect(await screen.findByText(/successfully deleted/i)).toBeVisible(); + }) +}) \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/networkNodes.stories.js b/plugins/lime-plugin-reachable-nodes/networkNodes.stories.js new file mode 100644 index 00000000..86ada871 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/networkNodes.stories.js @@ -0,0 +1,34 @@ +import { NetworkNodesPage_ } from "./src/networkNodesPage"; +import { DeleteNodesPage_ } from "./src/containers/deleteNodesPage"; + +export default { + title: 'Containers/Network nodes', +}; + +const nodes = [ + { hostname: "ql-czuk", status: "connected" }, + { hostname: "ql-irene", status: "connected" }, + { hostname: "ql-ipem", status: "connected" }, + { hostname: "ql-czuck-bbone", status: "connected" }, + { hostname: "ql-graciela", status: "connected" }, + { hostname: "ql-marisa", status: "connected" }, + { hostname: "ql-anaymarcos", status: "connected" }, + { hostname: "ql-quinteros", status: "connected" }, + { hostname: "ql-guada", status: "connected" }, + { hostname: "ql-refu-bbone", status: "disconnected" }, + { hostname: "si-soniam", status: "disconnected" }, + { hostname: "si-giordano", status: "disconnected" }, + { hostname: "si-mario", status: "disconnected" }, + { hostname: "si-manu", status: "disconnected" }, +]; + +export const networkNodesPage = () => ( + +) + +export const deleteNodesPage = (args) => ( + +) +deleteNodesPage.argTypes = { + onDelete: { action: 'deleted' } +} \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js new file mode 100644 index 00000000..f6171fcc --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/containers/deleteNodesPage/deleteNodesPage.js @@ -0,0 +1,86 @@ +import { h } from "preact"; +import { List, ListItem } from 'components/list'; +import Loading from 'components/loading'; +import Toast from 'components/toast'; +import { useEffect, useState } from 'preact/hooks'; +import { useSet } from 'react-use'; +import { useMarkNodesAsGone, useNetworkNodes } from '../../networkNodesQueries' +import style from './style.less'; +import I18n from 'i18n-js'; + +export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) => { + const [selectedNodes, { toggle, has, reset }] = useSet(new Set([])); + const [showSuccess, setshowSuccess] = useState(false); + const disconnectedNodes = nodes.filter(n => n.status === "disconnected"); + + useEffect(() => { + if (isSuccess) { + reset(); + setshowSuccess(true); + setTimeout(() => { + setshowSuccess(false); + }, 2000); + } + }, [isSuccess]) + + return ( +
+
+

{I18n.t("Delete Nodes")}

+ {disconnectedNodes.length > 0 && +

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

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

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

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

+

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

+

+

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

+
+); + +const PageTabs = ({ nodes, ...props }) => { + const nConnected = nodes.filter(n => n.status === "connected").length; + const nDisconnected = nodes.filter(n => n.status === "disconnected").length; + const tabs = [ + { key: 'connected', repr: I18n.t('Connected (%{howMany})', { howMany: nConnected }) }, + { key: 'disconnected', repr: I18n.t('Disconnected (%{howMany})', { howMany: nDisconnected }) }, + ]; + return +} + +export const NetworkNodesPage_ = ({ nodes }) => { + const [selectedGroup, setselectedGroup] = useState('connected'); + return ( +
+
+ +
+ +
+
+ + {nodes + .filter(n => n.status === selectedGroup) + .sort((a, b) => a.hostname > b.hostname) + .map( + node => + +
+ {node.hostname} +
+
+ )} + {selectedGroup === "disconnected" && + nodes.filter(n => n.status == "disconnected").length && + + } +
+
+ ) +} + +const NetworkNodesPage = () => { + const { data: nodes, isLoading } = useNetworkNodes(); + + if (isLoading) { + return
+ } + + return +} + +export default NetworkNodesPage \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js b/plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js new file mode 100644 index 00000000..fb9b8b76 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/networkNodesQueries.js @@ -0,0 +1,16 @@ +import { useQuery, useMutation } from 'react-query'; +import { getNodes, markNodesAsGone } from './networkNodesApi'; +import queryCache from 'utils/queryCache'; + +export const useNetworkNodes = () => useQuery(['network-nodes', 'get_nodes'], getNodes) + +export const useMarkNodesAsGone = () => useMutation(markNodesAsGone, { + onSuccess: hostnames => queryCache.setQueryData(['network-nodes', 'get_nodes'], + old => { + const result = old.map( + node => hostnames.indexOf(node.hostname) != -1 ? { ...node, status: "gone" } : node + ) + return result; + } + ) +}) \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/src/style.less b/plugins/lime-plugin-reachable-nodes/src/style.less new file mode 100644 index 00000000..b3ea1445 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/style.less @@ -0,0 +1,15 @@ +.deleteNodesLegend { + text-align: center; + flex: auto; + padding: 0.5em; +} + +.nodeItem { + font-size: 2rem; + display: flex; + flex: auto +} + +.helpWrapper { + padding: 1em; +} \ No newline at end of file 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 53a06c08..00000000 --- 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 83695d01..e86f61e0 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 00000000..060f10aa --- /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 00000000..a8ae2df8 --- /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 00000000..35389401 --- /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 00000000..e3279673 --- /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 f6171fcc..578ef834 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 a2d02a0b..38feb6ce 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 69fef588..c5ee2904 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 720909a9..0e82bc51 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 a279dd8c..00000000 --- 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 35389401..d21ee466 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 675c2adc..89eb8ea8 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 d21ee466..d4a606bb 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 060f10aa..9dfe24fc 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 578ef834..f0417d7d 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 89eb8ea8..8f371914 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 e86f61e0..8497de20 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 e3279673..091be2c8 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 6159bd1d..f378f77b 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 cfc9fb3b..00000000 --- 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 86ada871..00000000 --- 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 00000000..be0d3371 --- /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 00000000..195b3226 --- /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 060496ce..00000000 --- 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 bc1e2fef..00000000 --- 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 ba59c9d4..00000000 --- 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 55cd73d1..00000000 --- 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 fb9b8b76..00000000 --- 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 00000000..55b9138b --- /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 00000000..92172c6b --- /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 b3ea1445..00000000 --- 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 fa98012f..1cb7fd19 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 8f371914..8459f73b 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 00000000..70f42889 --- /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 00000000..e9a01c90 --- /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 00000000..ed3e95da --- /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 00000000..a4da4489 --- /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 00000000..78201275 --- /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 8459f73b..f9ab9b77 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 8497de20..c194666e 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 00000000..0b786cbe --- /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 ed3e95da..0e1ba5a9 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 a4da4489..16d4c800 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 00000000..3f3dbe58 --- /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 78201275..00000000 --- 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 38feb6ce..fc588626 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 c5ee2904..c9c83a9d 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 8cfbf48c..009ce5c6 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 8624eb5d..e3ebeaf4 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 5a72fcdf..539c126f 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 9dfe24fc..8ec7143d 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 f0417d7d..418a762f 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 0e82bc51..ad8a9356 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 be0d3371..e490f644 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 92172c6b..bc02bd4f 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 0e1ba5a9..99b5c203 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 16d4c800..aa3d68be 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 f378f77b..6d601450 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 d7e9ed58..c455134b 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 091be2c8..5f7be227 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 418a762f..b33a0311 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 e3ebeaf4..05186ad9 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 0303aa3c..9f2019d7 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 539c126f..f256d8b0 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 bc02bd4f..798a80b9 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 e9a01c90..38dcd2f4 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 99b5c203..b14e582e 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 }