From 1526a1b336b6c1b4db731f465285ae8b1a3f1dfd Mon Sep 17 00:00:00 2001 From: gillespi314 <73313222+gillespi314@users.noreply.github.com> Date: Thu, 17 Feb 2022 16:54:27 -0600 Subject: [PATCH] Enhance activity feed UI (#4177) * Refactor for useQuery * Add apply policy spec activity --- frontend/interfaces/activity.ts | 29 +-- .../cards/ActivityFeed/ActivityFeed.tsx | 223 ++++++++++-------- .../Homepage/cards/ActivityFeed/_styles.scss | 6 +- frontend/services/entities/activities.ts | 12 +- 4 files changed, 142 insertions(+), 128 deletions(-) diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index bef8dcf4a6..c3a0c96631 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -1,10 +1,13 @@ -import PropTypes from "prop-types"; +import { IPolicy } from "./policy"; import { IQuery } from "./query"; export enum ActivityType { CreatedPack = "created_pack", DeletedPack = "deleted_pack", EditedPack = "edited_pack", + CreatedPolicy = "created_policy", + DeletedPolicy = "deleted_policy", + EditedPolicy = "edited_policy", CreatedSavedQuery = "created_saved_query", DeletedSavedQuery = "deleted_saved_query", EditedSavedQuery = "edited_saved_query", @@ -12,6 +15,7 @@ export enum ActivityType { DeletedTeam = "deleted_team", LiveQuery = "live_query", AppliedSpecPack = "applied_spec_pack", + AppliedSpecPolicy = "applied_spec_policy", AppliedSpecSavedQuery = "applied_spec_saved_query", } export interface IActivity { @@ -27,29 +31,12 @@ export interface IActivity { export interface IActivityDetails { pack_id?: number; pack_name?: string; + policy_id?: number; + policy_name?: string; query_id?: number; query_name?: string; team_id?: number; team_name?: string; targets_count?: number; - specs?: IQuery[]; + specs?: IQuery[] | IPolicy[]; } - -export default PropTypes.shape({ - created_at: PropTypes.string, - id: PropTypes.number, - actor_full_name: PropTypes.string, - actor_id: PropTypes.number, - actor_gravatar: PropTypes.string, - actor_email: PropTypes.string, - type: PropTypes.string, - details: PropTypes.shape({ - pack_id: PropTypes.number, - pack_name: PropTypes.string, - query_id: PropTypes.number, - query_name: PropTypes.string, - team_id: PropTypes.number, - team_name: PropTypes.string, - targets_count: PropTypes.number, - }), -}); diff --git a/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx index d4b5461665..54f6e57e2e 100644 --- a/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/Homepage/cards/ActivityFeed/ActivityFeed.tsx @@ -1,10 +1,11 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; +import { useQuery } from "react-query"; import { find, isEmpty, lowerCase } from "lodash"; -import { formatDistanceToNow } from "date-fns"; +import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict"; -// @ts-ignore -// import Fleet from "fleet"; -import activitiesAPI from "services/entities/activities"; +import activitiesAPI, { + IActivitiesResponse, +} from "services/entities/activities"; import { addGravatarUrlToResource } from "fleet/helpers"; import { IActivity, ActivityType } from "interfaces/activity"; @@ -23,9 +24,15 @@ interface IActvityCardProps { setShowActivityFeedTitle: (showActivityFeedTitle: boolean) => void; } +interface IActivityDisplay extends IActivity { + key?: string; +} + const DEFAULT_GRAVATAR_URL = "https://www.gravatar.com/avatar/00000000000000000000000000000000?d=blank&size=200"; +const DEFAULT_PER_PAGE = 8; + const TAGGED_TEMPLATES = { liveQueryActivityTemplate: (activity: IActivity) => { const count = activity.details?.targets_count; @@ -36,6 +43,9 @@ const TAGGED_TEMPLATES = { editPackCtlActivityTemplate: () => { return "edited a pack using fleetctl"; }, + editPolicyCtlActivityTemplate: () => { + return "edited policies using fleetctl"; + }, editQueryCtlActivityTemplate: (activity: IActivity) => { const count = activity.details?.specs?.length; return typeof count === "undefined" || typeof count !== "number" @@ -62,88 +72,67 @@ const TAGGED_TEMPLATES = { 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 => { - try { - const { activities: responseActivities } = await activitiesAPI.loadNext( - pageIndex - ); - - if (responseActivities.length) { - setActivities(responseActivities); - } else { + const { + data: activities, + error: errorActivities, + isFetching: isFetchingActivities, + } = useQuery< + IActivitiesResponse, + Error, + IActivity[], + Array<{ + scope: string; + pageIndex: number; + perPage: number; + }> + >( + [{ scope: "activities", pageIndex, perPage: DEFAULT_PER_PAGE }], + ({ queryKey: [{ pageIndex: page, perPage }] }) => { + return activitiesAPI.loadNext(page, perPage); + }, + { + keepPreviousData: true, + staleTime: 5000, + select: (data) => data.activities, + onSuccess: (results) => { + setShowActivityFeedTitle(true); + if (results.length < DEFAULT_PER_PAGE) { setShowMore(false); } - setShowActivityFeedTitle(true); - setIsLoadingActivityFeed(false); - } catch (err) { - setIsLoadingError(true); - setIsLoadingActivityFeed(false); - } - }; - - getActivities(); - }, [pageIndex]); + }, + } + ); const onLoadPrevious = () => { - setIsLoadingActivityFeed(true); setShowMore(true); setPageIndex(pageIndex - 1); }; const onLoadNext = () => { - setIsLoadingActivityFeed(true); setPageIndex(pageIndex + 1); }; const getDetail = (activity: IActivity) => { - if (activity.type === ActivityType.LiveQuery) { - return TAGGED_TEMPLATES.liveQueryActivityTemplate(activity); + switch (activity.type) { + case ActivityType.LiveQuery: { + return TAGGED_TEMPLATES.liveQueryActivityTemplate(activity); + } + case ActivityType.AppliedSpecPack: { + return TAGGED_TEMPLATES.editPackCtlActivityTemplate(); + } + case ActivityType.AppliedSpecPolicy: { + return TAGGED_TEMPLATES.editPolicyCtlActivityTemplate(); + } + case ActivityType.AppliedSpecSavedQuery: { + return TAGGED_TEMPLATES.editQueryCtlActivityTemplate(activity); + } + default: { + return TAGGED_TEMPLATES.defaultActivityTemplate(activity); + } } - if (activity.type === ActivityType.AppliedSpecPack) { - return TAGGED_TEMPLATES.editPackCtlActivityTemplate(); - } - if (activity.type === ActivityType.AppliedSpecSavedQuery) { - return TAGGED_TEMPLATES.editQueryCtlActivityTemplate(activity); - } - return TAGGED_TEMPLATES.defaultActivityTemplate(activity); - }; - - const renderActivityBlock = (activity: IActivity, i: number) => { - const { actor_email } = activity; - const { gravatarURL } = actor_email - ? addGravatarUrlToResource({ email: actor_email }) - : { gravatarURL: DEFAULT_GRAVATAR_URL }; - - return ( -
- -
-

- {activity.actor_full_name} {getDetail(activity)}. -

- - {formatDistanceToNow(new Date(activity.created_at), { - addSuffix: true, - })} - -
-
- ); }; const renderError = () => { @@ -175,7 +164,7 @@ const ActivityFeed = ({ return (

- Fleet has not recorded any activities. + This is the start of your Fleet activities.

Did you recently edit your queries, update your packs, or run a live @@ -185,52 +174,80 @@ const ActivityFeed = ({ ); }; - const renderActivities = activities.map((activity: IActivity, i: number) => - renderActivityBlock(activity, i) - ); + const renderActivityBlock = (activity: IActivityDisplay) => { + const { actor_email, id, key } = activity; + const { gravatarURL } = actor_email + ? addGravatarUrlToResource({ email: actor_email }) + : { gravatarURL: DEFAULT_GRAVATAR_URL }; + + return ( +

+ +
+

+ {activity.actor_full_name} {getDetail(activity)}. +

+ + {formatDistanceToNowStrict(new Date(activity.created_at), { + addSuffix: true, + })} + +
+
+ ); + }; // Renders opaque information as activity feed is loading - const opacity = isLoadingActivityFeed ? { opacity: 0.4 } : { opacity: 1 }; + const opacity = isFetchingActivities ? { opacity: 0.4 } : { opacity: 1 }; return (
- {isLoadingError && renderError()} - {!isLoadingError && !isLoadingActivityFeed && isEmpty(activities) ? ( + {errorActivities && renderError()} + {!errorActivities && !isFetchingActivities && isEmpty(activities) ? ( renderNoActivities() ) : ( <> - {isLoadingActivityFeed && ( + {isFetchingActivities && (
)} -
{renderActivities}
+
+ {activities?.map((activity) => renderActivityBlock(activity))} +
)} - {!isLoadingError && !isEmpty(activities) && ( -
- - -
- )} + {!errorActivities && + (!isEmpty(activities) || (isEmpty(activities) && pageIndex > 0)) && ( +
+ + +
+ )}
); }; diff --git a/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss b/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss index b058d74642..4714c26246 100644 --- a/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss +++ b/frontend/pages/Homepage/cards/ActivityFeed/_styles.scss @@ -3,7 +3,7 @@ display: flex; flex-direction: column; position: relative; - min-height: 400px; + min-height: 592px; &__header-wrap { .form-field { @@ -166,6 +166,8 @@ display: flex; justify-content: center; align-items: center; - margin-top: 40px; // Aligns with software table spinner + .loading-spinner { + margin-top: 33px; + } } } diff --git a/frontend/services/entities/activities.ts b/frontend/services/entities/activities.ts index 9c3665424e..1bf9f48f95 100644 --- a/frontend/services/entities/activities.ts +++ b/frontend/services/entities/activities.ts @@ -1,13 +1,21 @@ -import sendRequest from "services"; import endpoints from "fleet/endpoints"; +import { IActivity } from "interfaces/activity"; +import sendRequest from "services"; const DEFAULT_PAGE = 0; const PER_PAGE = 8; const ORDER_KEY = "created_at"; const ORDER_DIRECTION = "desc"; +export interface IActivitiesResponse { + activities: IActivity[]; +} + export default { - loadNext: (page = DEFAULT_PAGE, perPage = PER_PAGE) => { + loadNext: ( + page = DEFAULT_PAGE, + perPage = PER_PAGE + ): Promise => { const { ACTIVITIES } = endpoints; const pagination = `page=${page}&per_page=${perPage}`; const sort = `order_key=${ORDER_KEY}&order_direction=${ORDER_DIRECTION}`;