From cc5a9b103386f396c7132f48d8183a7f21c8a5bc Mon Sep 17 00:00:00 2001 From: Sarooj bukhari Date: Mon, 10 Jul 2023 19:49:52 +0500 Subject: [PATCH] Fix table loading glitch (#227) --- src/index.css | 5 + src/views/AccessControl.tsx | 84 +-- src/views/Nameservers.tsx | 82 +-- src/views/Peers.tsx | 97 +-- src/views/Routes.tsx | 1229 +++++++++++++---------------------- src/views/SetupKeys.tsx | 126 ++-- 6 files changed, 661 insertions(+), 962 deletions(-) diff --git a/src/index.css b/src/index.css index 094486fe..f3bb4a5f 100644 --- a/src/index.css +++ b/src/index.css @@ -467,4 +467,9 @@ ul.ant-list-items { .style-like-text .ant-select-arrow { display: none !important; +} + + +.ant-spin-nested-loading .ant-spin-spinning { + min-height: 300px; } \ No newline at end of file diff --git a/src/views/AccessControl.tsx b/src/views/AccessControl.tsx index 0b7c1dd3..6b8ae994 100644 --- a/src/views/AccessControl.tsx +++ b/src/views/AccessControl.tsx @@ -473,7 +473,7 @@ export const AccessControl = () => { }; const renderPorts = (ports: string[]) => { - if (!ports) { + if (!ports) { return ( ALL @@ -661,7 +661,46 @@ export const AccessControl = () => { /> )} - {!showTutorial && ( + {showTutorial && !loading ? ( + + + Create New Rule + + + It looks like you don't have any rules. {"\n"} + Get started with access control by adding a new one. + + {" "} + Learn more + + + + + ) : ( { }`} showSorterTooltip={false} scroll={{ x: true }} + style={{ minHeight: "300px" }} loading={tableSpin(loading)} dataSource={dataTable} > @@ -825,46 +865,6 @@ export const AccessControl = () => { />
)} - {showTutorial && ( - - - Create New Rule - - - It looks like you don't have any rules. {"\n"} - Get started with access control by adding a new one. - - {" "} - Learn more - - - - - )}
diff --git a/src/views/Nameservers.tsx b/src/views/Nameservers.tsx index 743d0f18..fd5167bd 100644 --- a/src/views/Nameservers.tsx +++ b/src/views/Nameservers.tsx @@ -540,7 +540,46 @@ export const Nameservers = () => { /> )} - {!showTutorial && ( + {showTutorial && !loading ? ( + + + Create Nameserver + + + It looks like you don't have any nameservers. {"\n"} + Get started by adding one to your network. + + {" "} + Learn more + + + + + ) : ( { }`} showSorterTooltip={false} scroll={{ x: true }} + style={{ minHeight: "300px" }} loading={tableSpin(loading)} dataSource={dataTable} > @@ -651,46 +691,6 @@ export const Nameservers = () => { />
)} - {showTutorial && ( - - - Create Nameserver - - - It looks like you don't have any nameservers. {"\n"} - Get started by adding one to your network. - - {" "} - Learn more - - - - - )}
diff --git a/src/views/Peers.tsx b/src/views/Peers.tsx index a51120ac..baa03c64 100644 --- a/src/views/Peers.tsx +++ b/src/views/Peers.tsx @@ -206,8 +206,6 @@ export const Peers = () => { } }, [deletedPeer]); - - const filterDataTable = (): Peer[] => { const t = textToSearch.toLowerCase().trim(); let f: Peer[] = filter(peers, (f: Peer) => { @@ -511,7 +509,14 @@ export const Peers = () => { let loginExpire = peer.login_expired ? ( - needs login + + + needs login + + ) : ( "" @@ -519,9 +524,11 @@ export const Peers = () => { const userEmail = users?.find((u) => u.id === peer.user_id)?.email; let expiry = !peer.login_expiration_enabled ? ( - - expiration disabled - + + + expiration disabled + + ) : null; if (!userEmail) { return ( @@ -573,12 +580,14 @@ export const Peers = () => { Peers {peers.length ? ( - - A list of all machines and devices connected to your private network. Use this view to manage peers + + A list of all machines and devices connected to your private + network. Use this view to manage peers ) : ( - - A list of all machines and devices connected to your private network. Use this view to manage peers + + A list of all machines and devices connected to your private + network. Use this view to manage peers )} @@ -649,7 +658,39 @@ export const Peers = () => { /> )} - {!showTutorial && ( + {showTutorial && !loading ? ( + + + Get Started + + + It looks like you don't have any connected machines.{" "} + {"\n"} + Get started by adding one to your network. + + + + ) : ( { scroll={{ x: true }} loading={tableSpin(loading)} dataSource={dataTable} + style={{ minHeight: "300px" }} > { />
)} - {showTutorial && ( - - - Get Started - - - It looks like you don't have any connected machines.{" "} - {"\n"} - Get started by adding one to your network. - - - - )}
diff --git a/src/views/Routes.tsx b/src/views/Routes.tsx index 0549ff16..4e7b34ac 100644 --- a/src/views/Routes.tsx +++ b/src/views/Routes.tsx @@ -1,143 +1,99 @@ import React, { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "typesafe-actions"; +import { actions as setupKeyActions } from "../store/setup-key"; +import { Container } from "../components/Container"; import { Alert, Button, Card, Col, Input, - Menu, message, Modal, - List, - Spin, Popover, Radio, RadioChangeEvent, Row, + Select, + Badge, Space, - Switch, Table, Tag, - Collapse, Typography, - Badge, } from "antd"; -import { Container } from "../components/Container"; -import { useDispatch, useSelector } from "react-redux"; -import { RootState } from "typesafe-actions"; -import { Route, RouteToSave } from "../store/route/types"; -import { actions as routeActions } from "../store/route"; -import { actions as peerActions } from "../store/peer"; -import { filter, sortBy } from "lodash"; -import { EllipsisOutlined, ExclamationCircleOutlined } from "@ant-design/icons"; -import RouteAddNew from "../components/RouteAddNew"; -import { - GroupedDataTable, - initPeerMaps, - masqueradeDisabledMSG, - masqueradeEnabledMSG, - peerToPeerIP, - RouteDataTable, - transformDataTable, - transformGroupedDataTable, -} from "../utils/routes"; -import { useGetTokenSilently } from "../utils/token"; +import { SetupKey, SetupKeyToSave } from "../store/setup-key/types"; +import { filter } from "lodash"; +import { formatDate, timeAgo } from "../utils/common"; +import { ExclamationCircleOutlined } from "@ant-design/icons"; +import SetupKeyNew from "../components/SetupKeyNew"; +import SetupKeyEdit from "../components/SetupKeyEdit"; +import ButtonCopyMessage from "../components/ButtonCopyMessage"; +import tableSpin from "../components/Spin"; +import { actions as groupActions } from "../store/group"; import { Group } from "../store/group/types"; import { TooltipPlacement } from "antd/es/tooltip"; -import { actions as groupActions } from "../store/group"; -import { useGetGroupTagHelpers } from "../utils/groups"; -import RouteUpdate from "../components/RouteUpdate"; -import RoutePeerUpdate from "../components/RoutePeerUpdate"; +import { useGetTokenSilently } from "../utils/token"; +import { usePageSizeHelpers } from "../utils/pageSize"; -const { Title, Paragraph, Text } = Typography; +const { Title, Text, Paragraph } = Typography; const { Column } = Table; -const { confirm } = Modal; -const { Panel } = Collapse; -export const Routes = () => { +interface SetupKeyDataTable extends SetupKey { + key: string; + groupsCount: number; +} + +export const SetupKeys = () => { + const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers(); const { getTokenSilently } = useGetTokenSilently(); const dispatch = useDispatch(); - const { getGroupNamesFromIDs } = useGetGroupTagHelpers(); - const groups = useSelector((state: RootState) => state.group.data); - const routes = useSelector((state: RootState) => state.route.data); - const failed = useSelector((state: RootState) => state.route.failed); - const loading = useSelector((state: RootState) => state.route.loading); - const deletedRoute = useSelector( - (state: RootState) => state.route.deletedRoute - ); - const setEditRoutePeerVisible = useSelector( - (state: RootState) => state.route.setEditRoutePeerVisible + const setupKeys = useSelector((state: RootState) => state.setupKey.data); + const failed = useSelector((state: RootState) => state.setupKey.failed); + const loading = useSelector((state: RootState) => state.setupKey.loading); + const deletedSetupKey = useSelector( + (state: RootState) => state.setupKey.deletedSetupKey ); - const savedRoute = useSelector((state: RootState) => state.route.savedRoute); - const peers = useSelector((state: RootState) => state.peer.data); - const loadingPeer = useSelector((state: RootState) => state.peer.loading); - const setupNewRouteVisible = useSelector( - (state: RootState) => state.route.setupNewRouteVisible + const savedSetupKey = useSelector( + (state: RootState) => state.setupKey.savedSetupKey ); - const [showTutorial, setShowTutorial] = useState(true); + const groups = useSelector((state: RootState) => state.group.data); + const [textToSearch, setTextToSearch] = useState(""); - const [optionAllEnable, setOptionAllEnable] = useState("enabled"); - const [dataTable, setDataTable] = useState([] as RouteDataTable[]); - const [routeToAction, setRouteToAction] = useState( - null as RouteDataTable | null + const [optionValidAll, setOptionValidAll] = useState("valid"); + const [dataTable, setDataTable] = useState([] as SetupKeyDataTable[]); + const setupNewKeyVisible = useSelector( + (state: RootState) => state.setupKey.setupNewKeyVisible ); - const [groupedDataTable, setGroupedDataTable] = useState( - [] as GroupedDataTable[] + const setupEditKeyVisible = useSelector( + (state: RootState) => state.setupKey.setupEditKeyVisible ); - const [expandRowsOnClick, setExpandRowsOnClick] = useState(true); const [groupPopupVisible, setGroupPopupVisible] = useState(""); - const [peerNameToIP, peerIPToName] = initPeerMaps(peers); - const optionsAllEnabled = [ - { label: "Enabled", value: "enabled" }, + const styleNotification = { marginTop: 85 }; + const showTutorial = !dataTable.length; + + const optionsValidAll = [ + { label: "Valid", value: "valid" }, { label: "All", value: "all" }, ]; - const itemsMenuAction = [ - { - key: "view", - label: ( - - ), - }, - { - key: "delete", - label: ( - - ), - }, - ]; - const actionsMenu = ; + const [confirmModal, confirmModalContextHolder] = Modal.useModal(); - const isShowTutorial = (routes: Route[]): boolean => { - return ( - !routes.length || (routes.length === 1 && routes[0].network === "Default") + const transformDataTable = (d: SetupKey[]): SetupKeyDataTable[] => { + return d.map( + (p) => + ({ + ...p, + groupsCount: p.auto_groups ? p.auto_groups.length : 0, + } as SetupKeyDataTable) ); }; - useEffect(() => { - return () => { - dispatch(routeActions.setSetupEditRoutePeerVisible(false)); - }; - }, []); - - useEffect(() => { - dispatch( - routeActions.getRoutes.request({ - getAccessTokenSilently: getTokenSilently, - payload: null, - }) - ); - }, [peers]); - useEffect(() => { dispatch( - peerActions.getPeers.request({ + setupKeyActions.getSetupKeys.request({ getAccessTokenSilently: getTokenSilently, payload: null, }) @@ -150,100 +106,98 @@ export const Routes = () => { ); }, []); - const filterGroupedDataTable = ( - routes: GroupedDataTable[] - ): GroupedDataTable[] => { - const t = textToSearch.toLowerCase().trim(); - let f: GroupedDataTable[] = filter( - routes, - (f) => - f.network_id.toLowerCase().includes(t) || - f.network.toLowerCase().includes(t) || - f.description.toLowerCase().includes(t) || - t === "" || - getGroupNamesFromIDs(f.routesGroups).find((u) => - u.toLowerCase().trim().includes(t) - ) - ) as GroupedDataTable[]; - if (optionAllEnable !== "all") { - f = filter(f, (f) => f.enabled); - } - - f.sort(function (a, b) { - if (a.network_id < b.network_id) { - return -1; - } - if (a.network_id > b.network_id) { - return 1; - } - return 0; - }); - - f.forEach((item) => { - item.groupedRoutes.sort(function (a, b) { - if (a.peer_name < b.peer_name) { - return -1; - } - if (a.peer_name > b.peer_name) { - return 1; - } - return 0; - }); - }); - - return f; - }; - useEffect(() => { - setGroupedDataTable( - filterGroupedDataTable(transformGroupedDataTable(routes, peers)) - ); - }, [dataTable]); + setDataTable(transformDataTable(filterDataTable())); + }, [setupKeys]); useEffect(() => { - if (failed) { - setShowTutorial(false); - } else { - setShowTutorial(isShowTutorial(routes)); - setDataTable(sortBy(transformDataTable(routes, peers), "network_id")); - } - }, [routes]); - - useEffect(() => { - setGroupedDataTable( - filterGroupedDataTable(transformGroupedDataTable(routes, peers)) - ); - }, [textToSearch, optionAllEnable]); + setDataTable(transformDataTable(filterDataTable())); + }, [textToSearch, optionValidAll]); const deleteKey = "deleting"; useEffect(() => { - const style = { marginTop: 85 }; - if (deletedRoute.loading) { + if (deletedSetupKey.loading) { message.loading({ content: "Deleting...", - duration: 0, key: deleteKey, - style, + style: styleNotification, }); - } else if (deletedRoute.success) { + } else if (deletedSetupKey.success) { message.success({ - content: "Route has been successfully deleted.", + content: "Setup key has been successfully removed.", key: deleteKey, duration: 2, - style, + style: styleNotification, }); - dispatch(routeActions.resetDeletedRoute(null)); - } else if (deletedRoute.error) { + dispatch( + setupKeyActions.setDeleteSetupKey({ + ...deletedSetupKey, + success: false, + }) + ); + dispatch(setupKeyActions.resetDeletedSetupKey(null)); + } else if (deletedSetupKey.error) { message.error({ content: - "Failed to delete route. You might not have enough permissions.", + "Failed to delete setup key. You might not have enough permissions.", key: deleteKey, duration: 2, - style, + style: styleNotification, }); - dispatch(routeActions.resetDeletedRoute(null)); + dispatch( + setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, error: null }) + ); + dispatch(setupKeyActions.resetDeletedSetupKey(null)); } - }, [deletedRoute]); + }, [deletedSetupKey]); + + const createKey = "saving"; + useEffect(() => { + if (savedSetupKey.loading) { + message.loading({ + content: "Saving...", + key: createKey, + duration: 1, + style: styleNotification, + }); + } else if (savedSetupKey.success) { + dispatch( + setupKeyActions.setSavedSetupKey({ ...savedSetupKey, success: false }) + ); + dispatch(setupKeyActions.resetSavedSetupKey(null)); + dispatch(setupKeyActions.setSetupEditKeyVisible(false)); + } else if (savedSetupKey.error) { + message.error({ + content: + "Failed to update setup key. You might not have enough permissions.", + key: createKey, + duration: 2, + style: styleNotification, + }); + dispatch( + setupKeyActions.setSavedSetupKey({ ...savedSetupKey, error: null }) + ); + dispatch(setupKeyActions.resetSavedSetupKey(null)); + } + }, [savedSetupKey]); + + const filterDataTable = (): SetupKey[] => { + const t = textToSearch.toLowerCase().trim(); + let f: SetupKey[] = [...setupKeys]; + if (optionValidAll === "valid") { + f = filter(setupKeys, (_f: SetupKey) => _f.valid && !_f.revoked); + } + f = filter( + f, + (_f: SetupKey) => + _f.name.toLowerCase().includes(t) || + _f.state.includes(t) || + _f.type.toLowerCase().includes(t) || + _f.key.toLowerCase().includes(t) || + t === "" + ) as SetupKey[]; + return f; + }; const onChangeTextToSearch = ( e: React.ChangeEvent @@ -252,103 +206,85 @@ export const Routes = () => { }; const searchDataTable = () => { - setGroupedDataTable( - filterGroupedDataTable(transformGroupedDataTable(routes, peers)) - ); + const data = filterDataTable(); + setDataTable(transformDataTable(data)); }; - const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => { - setOptionAllEnable(value); + const onChangeValidAll = ({ target: { value } }: RadioChangeEvent) => { + setOptionValidAll(value); }; - const showConfirmDelete = (selectedRoute: any) => { - setRouteToAction(selectedRoute as RouteDataTable); - let name = selectedRoute ? selectedRoute.network_id : ""; - confirm({ + const showConfirmRevoke = (setupKeyToAction: SetupKeyDataTable) => { + let name = setupKeyToAction ? setupKeyToAction.name : ""; + confirmModal.confirm({ icon: , - title: Delete network route {name}, - width: 600, + title: Revoke setupKey {name}, + width: 500, content: ( - - Are you sure you want to delete this route from your account? - + Are you sure you want to revoke key? ), - okType: "danger", onOk() { dispatch( - routeActions.deleteRoute.request({ + setupKeyActions.saveSetupKey.request({ getAccessTokenSilently: getTokenSilently, - payload: selectedRoute?.id || "", + payload: { + id: setupKeyToAction ? setupKeyToAction.id : null, + revoked: true, + name: setupKeyToAction ? setupKeyToAction.name : null, + auto_groups: + setupKeyToAction && setupKeyToAction.auto_groups + ? setupKeyToAction.auto_groups + : [], + } as SetupKeyToSave, }) ); }, - onCancel() { - setRouteToAction(null); - }, }); }; - const onClickAddNewRoute = () => { - dispatch(routeActions.setSetupNewRouteVisible(true)); + const onClickAddNewSetupKey = () => { + const autoGroups: string[] = []; + dispatch(setupKeyActions.setSetupNewKeyVisible(true)); dispatch( - routeActions.setRoute({ - network: "", - network_id: "", - description: "", - peer: "", - masquerade: true, - metric: 9999, - enabled: true, - groups: [], - } as Route) + setupKeyActions.setSetupKey({ + name: "", + type: "one-off", + auto_groups: autoGroups, + expires_in: 7, + } as SetupKey) ); }; - const onClickViewRoute = (selectedRoute: any) => { - setRouteToAction(selectedRoute as RouteDataTable); - dispatch(routeActions.setSetupNewRouteHA(false)); + const setKeyAndView = (key: SetupKeyDataTable) => { + dispatch(setupKeyActions.setSetupEditKeyVisible(true)); dispatch( - routeActions.setRoute({ - id: selectedRoute?.id || null, - network: selectedRoute?.network, - network_id: selectedRoute?.network_id, - description: selectedRoute?.description, - peer: peerToPeerIP(selectedRoute!.peer_name, selectedRoute!.peer_ip), - metric: selectedRoute?.metric, - masquerade: selectedRoute?.masquerade, - enabled: selectedRoute?.enabled, - groups: selectedRoute?.groups, - } as Route) + setupKeyActions.setSetupKey({ + id: key?.id || null, + key: key?.key, + name: key?.name, + revoked: key?.revoked, + expires: key?.expires, + state: key?.state, + type: key?.type, + used_times: key?.used_times, + valid: key?.valid, + auto_groups: key?.auto_groups, + last_used: key?.last_used, + usage_limit: key?.usage_limit, + } as SetupKey) ); - dispatch(routeActions.setSetupEditRoutePeerVisible(true)); }; - const setRouteAndView = (route: RouteDataTable, event: any) => { - event.preventDefault(); - event.stopPropagation(); - if (!route.id) { - dispatch(routeActions.setSetupNewRouteHA(true)); + useEffect(() => { + if (setupNewKeyVisible) { + setGroupPopupVisible(""); } - dispatch( - routeActions.setRoute({ - id: route.id || null, - network: route.network, - network_id: route.network_id, - description: route.description, - peer: route.peer ? peerToPeerIP(route.peer_name, route.peer_ip) : "", - metric: route.metric ? route.metric : 9999, - masquerade: route.masquerade, - enabled: route.enabled, - groups: route.groups, - } as Route) - ); - dispatch(routeActions.setSetupEditRouteVisible(true)); - }; + }, [setupNewKeyVisible]); const onPopoverVisibleChange = (b: boolean, key: string) => { - if (setupNewRouteVisible) { + if (setupNewKeyVisible) { setGroupPopupVisible(""); } else { if (b) { @@ -360,8 +296,9 @@ export const Routes = () => { }; const renderPopoverGroups = ( - rowGroups: string[] | null, - userToAction: RouteDataTable + label: string, + rowGroups: string[] | string[] | null, + setupKeyToAction: SetupKeyDataTable ) => { let groupsMap = new Map(); groups.forEach((g) => { @@ -378,13 +315,11 @@ export const Routes = () => { let btn = ( ); - if (!displayGroups || displayGroups!.length < 1) { return btn; } @@ -403,549 +338,315 @@ export const Routes = () => { ); }); - const updateContent = - displayGroups && displayGroups.length > 1 - ? content && content?.slice(1) - : content; - const mainContent = {updateContent}; + const mainContent = {content}; let popoverPlacement = "top"; if (content && content.length > 5) { popoverPlacement = "rightTop"; } return ( - <> - {displayGroups.length === 1 ? ( - <>{displayGroups[0].name} - ) : ( - - onPopoverVisibleChange(b, userToAction.key) - } - open={groupPopupVisible === userToAction.key} - content={mainContent} - title={null} - > - {displayGroups[0].name} {btn} - - )} - - ); - }; - - const callback = (key: any) => {}; - - const getAccordianHeader = (record: any) => { - return ( -
-

- {record.network_id} - -

-

{record.network}

-

- {record.routesCount > 1 ? ( - <> - on - - - ) : ( - <> - - off - - - - )} -

-

- -

-
- ); - }; - - const showConfirmationDeleteAllRoutes = (selectedGroup: any, event: any) => { - event.preventDefault(); - event.stopPropagation(); - let name = selectedGroup ? selectedGroup.network_id : ""; - confirm({ - icon: , - title: ( - - Delete routes to network {name} - - ), - width: 600, - content: ( - - - This operation will delete all routes to the network {name}. Are you sure? - - - ( - - - {item.peer_name} - - )} - bordered={false} - split={false} - itemLayout={"vertical"} - /> - - } - type="warning" - showIcon={false} - closable={false} - /> - - ), - okType: "danger", - onOk() { - dispatch( - routeActions.deleteRoute.request({ - getAccessTokenSilently: getTokenSilently, - payload: - selectedGroup.groupedRoutes.map((element: any) => { - return element?.id; - }) || "", - }) - ); - }, - onCancel() {}, - }); - }; - - const changeRouteStatus = (record: any, checked: boolean) => { - const updateReponse = { ...record, enabled: checked }; - dispatch( - routeActions.saveRoute.request({ - getAccessTokenSilently: getTokenSilently, - payload: updateReponse, - }) + + onPopoverVisibleChange(b, setupKeyToAction.key) + } + open={groupPopupVisible === setupKeyToAction.key} + content={mainContent} + title={null} + > + {btn} + ); }; return ( <> - {!setEditRoutePeerVisible ? ( - <> - - - - Network Routes - - {routes.length ? ( - - Network routes allow you to access other networks like LANs - and VPCs without installing NetBird on every resource. - - {" "} - Learn more - - - ) : ( - - Network routes allow you to access other networks like LANs - and VPCs without installing NetBird on every resource. - - {" "} - Learn more - - - )} - - - - - + {!setupEditKeyVisible && ( + + + Setup Keys + {setupKeys.length ? ( + + Setup keys are pre-authentication keys that allow to register + new machines in your network. + + {" "} + Learn more + + + ) : ( + + Setup key is a pre-authentication key that allows to register + new machines in your network + + {" "} + Learn more + + + )} + + + + {/**/} + + + + + +