Skip to content

Commit

Permalink
feat(network nodes): add network nodes page
Browse files Browse the repository at this point in the history
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
  • Loading branch information
germanferrero committed Oct 14, 2021
1 parent 142d0ac commit f33e77a
Show file tree
Hide file tree
Showing 15 changed files with 507 additions and 2 deletions.
17 changes: 17 additions & 0 deletions i18n/generic.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -65,13 +75,15 @@
"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",
"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",
"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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,9 @@
"zero": "No one has joined yet.",
"one": "One person has joined.",
"other": "%{count} people have joined."
},
"count_selected_nodes_19bbd632": {
"one": "node selected",
"other": "nodes selected"
}
}
21 changes: 19 additions & 2 deletions i18n/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,25 @@
"visit_a_neighboring_node_4116be4": "Visitar un nodo vecino",
"select_new_node_5b2e9165": "Selecciona el nodo",
"visit_864b4060": "Visitar",
"go_to_community_view_d12b8d67": "Ir a Vista de Comunidad",
"go_to_node_view_26ba929d": "Ir a Vista de Nodo",
"network_nodes_4368eb67": "Nodos de la Red"
"network_nodes_4368eb67": "Nodos de la Red",
"connected_howmany_228826ea": "Conectados (%{howMany})",
"connected_nodes_4b7e6f49": "Nodos Conectados",
"delete_a6efa79d": "Eliminar",
"delete_nodes_f63ec0d5": "Eliminar Nodos",
"disconnected_howmany_10fc7bd5": "Desconectados (%{howMany})",
"disconnected_nodes_88f80d1e": "Nodos Desconectados",
"go_to_community_view_d12b8d67": "Ir a Vista de Comunidad",
"go_to_delete_nodes_1203128b": "Ir a Eliminar Nodos",
"count_selected_nodes_19bbd632": {
"one": "nodo seleccionado",
"other": "nodos seleccionados"
},
"successfully_deleted_23ce0a20": "Eliminado/s correctamente",
"there_are_no_left_discconected_nodes_cd78852e": "No hay nodos desconectados",
"these_are_the_nodes_with_which_you_do_not_have_con_ef5cc209": "Son los nodos con los que no tienes conectividad, es posible que no estén encendidos o que algún enlace para llegar a ellos esté caído.",
"these_are_the_nodes_with_which_you_have_connectivi_ef11819b": "Son los nodos con los que tienes conectividad, es decir que hay un camino funcionando entre tu nodo y cada uno de ellos.",
"if_some_of_these_nodes_no_longer_belong_to_the_net_a75d316f": "Si alguno de estos nodos no pertence más a la red puedes eliminarlo desde Eliminar Nodos.",
"select_the_nodes_which_no_longer_belong_to_the_net_8ed27f05": "Selecciona los nodos que ya no pertenecen a la red y elimínalos de la lista de nodos desconectados"
}

13 changes: 13 additions & 0 deletions plugins/lime-plugin-reachable-nodes/index.js
Original file line number Diff line number Diff line change
@@ -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]
]
};
134 changes: 134 additions & 0 deletions plugins/lime-plugin-reachable-nodes/networkNodes.spec.js
Original file line number Diff line number Diff line change
@@ -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(<NetworkNodesPage />);
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(<NetworkNodesPage />);
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(<NetworkNodesPage />);
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(<NetworkNodesPage />);
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(<NetworkNodesPage />);
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(<NetworkNodesPage />);
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(<DeleteNodesPage />);
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(<DeleteNodesPage />);
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(<DeleteNodesPage />);
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(<DeleteNodesPage />);
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();
})
})
34 changes: 34 additions & 0 deletions plugins/lime-plugin-reachable-nodes/networkNodes.stories.js
Original file line number Diff line number Diff line change
@@ -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 = () => (
<NetworkNodesPage_ nodes={nodes} />
)

export const deleteNodesPage = (args) => (
<DeleteNodesPage_ nodes={nodes} {...args} />
)
deleteNodesPage.argTypes = {
onDelete: { action: 'deleted' }
}
Original file line number Diff line number Diff line change
@@ -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 (
<div class="d-flex flex-column flex-grow-1 overflow-auto ">
<div class="d-flex flex-column flex-grow-1 overflow-auto container container-padded">
<h4>{I18n.t("Delete Nodes")}</h4>
{disconnectedNodes.length > 0 &&
<p>{I18n.t("Select the nodes which no longer belong to the network and "
+ "delete them from the list of disconnected nodes")}</p>
}
{disconnectedNodes.length === 0 &&
<p>{I18n.t("There are no left discconected nodes")}</p>
}
<List>
{disconnectedNodes.map(node =>
<ListItem key={node.hostname} onClick={() => toggle(node.hostname)} >
<div class={style.nodeItem} >
<input type="checkbox" name="selected-nodes" id={node.hostname}
checked={has(node.hostname)} />
{node.hostname}
</div>
</ListItem>
)}
</List>
</div>
{selectedNodes.size >= 1 &&
<div class={style.bottomAction}>
<span>
{[selectedNodes.size,
I18n.t('selected-nodes', { count: selectedNodes.size })
].join(' ')}
</span>
{!isSubmitting &&
<button class="ml-auto" onClick={() => onDelete([...selectedNodes])}>
{I18n.t("Delete")}
</button>
}
{isSubmitting &&
<div class="ml-auto">
<Loading />
</div>
}
</div>
}
{showSuccess &&
<Toast type={"success"} text={I18n.t("Successfully deleted")} />
}
</div>
)
};

const DeleteNodesPage = () => {
const [deleteNodes, { isSubmitting, isSuccess }] = useMarkNodesAsGone();
const { data: nodes, isLoading } = useNetworkNodes();
if (isLoading) {
return <div className="container container-center"><Loading /></div>
}

return <DeleteNodesPage_ nodes={nodes} onDelete={deleteNodes}
isSubmitting={isSubmitting} isSuccess={isSuccess} />
}

export default DeleteNodesPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './deleteNodesPage';
export { default } from './deleteNodesPage';
Loading

0 comments on commit f33e77a

Please sign in to comment.