From a85e47b28b992c6493a1a61ac52fecafb18e0b14 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 13 Jan 2022 12:12:54 -0500 Subject: [PATCH] Spiffier UI: Dashboard and Manage Host Page loading states (#3638) --- changes/issue-3051-update-loading-states | 1 + .../TableContainer/TableContainer.tsx | 8 +- .../HostSidePanel/HostSidePanel.tsx | 18 +- .../side_panels/HostSidePanel/_styles.scss | 4 + frontend/pages/Homepage/Homepage.tsx | 94 +++++++--- frontend/pages/Homepage/_styles.scss | 15 ++ .../cards/ActivityFeed/ActivityFeed.tsx | 49 +++-- .../Homepage/cards/ActivityFeed/_styles.scss | 21 ++- .../cards/HostsStatus/HostsStatus.tsx | 12 +- .../cards/HostsSummary/HostsSummary.tsx | 22 ++- .../Homepage/cards/Software/Software.tsx | 176 ++++++++++-------- .../Homepage/cards/Software/_styles.scss | 1 + .../Homepage/components/InfoCard/InfoCard.tsx | 21 ++- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 7 +- 14 files changed, 302 insertions(+), 147 deletions(-) create mode 100644 changes/issue-3051-update-loading-states diff --git a/changes/issue-3051-update-loading-states b/changes/issue-3051-update-loading-states new file mode 100644 index 0000000000..298f8b3d71 --- /dev/null +++ b/changes/issue-3051-update-loading-states @@ -0,0 +1 @@ +* Better loading states for dashboard and manage host page \ No newline at end of file diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index e22f10324a..b9f9f25c23 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -205,6 +205,8 @@ const TableContainer = ({ return data.length; }, [filteredCount, clientFilterCount, data]); + const opacity = isLoading ? { opacity: 0.4 } : { opacity: 1 }; + return (
{wideSearch && searchable && ( @@ -217,10 +219,12 @@ const TableContainer = ({ )}
{renderCount && ( -

{renderCount()}

+

+ {renderCount()} +

)} {!renderCount && data && displayCount() && !disableCount ? ( -

+

{TableContainerUtils.generateResultsCountText( resultsTitle, displayCount() diff --git a/frontend/components/side_panels/HostSidePanel/HostSidePanel.tsx b/frontend/components/side_panels/HostSidePanel/HostSidePanel.tsx index 69bdb4e6a3..4d528be07b 100644 --- a/frontend/components/side_panels/HostSidePanel/HostSidePanel.tsx +++ b/frontend/components/side_panels/HostSidePanel/HostSidePanel.tsx @@ -1,6 +1,10 @@ import React, { useState, useCallback } from "react"; import { filter } from "lodash"; +import { ILabel } from "interfaces/label"; +import { PLATFORM_LABEL_DISPLAY_ORDER } from "utilities/constants"; + +import Spinner from "components/Spinner"; import Button from "components/buttons/Button"; // @ts-ignore import InputField from "components/forms/fields/InputField"; @@ -8,21 +12,20 @@ import InputField from "components/forms/fields/InputField"; import PanelGroup from "components/side_panels/HostSidePanel/PanelGroup"; // @ts-ignore import SecondarySidePanelContainer from "components/side_panels/SecondarySidePanelContainer"; -import { ILabel } from "interfaces/label"; -import { PLATFORM_LABEL_DISPLAY_ORDER } from "utilities/constants"; import PlusIcon from "../../../../assets/images/icon-plus-16x16@2x.png"; const baseClass = "host-side-panel"; interface IHostSidePanelProps { - labels: ILabel[]; + labels?: ILabel[]; onAddLabelClick: (evt: React.MouseEvent) => void; onLabelClick: ( selectedLabel: ILabel ) => (evt: React.MouseEvent) => void; selectedFilter: string | undefined; canAddNewLabel: boolean; + isLabelsLoading: boolean; } const HostSidePanel = ({ @@ -31,6 +34,7 @@ const HostSidePanel = ({ onLabelClick, selectedFilter, canAddNewLabel, + isLabelsLoading, }: IHostSidePanelProps): JSX.Element => { const [labelFilter, setLabelFilter] = useState(""); @@ -41,6 +45,14 @@ const HostSidePanel = ({ [setLabelFilter] ); + if (isLabelsLoading || !labels) { + return ( + + + + ); + } + const allHostLabels = filter(labels, { type: "all" }); const hostPlatformLabels = (() => { diff --git a/frontend/components/side_panels/HostSidePanel/_styles.scss b/frontend/components/side_panels/HostSidePanel/_styles.scss index 182a3275fb..dfbfa8362e 100644 --- a/frontend/components/side_panels/HostSidePanel/_styles.scss +++ b/frontend/components/side_panels/HostSidePanel/_styles.scss @@ -1,6 +1,10 @@ .host-side-panel { color: $core-fleet-black; + .loading-spinner { + margin-top: 155px; // line up with host table loading spinner + } + .input-icon-field { margin-top: 0; diff --git a/frontend/pages/Homepage/Homepage.tsx b/frontend/pages/Homepage/Homepage.tsx index 3dce4d2725..489bf89c3f 100644 --- a/frontend/pages/Homepage/Homepage.tsx +++ b/frontend/pages/Homepage/Homepage.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useState } from "react"; +import React, { useContext, useState, useCallback } from "react"; import { useQuery } from "react-query"; import paths from "router/paths"; import { AppContext } from "context/app"; @@ -11,6 +11,7 @@ import { ITeam } from "interfaces/team"; import sortUtils from "utilities/sort"; import TeamsDropdown from "components/TeamsDropdown"; +import Spinner from "components/Spinner"; import InfoCard from "./components/InfoCard"; import HostsStatus from "./cards/HostsStatus"; import HostsSummary from "./cards/HostsSummary"; @@ -52,6 +53,14 @@ const Homepage = (): JSX.Element => { const [onlineCount, setOnlineCount] = useState(); const [offlineCount, setOfflineCount] = useState(); const [newCount, setNewCount] = useState(); + const [showActivityFeedTitle, setShowActivityFeedTitle] = useState( + false + ); + const [showSoftwareUI, setShowSoftwareUI] = useState(false); + const [showHostsUI, setShowHostsUI] = useState(false); // Hides UI on first load only + const [isLoadingHostsSummary, setIsLoadingHostsSummary] = useState( + true + ); // Opaque UI on subsequent loads const { data: teams } = useQuery( ["teams"], @@ -76,6 +85,7 @@ const Homepage = (): JSX.Element => { useQuery( ["host summary", currentTeam], () => { + setIsLoadingHostsSummary(true); return hostSummaryAPI.getSummary(currentTeam?.id); }, { @@ -93,6 +103,8 @@ const Homepage = (): JSX.Element => { (platform: IHostSummaryPlatforms) => platform.platform === "windows" ) || { platform: "windows", hosts_count: 0 }; setWindowsCount(windowsHosts.hosts_count.toLocaleString("en-US")); + setIsLoadingHostsSummary(false); + setShowHostsUI(true); }, } ); @@ -124,33 +136,50 @@ const Homepage = (): JSX.Element => {

-
- - - -
-
- - - +
+ <> + {isLoadingHostsSummary && ( +
+ +
+ )} +
+ + + +
+
+ + + +
+
{isPreviewMode && (
@@ -175,16 +204,21 @@ const Homepage = (): JSX.Element => { text: "View all software", onClick: () => setIsSoftwareModalOpen(true), }} + showTitle={showSoftwareUI} > {!isPreviewMode && !currentTeam && isOnGlobalTeam && ( - - + + )}
diff --git a/frontend/pages/Homepage/_styles.scss b/frontend/pages/Homepage/_styles.scss index 1ba4ff0859..fa3ebeeddc 100644 --- a/frontend/pages/Homepage/_styles.scss +++ b/frontend/pages/Homepage/_styles.scss @@ -67,4 +67,19 @@ } } } + + .host-sections { + position: relative; + } + + .spinner { + z-index: 11; + opacity: 1; + position: absolute; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } } diff --git a/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx index a302f4dd11..81734c1e17 100644 --- a/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx @@ -19,6 +19,10 @@ import OpenNewTabIcon from "../../../../../assets/images/open-new-tab-12x12@2x.p const baseClass = "activity-feed"; +interface IActvityCardProps { + setShowActivityFeedTitle: (showActivityFeedTitle: boolean) => void; +} + const DEFAULT_GRAVATAR_URL = "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=blank&size=200"; @@ -55,12 +59,16 @@ const TAGGED_TEMPLATES = { }, }; -const ActivityFeed = (): JSX.Element => { - const [activities, setActivities] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [isLoadingError, setIsLoadingError] = useState(false); - const [pageIndex, setPageIndex] = useState(0); - const [showMore, setShowMore] = useState(true); +const ActivityFeed = ({ + setShowActivityFeedTitle, +}: IActvityCardProps): JSX.Element => { + const [activities, setActivities] = useState([]); + const [isLoadingError, setIsLoadingError] = useState(false); + const [pageIndex, setPageIndex] = useState(0); + const [showMore, setShowMore] = useState(true); + const [isLoadingActivityFeed, setIsLoadingActivityFeed] = useState( + true + ); useEffect((): void => { const getActivities = async (): Promise => { @@ -74,11 +82,11 @@ const ActivityFeed = (): JSX.Element => { } else { setShowMore(false); } - - setIsLoading(false); + setShowActivityFeedTitle(true); + setIsLoadingActivityFeed(false); } catch (err) { setIsLoadingError(true); - setIsLoading(false); + setIsLoadingActivityFeed(false); } }; @@ -86,13 +94,13 @@ const ActivityFeed = (): JSX.Element => { }, [pageIndex]); const onLoadPrevious = () => { - setIsLoading(true); + setIsLoadingActivityFeed(true); setShowMore(true); setPageIndex(pageIndex - 1); }; const onLoadNext = () => { - setIsLoading(true); + setIsLoadingActivityFeed(true); setPageIndex(pageIndex + 1); }; @@ -179,19 +187,28 @@ const ActivityFeed = (): JSX.Element => { renderActivityBlock(activity, i) ); + // Renders opaque information as activity feed is loading + const opacity = isLoadingActivityFeed ? { opacity: 0.4 } : { opacity: 1 }; + return (
{isLoadingError && renderError()} - {!isLoadingError && !isLoading && isEmpty(activities) ? ( + {!isLoadingError && !isLoadingActivityFeed && isEmpty(activities) ? ( renderNoActivities() ) : ( -
{renderActivities}
+ <> + {isLoadingActivityFeed && ( +
+ +
+ )} +
{renderActivities}
+ )} - {isLoading && } {!isLoadingError && !isEmpty(activities) && (