diff --git a/i18n/generic.json b/i18n/generic.json new file mode 100644 index 00000000..c194666e --- /dev/null +++ b/i18n/generic.json @@ -0,0 +1,215 @@ +{ + "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" + }, + "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", + "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", + "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", + "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", + "reachable_howmany_6f891e31": "Reachable (%{howMany})", + "reachable_nodes_748c93f0": "Reachable Nodes", + "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", + "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", + "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", + "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", + "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_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", + "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", + "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", + "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", + "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", + "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 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/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/deleteNodes.spec.js b/plugins/lime-plugin-delete-nodes/deleteNodes.spec.js new file mode 100644 index 00000000..8ec7143d --- /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 () => ({ + '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 () => []); + }); + + afterEach(() => { + act(() => queryCache.clear()); + getNodes.mockClear(); + markNodesAsGone.mockClear(); + }); + + it('shows the list of unreachable 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(); + }) +}) 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..d4a606bb --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/index.js @@ -0,0 +1,10 @@ +import Page from './src/deleteNodesPage'; +import Menu from './src/deleteNodesMenu'; + +export default { + name: 'deleteNodes', + page: Page, + menu: Menu, + isCommunityProtected: true, + menuView: 'community' +}; 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..5f7be227 --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js @@ -0,0 +1,8 @@ +import { h } from 'preact'; +import { Trans } from '@lingui/macro'; + +const Menu = () => ( + 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 new file mode 100644 index 00000000..b33a0311 --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js @@ -0,0 +1,91 @@ +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 'plugins/lime-plugin-network-nodes/src/networkNodesQueries' +import style from './deleteNodesStyle.less'; +import { Trans } from '@lingui/macro'; + +export const DeleteNodesPage_ = ({ nodes, onDelete, isSubmitting, isSuccess }) => { + const [selectedNodes, { toggle, has, reset }] = useSet(new Set([])); + const [showSuccess, setshowSuccess] = useState(false); + const unreachableNodes = Object.values(nodes).filter(n => n.status === "unreachable"); + + useEffect(() => { + if (isSuccess) { + reset(); + setshowSuccess(true); + setTimeout(() => { + setshowSuccess(false); + }, 2000); + } + }, [isSuccess]) + + return ( +
+
+

Delete Nodes

