Skip to content

Commit

Permalink
Add UI for VPP activities (#20493)
Browse files Browse the repository at this point in the history
  • Loading branch information
gillespi314 authored Jul 16, 2024
1 parent 5ea213e commit 60ced95
Show file tree
Hide file tree
Showing 19 changed files with 419 additions and 60 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React from "react";
import { useQuery } from "react-query";

import {
SoftwareInstallStatus,
getInstallStatusPredicate,
} from "interfaces/software";
import mdmApi from "services/entities/mdm";

import Modal from "components/Modal";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import Textarea from "components/Textarea";
import DataError from "components/DataError/DataError";
import Spinner from "components/Spinner/Spinner";
import { IMdmCommandResult } from "interfaces/mdm";
import { IActivityDetails } from "interfaces/activity";

import { IconNames } from "components/icons";
import {
getInstallDetailsStatusPredicate,
INSTALL_DETAILS_STATUS_ICONS,
} from "../constants";

const baseClass = "app-install-details";

export type IAppInstallDetails = Pick<
IActivityDetails,
| "host_id"
| "command_uuid"
| "host_display_name"
| "software_title"
| "app_store_id"
| "status"
>;

export const AppInstallDetails = ({
status,
command_uuid = "",
host_display_name = "",
software_title = "",
}: IAppInstallDetails) => {
const { data: result, isLoading, isError } = useQuery<
IMdmCommandResult,
Error
>(
["mdm_command_results", command_uuid],
async () => {
return mdmApi.getCommandResults(command_uuid).then((response) => {
const results = response.results?.[0];
if (!results) {
return Promise.reject(new Error("No data returned"));
}
return {
...results,
payload: atob(results.payload),
result: atob(results.result),
};
});
},
{
refetchOnWindowFocus: false,
staleTime: 3000,
}
);

if (isLoading) {
return <Spinner />;
} else if (isError) {
return <DataError description="Close this modal and try again." />;
} else if (!result) {
// FIXME: Find a better solution for this.
return <DataError description="No data returned." />;
}

// Note: We need to reconcile status values from two different sources. From props, we
// get the status from the activity item details (which can be "failed", "pending", or
// "installed"). From the command results API response, we also receive the raw status
// from the MDM protocol, e.g., "NotNow" or "Acknowledged". We need to display some special
// messaging for the "NotNow" status, which otherwise would be treated as "pending".
const isStatusNotNow = result.status === "NotNow";
let iconName: IconNames;
let predicate: string;
let subordinate: string;
if (isStatusNotNow) {
iconName = INSTALL_DETAILS_STATUS_ICONS.pending;
predicate = "tried to install";
subordinate =
" but couldn’t because the host was locked or was running on battery power while in Power Nap. Fleet will try again";
} else {
iconName = INSTALL_DETAILS_STATUS_ICONS[status as SoftwareInstallStatus];
predicate = getInstallDetailsStatusPredicate(status);
subordinate = status === "pending" ? " when it comes online" : "";
}

const showCommandResponse = isStatusNotNow || status !== "pending";

return (
<>
<div className={`${baseClass}__software-install-details`}>
<div className={`${baseClass}__status-message`}>
{!!iconName && <Icon name={iconName} />}
<span>
Fleet {predicate} <b>{software_title}</b> on{" "}
<b>{host_display_name}</b>
{subordinate}.
</span>
</div>
<div className={`${baseClass}__script-output`}>
Request payload:
<Textarea className={`${baseClass}__output-textarea`}>
{result.payload}
</Textarea>
</div>
{showCommandResponse && (
<div className={`${baseClass}__script-output`}>
The response from <b>{host_display_name}</b>:
<Textarea className={`${baseClass}__output-textarea`}>
{result.result}
</Textarea>
</div>
)}
</div>
</>
);
};

export const AppInstallDetailsModal = ({
details,
onCancel,
}: {
details: IAppInstallDetails;
onCancel: () => void;
}) => {
return (
<Modal
title="Install details"
onExit={onCancel}
onEnter={onCancel}
className={baseClass}
>
<>
<div className={`${baseClass}__modal-content`}>
<AppInstallDetails {...details} />
</div>
<div className="modal-cta-wrap">
<Button onClick={onCancel} variant="brand">
Done
</Button>
</div>
</>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.app-install-details {
.modal__content {
margin-top: $pad-xlarge;
}
&__status-message {
display: flex;
align-items: center;
gap: $pad-small;
margin: 0;
.icon {
padding-top: 3px;
align-self: flex-start;
}
}
&__script-output {
padding-top: $pad-xlarge;
.textarea {
margin-top: $pad-medium;
overflow-wrap: break-word;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AppInstallDetails, AppInstallDetailsModal } from "./AppInstallDetails";
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useQuery } from "react-query";
import {
ISoftwareInstallResult,
ISoftwareInstallResults,
SoftwareInstallStatus,
} from "interfaces/software";
import softwareAPI from "services/entities/software";

Expand All @@ -14,55 +13,41 @@ import Icon from "components/Icon";
import Textarea from "components/Textarea";
import DataError from "components/DataError/DataError";
import Spinner from "components/Spinner/Spinner";
import { IconNames } from "components/icons";
import {
INSTALL_DETAILS_STATUS_ICONS,
SOFTWARE_INSTALL_OUTPUT_DISPLAY_LABELS,
getInstallDetailsStatusPredicate,
} from "../constants";

const baseClass = "software-install-details";

const STATUS_ICONS: Record<SoftwareInstallStatus, IconNames> = {
pending: "pending-outline",
installed: "success-outline",
failed: "error-outline",
} as const;

const STATUS_PREDICATES: Record<SoftwareInstallStatus, string> = {
pending: "will install",
installed: "installed",
failed: "failed to install",
} as const;

const StatusMessage = ({
result: { host_display_name, software_package, software_title, status },
}: {
result: ISoftwareInstallResult;
}) => {
return (
<div className={`${baseClass}__status-message`}>
<Icon name={STATUS_ICONS[status]} />
<Icon name={INSTALL_DETAILS_STATUS_ICONS[status]} />
<span>
Fleet {STATUS_PREDICATES[status]} <b>{software_title}</b> (
{software_package}) on <b>{host_display_name}</b>
Fleet {getInstallDetailsStatusPredicate(status)} <b>{software_title}</b>{" "}
({software_package}) on <b>{host_display_name}</b>
{status === "pending" ? " when it comes online" : ""}.
</span>
</div>
);
};

const OUTPUT_DISPLAY_LABELS = {
pre_install_query_output: "Pre-install condition",
output: "Software install output",
post_install_script_output: "Post-install script output",
} as const;

const Output = ({
displayKey,
result,
}: {
displayKey: keyof typeof OUTPUT_DISPLAY_LABELS;
displayKey: keyof typeof SOFTWARE_INSTALL_OUTPUT_DISPLAY_LABELS;
result: ISoftwareInstallResult;
}) => {
return (
<div className={`${baseClass}__script-output`}>
{OUTPUT_DISPLAY_LABELS[displayKey]}:
{SOFTWARE_INSTALL_OUTPUT_DISPLAY_LABELS[displayKey]}:
<Textarea className={`${baseClass}__output-textarea`}>
{result[displayKey]}
</Textarea>
Expand Down
39 changes: 39 additions & 0 deletions frontend/components/ActivityDetails/InstallDetails/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IconNames } from "components/icons";
import { SoftwareInstallStatus } from "interfaces/software";

export const INSTALL_DETAILS_STATUS_ICONS: Record<
SoftwareInstallStatus,
IconNames
> = {
pending: "pending-outline",
installed: "success-outline",
failed: "error-outline",
} as const;

const INSTALL_DETAILS_STATUS_PREDICATES: Record<
SoftwareInstallStatus,
string
> = {
pending: "will install",
installed: "installed",
failed: "failed to install",
} as const;

export const getInstallDetailsStatusPredicate = (
status: string | undefined
) => {
if (!status) {
return INSTALL_DETAILS_STATUS_PREDICATES.pending;
}
return (
INSTALL_DETAILS_STATUS_PREDICATES[
status.toLowerCase() as SoftwareInstallStatus
] || INSTALL_DETAILS_STATUS_PREDICATES.pending
);
};

export const SOFTWARE_INSTALL_OUTPUT_DISPLAY_LABELS = {
pre_install_query_output: "Pre-install condition",
output: "Software install output",
post_install_script_output: "Post-install script output",
} as const;
13 changes: 11 additions & 2 deletions frontend/interfaces/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,26 @@ export enum ActivityType {
AddedSoftware = "added_software",
DeletedSoftware = "deleted_software",
InstalledSoftware = "installed_software",
EnabledVpp = "enabled_vpp",
DisabledVpp = "disabled_vpp",
AddedAppStoreApp = "added_app_store_app",
DeletedAppStoreApp = "deleted_app_store_app",
InstalledAppStoreApp = "installed_app_store_app",
}

// This is a subset of ActivityType that are shown only for the host past activities
export type IHostPastActivityType =
| ActivityType.RanScript
| ActivityType.LockedHost
| ActivityType.UnlockedHost
| ActivityType.InstalledSoftware;
| ActivityType.InstalledSoftware
| ActivityType.InstalledAppStoreApp;

// This is a subset of ActivityType that are shown only for the host upcoming activities
export type IHostUpcomingActivityType =
| ActivityType.RanScript
| ActivityType.InstalledSoftware;
| ActivityType.InstalledSoftware
| ActivityType.InstalledAppStoreApp;

export interface IActivity {
created_at: string;
Expand Down Expand Up @@ -153,4 +160,6 @@ export interface IActivityDetails {
status?: string;
install_uuid?: string;
self_service?: boolean;
command_uuid?: string;
app_store_id?: number;
}
19 changes: 19 additions & 0 deletions frontend/interfaces/mdm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,22 @@ export enum BootstrapPackageStatus {
PENDING = "pending",
FAILED = "failed",
}

/**
* IMdmCommandResult is the shape of an mdm command result object
* returned by the Fleet API.
*/
export interface IMdmCommandResult {
host_uuid: string;
command_uuid: string;
/** Status is the status of the command. It can be one of Acknowledged, Error, or NotNow for
// Apple, or 200, 400, etc for Windows. */
status: string;
updated_at: string;
request_type: string;
hostname: string;
/** Payload is a base64-encoded string containing the MDM command request */
payload: string;
/** Result is a base64-enconded string containing the MDM command response */
result: string;
}
24 changes: 24 additions & 0 deletions frontend/interfaces/software.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { startCase } from "lodash";
import PropTypes from "prop-types";

import { IconNames } from "components/icons";

import vulnerabilityInterface from "./vulnerability";

export default PropTypes.shape({
Expand Down Expand Up @@ -241,3 +244,24 @@ export type IDeviceSoftware = Omit<
version: string;
};
};
const INSTALL_STATUS_PREDICATES: Record<SoftwareInstallStatus, string> = {
failed: "failed to install",
installed: "installed",
pending: "told Fleet to install",
} as const;

export const getInstallStatusPredicate = (status: string | undefined) => {
if (!status) {
return INSTALL_STATUS_PREDICATES.pending;
}
return (
INSTALL_STATUS_PREDICATES[status.toLowerCase() as SoftwareInstallStatus] ||
INSTALL_STATUS_PREDICATES.pending
);
};

export const INSTALL_STATUS_ICONS: Record<SoftwareInstallStatus, IconNames> = {
pending: "pending-outline",
installed: "success-outline",
failed: "error-outline",
} as const;
Loading

0 comments on commit 60ced95

Please sign in to comment.