Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Reachable/Unreachable Nodes and Delete Nodes Screens #301

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions i18n/generic.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,16 @@
"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",
Expand Down Expand Up @@ -95,6 +101,7 @@
"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",
Expand All @@ -116,6 +123,8 @@
"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",
Expand All @@ -134,6 +143,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_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",
Expand All @@ -146,6 +156,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 @@ -155,18 +166,24 @@
"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_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",
"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",
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"
}
}
19 changes: 18 additions & 1 deletion i18n/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,24 @@
"visit_a_neighboring_node_4116be4": "Visitar un nodo vecino",
"select_new_node_5b2e9165": "Selecciona el nodo",
"visit_864b4060": "Visitar",
"go_to_node_view_26ba929d": "Ir a Vista de Nodo",
"network_nodes_4368eb67": "Nodos de la Red",
"delete_a6efa79d": "Eliminar",
"delete_nodes_f63ec0d5": "Baja de Nodos",
"go_to_community_view_d12b8d67": "Ir a Vista de Comunidad",
"go_to_node_view_26ba929d": "Ir a Vista de Nodo"
"count_selected_nodes_19bbd632": {
"one": "nodo seleccionado",
"other": "nodos seleccionados"
},
"successfully_deleted_23ce0a20": "Eliminado/s correctamente",
"select_the_nodes_which_no_longer_belong_to_the_net_92f853ef": "Selecciona los nodos que ya no pertenecen a la red y elimínalos de la lista de nodos no alcanzables",
"there_are_no_left_unreachable_nodes_c0bec63d": "No hay nodos inalcanzables",
"reachable_howmany_6f891e31": "Alcanzables (%{howMany})",
"reachable_nodes_748c93f0": "Nodos Alcanzables",
"these_are_the_nodes_that_can_be_reached_from_your__4c524abe": "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_that_can_t_be_reached_from_you_dbbf9032": "Son los nodos con los que tienes conectividad, es decir que hay un camino funcionando entre tu nodo y cada uno de ellos.",
"this_information_is_synced_periodically_and_can_be_8b74cb8c": "Esta información se sincroniza periódicamente, puede estar desactualizada algunos minutos.",
"unreachable_howmany_e5c8f844": "No Alcanzables (%{howMany})",
"unreachable_nodes_e6785f10": "Nodos No Alcanzables"
}

3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
moduleNameMapper: {
...preactPreset.moduleNameMapper,
'^components/(.*)$': '<rootDir>/src/components/$1',
'^utils/(.*)$': '<rootDir>/src/utils/$1'
'^utils/(.*)$': '<rootDir>/src/utils/$1',
'^plugins/(.*)$': '<rootDir>/plugins/$1'
}
};
78 changes: 78 additions & 0 deletions plugins/lime-plugin-delete-nodes/deleteNodes.spec.js
Original file line number Diff line number Diff line change
@@ -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_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());
getNodes.mockClear();
markNodesAsGone.mockClear();
});

it('shows the list of unreachable 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();
})
})
21 changes: 21 additions & 0 deletions plugins/lime-plugin-delete-nodes/deleteNodes.stories.js
Original file line number Diff line number Diff line change
@@ -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_ nodes={nodes} {...args} />
);

deleteNodesPage.argTypes = {
onDelete: { action: 'deleted' }
};
10 changes: 10 additions & 0 deletions plugins/lime-plugin-delete-nodes/index.js
Original file line number Diff line number Diff line change
@@ -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'
};
8 changes: 8 additions & 0 deletions plugins/lime-plugin-delete-nodes/src/deleteNodesMenu.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { h } from 'preact';
import I18n from 'i18n-js';

const Menu = () => (
<a href={'#/deletenodes'}>{I18n.t('Delete Nodes')}</a>
);

export default Menu;
86 changes: 86 additions & 0 deletions plugins/lime-plugin-delete-nodes/src/deleteNodesPage.js
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 'plugins/lime-plugin-network-nodes/src/networkNodesQueries'
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 unreachableNodes = nodes.filter(n => n.status === "unreachable");

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>
{unreachableNodes.length > 0 &&
<p>{I18n.t("Select the nodes which no longer belong to the network and "
+ "delete them from the list of unreachable nodes")}</p>
}
{unreachableNodes.length === 0 &&
<p>{I18n.t("There are no left unreachable nodes")}</p>
}
<List>
{unreachableNodes.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>
<div class={style.bottomAction}>
<span>
{[selectedNodes.size,
I18n.t('selected-nodes', { count: selectedNodes.size })
].join(' ')}
</span>
{!isSubmitting &&
<button class="ml-auto"
onClick={() => onDelete([...selectedNodes])}
disabled={selectedNodes.size < 1}>
{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;
17 changes: 17 additions & 0 deletions plugins/lime-plugin-delete-nodes/src/deleteNodesStyle.less
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 9 additions & 0 deletions plugins/lime-plugin-network-nodes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Page from './src/networkNodesPage';
import Menu from './src/networkNodesMenu';

export default {
name: 'networkNodes',
page: Page,
menu: Menu,
menuView: 'community'
};
Loading