From 375526452928cf1eda5b421cbc0b39418837a1a1 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Tue, 29 Aug 2023 11:47:37 +0100 Subject: [PATCH] Feat UI view script activity and script details (#13388) relates to #13308 Implements the UI for the activity item for script ran, and the script details modal. NOTE: Still have to do API integration and will do when API is ready in another PR. - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - [ ] Added/updated tests - [x] Manual QA for all new/changed functionality --- ...plement-script-run-activity-and-details-ui | 1 + frontend/components/DataError/DataError.tsx | 9 +- frontend/components/Textarea/Textarea.tsx | 17 ++ frontend/components/Textarea/_styles.scss | 6 + frontend/components/Textarea/index.ts | 1 + .../TooltipWrapper/TooltipWrapper.tsx | 8 +- frontend/interfaces/activity.ts | 1 + .../cards/ActivityFeed/ActivityFeed.tsx | 6 + .../ActivityItem/ActivityItem.tsx | 22 ++ .../ScriptDetailsModal/ScriptDetailsModal.tsx | 193 ++++++++++++++++++ .../ScriptDetailsModal/_styles.scss | 55 +++++ .../components/ScriptDetailsModal/index.ts | 1 + frontend/styles/var/_global.scss | 1 + 13 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 changes/issue-13308-implement-script-run-activity-and-details-ui create mode 100644 frontend/components/Textarea/Textarea.tsx create mode 100644 frontend/components/Textarea/_styles.scss create mode 100644 frontend/components/Textarea/index.ts create mode 100644 frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/ScriptDetailsModal.tsx create mode 100644 frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/_styles.scss create mode 100644 frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/index.ts diff --git a/changes/issue-13308-implement-script-run-activity-and-details-ui b/changes/issue-13308-implement-script-run-activity-and-details-ui new file mode 100644 index 0000000000..0bffcd960c --- /dev/null +++ b/changes/issue-13308-implement-script-run-activity-and-details-ui @@ -0,0 +1 @@ +- add UI for script run activity and script details modal diff --git a/frontend/components/DataError/DataError.tsx b/frontend/components/DataError/DataError.tsx index 8bea008893..0794d6ee04 100644 --- a/frontend/components/DataError/DataError.tsx +++ b/frontend/components/DataError/DataError.tsx @@ -7,12 +7,17 @@ import Icon from "components/Icon"; const baseClass = "data-error"; interface IDataErrorProps { + /** the description text displayed under the header */ + description?: string; children?: React.ReactNode; card?: boolean; className?: string; } +const DEFAULT_DESCRIPTION = "Refresh the page or log in again."; + const DataError = ({ + description = DEFAULT_DESCRIPTION, children, card, className, @@ -31,9 +36,7 @@ const DataError = ({ <> {children || ( <> - - Refresh the page or log in again. - + {description} If this keeps happening, please  { + const classNames = classnames(baseClass, className); + return
{children}
; +}; + +export default Textarea; diff --git a/frontend/components/Textarea/_styles.scss b/frontend/components/Textarea/_styles.scss new file mode 100644 index 0000000000..6ca3487d1e --- /dev/null +++ b/frontend/components/Textarea/_styles.scss @@ -0,0 +1,6 @@ +.textarea { + padding: 10px; + border-radius: $border-radius; + border: 1px solid $ui-fleet-black-10; + background-color: $ui-off-white; +} diff --git a/frontend/components/Textarea/index.ts b/frontend/components/Textarea/index.ts new file mode 100644 index 0000000000..3fb65ab35a --- /dev/null +++ b/frontend/components/Textarea/index.ts @@ -0,0 +1 @@ +export { default } from "./Textarea"; diff --git a/frontend/components/TooltipWrapper/TooltipWrapper.tsx b/frontend/components/TooltipWrapper/TooltipWrapper.tsx index aec640cf2c..b49e88a471 100644 --- a/frontend/components/TooltipWrapper/TooltipWrapper.tsx +++ b/frontend/components/TooltipWrapper/TooltipWrapper.tsx @@ -9,6 +9,7 @@ interface ITooltipWrapperProps { position?: "top" | "bottom"; isDelayed?: boolean; className?: string; + tooltipClass?: string; } const baseClass = "component__tooltip-wrapper"; @@ -19,11 +20,12 @@ const TooltipWrapper = ({ position = "bottom", isDelayed, className, + tooltipClass, }: ITooltipWrapperProps): JSX.Element => { const classname = classnames(baseClass, className); - const tipClass = isDelayed - ? `${baseClass}__tip-text delayed-tip` - : `${baseClass}__tip-text`; + const tipClass = classnames(`${baseClass}__tip-text`, tooltipClass, { + "delayed-tip": isDelayed, + }); const sanitizedTipContent = DOMPurify.sanitize(tipContent); diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 92e6afdbdd..ed6f566b64 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -48,6 +48,7 @@ export enum ActivityType { TransferredHosts = "transferred_hosts", EnabledWindowsMdm = "enabled_windows_mdm", DisabledWindowsMdm = "disabled_windows_mdm", + RanScript = "ran_script", } export interface IActivity { created_at: string; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 79526130ad..889de6a3dc 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -15,6 +15,7 @@ import Spinner from "components/Spinner"; // @ts-ignore import FleetIcon from "components/icons/FleetIcon"; import ActivityItem from "./ActivityItem"; +import ScriptDetailsModal from "./components/ScriptDetailsModal/ScriptDetailsModal"; const baseClass = "activity-feed"; interface IActvityCardProps { @@ -32,6 +33,7 @@ const ActivityFeed = ({ }: IActvityCardProps): JSX.Element => { const [pageIndex, setPageIndex] = useState(0); const [showShowQueryModal, setShowShowQueryModal] = useState(false); + const [showScriptDetailsModal, setShowScriptDetailsModal] = useState(false); const queryShown = useRef(""); const { @@ -156,6 +158,10 @@ const ActivityFeed = ({ onCancel={() => setShowShowQueryModal(false)} /> )} + {/* TODO: show/hide when we integrate with API activites. */} + {false && ( + setShowShowQueryModal(false)} /> + )} ); }; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index 418a71fa38..123ef6ffcd 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -488,6 +488,25 @@ const TAGGED_TEMPLATES = { disabledWindowsMdm: (activity: IActivity) => { return <> told Fleet to turn off Windows MDM features.; }, + ranScript: ( + activity: IActivity, + onDetailsClick?: (details: IActivityDetails) => void + ) => { + return ( + <> + {" "} + ran a script on {activity.details?.host_display_name}.{" "} + + + ); + }, }; const getDetail = ( @@ -598,6 +617,9 @@ const getDetail = ( case ActivityType.DisabledWindowsMdm: { return TAGGED_TEMPLATES.disabledWindowsMdm(activity); } + case ActivityType.RanScript: { + return TAGGED_TEMPLATES.ranScript(activity, onDetailsClick); + } default: { return TAGGED_TEMPLATES.defaultActivityTemplate(activity); } diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/ScriptDetailsModal.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/ScriptDetailsModal.tsx new file mode 100644 index 0000000000..621754485b --- /dev/null +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/ScriptDetailsModal.tsx @@ -0,0 +1,193 @@ +import React from "react"; +import { useQuery } from "react-query"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; +import TooltipWrapper from "components/TooltipWrapper"; +import Icon from "components/Icon"; +import Textarea from "components/Textarea"; +import DataError from "components/DataError/DataError"; +import Spinner from "components/Spinner/Spinner"; + +const baseClass = "script-details-modal"; + +const SCRIPT_RUNNING_CODE = -999; +const HOST_NOT_REACHED_CODE = -998; + +interface IScriptContentProps { + content: string; +} + +const ScriptContent = ({ content }: IScriptContentProps) => { + return ( +
+ Script content: + +
+ ); +}; + +interface IStatusMessageProps { + exitCode: number; + message: string; + runtime: number; +} + +const StatusMessage = ({ exitCode, message, runtime }: IStatusMessageProps) => { + let statusMessage: JSX.Element; + + // script timed out error + if (runtime > 30) { + statusMessage = ( +

+ + Timeout error: Fleet stopped the script after 30 seconds to protect host + performance. +

+ ); + // host could not be reached + } else if (exitCode === HOST_NOT_REACHED_CODE) { + statusMessage = ( +