+ {unreachableNodes.length > 0 && +

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

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

There are no left unreachable nodes

+ } + + {unreachableNodes.map(node => + toggle(node.hostname)} > +
+ + {node.hostname} +
+
+ )} +
+
+
+ + + + {!isSubmitting && + + } + {isSubmitting && +
+ +
+ } +
+ {showSuccess && + Successfully deleted} /> + } +
+ ) +}; + +const DeleteNodesPage = () => { + const [deleteNodes, { isSubmitting, isSuccess }] = useMarkNodesAsGone(); + const { data: nodes, isLoading } = useNetworkNodes(); + if (isLoading) { + return
+ } + + return +} + +export default DeleteNodesPage; diff --git a/plugins/lime-plugin-delete-nodes/src/deleteNodesStyle.less b/plugins/lime-plugin-delete-nodes/src/deleteNodesStyle.less new file mode 100644 index 00000000..6e844dce --- /dev/null +++ b/plugins/lime-plugin-delete-nodes/src/deleteNodesStyle.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-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..009ce5c6 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/networkNodes.spec.js @@ -0,0 +1,69 @@ +// 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', + status: 'recently_connected' + }, + "ql-nelson": { + ipv4: '10.5.0.17', + ipv6: 'fd0d:fe46:8ce8::8bbf:75bf', + board: 'LibreRouter v1', + 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' + } + })); + }); + + afterEach(() => { + cleanup(); + act(() => queryCache.clear()); + }); + + 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(); + }); + + 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.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(); + }); + + 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/components/expandableNode/index.js b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js new file mode 100644 index 00000000..05186ad9 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/components/expandableNode/index.js @@ -0,0 +1,25 @@ +import { h } from 'preact'; +import { Trans } from '@lingui/macro'; +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 && +
e.stopPropagation()}> + {ipv4 &&
IPv4: {ipv4}
} + {ipv6 &&
IPv6: {ipv6}
} + {board &&
Device: {board}
} + {fw_version &&
Firmware: {fw_version}
} +
+ } +
+
+ ) +} 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..fc588626 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.js @@ -0,0 +1,11 @@ +import api from 'utils/uhttpd.service'; + +export const getNodes = () => + 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 }) + .then(() => hostnames); 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..c9c83a9d --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesApi.spec.js @@ -0,0 +1,47 @@ +import { getNodes, markNodesAsGone } from './networkNodesApi' +import api from 'utils/uhttpd.service'; +jest.mock('utils/uhttpd.service') + +beforeEach(() => { + api.call.mockImplementation(async () => ({ 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(async () => ({ status: 'ok', nodes })); + expect(await getNodes()).toEqual(nodes); + }); +}); + +describe('markNodesAsGone', () => { + it('calls the expected endpoint', async () => { + 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(async () => ({status: 'ok'})); + const result = await markNodesAsGone(['node1', 'node2']) + expect(result).toEqual(['node1', 'node2']) + }) +}); 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..9f2019d7 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesMenu.js @@ -0,0 +1,8 @@ +import { h } from 'preact'; +import { Trans } from '@lingui/macro'; + +const Menu = () => ( + 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..f256d8b0 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesPage.js @@ -0,0 +1,49 @@ +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 { Trans } from '@lingui/macro'; + +export const _NetworkNodes = ({ nodes, isLoading, unfoldedNode, onUnfold }) => { + if (isLoading) { + return
+ } + return ( +
+
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 })) + .filter(n => n.status !== 'gone') + .sort((a, b) => a.hostname > b.hostname)); + + 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..ad8a9356 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesQueries.js @@ -0,0 +1,18 @@ +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; + hostnames.forEach(hostname => { + result[hostname] = {...old[hostname], status: "gone"} + }); + return result; + } + ) +}) 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..81dacaf6 --- /dev/null +++ b/plugins/lime-plugin-network-nodes/src/networkNodesStyle.less @@ -0,0 +1,5 @@ +.title { + text-align: center; + font-size: 2em; + padding-top: 1rem; +} \ No newline at end of file diff --git a/plugins/lime-plugin-reachable-nodes/index.js b/plugins/lime-plugin-reachable-nodes/index.js new file mode 100644 index 00000000..6d601450 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/index.js @@ -0,0 +1,9 @@ +import Page from './src/reachableNodesPage'; +import { ReachableNodesMenu } from './src/reachableNodesMenu'; + +export default { + name: 'ReachableNodes', + page: Page, + menu: ReachableNodesMenu, + menuView: 'community', +}; 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..e490f644 --- /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 () => ({ + '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 () => []); +}); + +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/reachableNodesMenu.js b/plugins/lime-plugin-reachable-nodes/src/reachableNodesMenu.js new file mode 100644 index 00000000..72998454 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/reachableNodesMenu.js @@ -0,0 +1,7 @@ +import { h } from 'preact'; + +import { Trans } from '@lingui/macro'; + +export const ReachableNodesMenu = () => ( + Reachable Nodes +); 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..798a80b9 --- /dev/null +++ b/plugins/lime-plugin-reachable-nodes/src/reachableNodesPage.js @@ -0,0 +1,88 @@ +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 { Trans } from '@lingui/macro'; + +const PageHelp = () => ( +
+

+

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. + +
+ + This information is synced periodically + and can be outdated by some minutes. + +

+

+

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. + +

+
+); + +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: Reachable ({nReachable}) }, + { key: 'unreachable', repr: Unreachable ({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 ( +
+
+ +
+ +
+
+ + {Object.values(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-upgraded-nodes/index.js b/plugins/lime-plugin-upgraded-nodes/index.js new file mode 100644 index 00000000..70f42889 --- /dev/null +++ b/plugins/lime-plugin-upgraded-nodes/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-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-upgraded-nodes/src/upgradedNodesMenu.js b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js new file mode 100644 index 00000000..38dcd2f4 --- /dev/null +++ b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesMenu.js @@ -0,0 +1,8 @@ +import { h } from 'preact'; +import { Trans } from '@lingui/macro'; + +const Menu = () => ( + 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 new file mode 100644 index 00000000..b14e582e --- /dev/null +++ b/plugins/lime-plugin-upgraded-nodes/src/upgradedNodesPage.js @@ -0,0 +1,122 @@ +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"; +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 { Trans } from '@lingui/macro'; + +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 = () => ( +
+

+

Upgraded Nodes
+ These are the nodes running 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 +

+
+); + +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: Upgraded ({nUpgraded})}, + { key: 'not_upgraded', repr: Not Upgraded ({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); + } + + 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 ( +
+
+ +
+ +
+
+ + {grouppedByVersion + .map((versionGroup, index) => ( + + {selectedGroup === 'not_upgraded' && +
+ {versionGroup.key} +
+ } + {versionGroup.values + .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 = [...Object.values(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-upgraded-nodes/upgradedNodes.spec.js b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js new file mode 100644 index 00000000..aa3d68be --- /dev/null +++ b/plugins/lime-plugin-upgraded-nodes/upgradedNodes.spec.js @@ -0,0 +1,52 @@ +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 () => ({ + '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' + })) + }); + + 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-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/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 99197c6a..f9ab9b77 100644 --- a/src/config.js +++ b/src/config.js @@ -10,6 +10,10 @@ 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'; +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 = [ @@ -24,5 +28,9 @@ export const plugins = [ ChangeNode, RemoteSupport, Pirania, + NetworkNodes, + ReachableNodes, + UpgradedNodes, + DeleteNodes, Fbw // fbw does not have menu item ];