+ + The script ran but Fleet couldn't get its output because Fleet + didn't hear back from the host. +

+ ); + // script still running + } else if (exitCode === SCRIPT_RUNNING_CODE) { + statusMessage = ( +

+ + Script is running. To see if the script finished, close this modal and + open it again. +

+ ); + } else { + // 0 or 1 exit code with message + statusMessage = ( +

+ + {`Exit code: ${exitCode} (${message})`} +

+ ); + } + + return
{statusMessage}
; +}; + +interface IScriptOutputProps { + output: string; +} + +const ScriptOutput = ({ output }: IScriptOutputProps) => { + return ( +
+

+ The{" "} + + output recorded + {" "} + when Marko's MacBook Pro ran the script above: +

+ +
+ ); +}; + +interface IScriptResultProps { + exitCode: number; + message: string; + output: string; + runtime: number; +} + +const ScriptResult = ({ + exitCode, + message, + output, + runtime, +}: IScriptResultProps) => { + const showOutputText = exitCode !== -998 && exitCode !== -999 && runtime < 30; + + return ( +
+ + {showOutputText && } +
+ ); +}; + +interface IScriptDetailsModalProps { + onCancel: () => void; +} + +const ScriptDetailsModal = ({ onCancel }: IScriptDetailsModalProps) => { + const TEST_DATA = { + script_contents: "test contentsss", + exit_code: 0, + output: "test output", + message: "test message", + runtime: 20, + }; + + const { data, isLoading, isError } = useQuery( + ["scriptDetailsModal"], + () => { + return new Promise((resolve) => resolve(TEST_DATA)); + }, + { refetchOnWindowFocus: false } + ); + + const renderContent = () => { + let content: JSX.Element; + + if (isLoading) { + content = ; + } else if (isError) { + content = ; + } else { + content = ( + <> + + + + ); + } + + return ( + <> +
{content}
+
+ +
+ + ); + }; + + return ( + + {renderContent()} + + ); +}; + +export default ScriptDetailsModal; diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/_styles.scss b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/_styles.scss new file mode 100644 index 0000000000..1158ba1f96 --- /dev/null +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/_styles.scss @@ -0,0 +1,55 @@ +.script-details-modal { + box-sizing: border-box; + width: 811px; + + &__modal-content { + display: flex; + flex-direction: column; + gap: $pad-xlarge; + } + + &__script-content { + display: flex; + flex-direction: column; + gap: $pad-medium; + } + + &__script-content-textarea { + box-sizing: border-box; + min-height: 300px; + font-family: "SourceCodePro"; + } + + &__status-message { + p { + display: flex; + align-items: center; + gap: $pad-small; + margin: 0; + } + } + + &__script-result { + display: flex; + flex-direction: column; + gap: $pad-xlarge; + } + + &__script-output { + display: flex; + flex-direction: column; + gap: $pad-medium; + + p { + margin: 0; + } + } + + &__output-tooltip { + width: 300px; + } + + &__output-textarea { + font-family: "SourceCodePro"; + } +} diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/index.ts b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/index.ts new file mode 100644 index 0000000000..70b0475d96 --- /dev/null +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/components/ScriptDetailsModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ScriptDetailsModal"; diff --git a/frontend/styles/var/_global.scss b/frontend/styles/var/_global.scss index 9dcced88e2..0c772c21e8 100644 --- a/frontend/styles/var/_global.scss +++ b/frontend/styles/var/_global.scss @@ -1 +1,2 @@ $border-radius: 4px; +$border-radius-large: 6px;