Skip to content

Commit

Permalink
UI – Policy software install automations (#21792)
Browse files Browse the repository at this point in the history
## Front end for #19551

Feature branch merge to `main` – all work as been previously approved in
individual PRs to the feature branch.

- [x] Changes file added for user-visible changes in `changes/`
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
  • Loading branch information
jacobshandling and Jacob Shandling authored Sep 3, 2024
1 parent 271368c commit 09b6402
Show file tree
Hide file tree
Showing 40 changed files with 817 additions and 414 deletions.
1 change: 1 addition & 0 deletions changes/19551-policy-software-automations
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Implement features allowing automatic installation of software on hosts that fail policies.
4 changes: 4 additions & 0 deletions frontend/__mocks__/policyMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const DEFAULT_POLICY_MOCK: IPolicyStats = {
has_run: true,
next_update_ms: 3600000,
calendar_events_enabled: true,
install_software: {
name: "testSw0",
software_title_id: 1,
},
};

const createMockPolicy = (overrides?: Partial<IPolicyStats>): IPolicyStats => {
Expand Down
6 changes: 6 additions & 0 deletions frontend/components/Editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ interface IEditorProps {
* @default "editor"
*/
name?: string;
/** Include correct styles as a form field.
* @default false
*/
isFormField?: boolean;
maxLines?: number;
className?: string;
onChange?: (value: string, event?: any) => void;

Check warning on line 38 in frontend/components/Editor/Editor.tsx

View workflow job for this annotation

GitHub Actions / lint-js (ubuntu-latest)

Unexpected any. Specify a different type
Expand All @@ -52,11 +56,13 @@ const Editor = ({
readOnly = false,
wrapEnabled = false,
name = "editor",
isFormField = false,
maxLines = 20,
className,
onChange,
}: IEditorProps) => {
const classNames = classnames(baseClass, className, {
"form-field": isFormField,
[`${baseClass}__error`]: !!error,
});

Expand Down
1 change: 0 additions & 1 deletion frontend/components/Editor/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
&__label {
font-size: $x-small;
font-weight: $bold;
margin-bottom: $pad-small;

&--error {
color: $core-vibrant-red;
Expand Down
3 changes: 3 additions & 0 deletions frontend/components/FleetAce/FleetAce.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface IFleetAceProps {
label?: string;
name?: string;
value?: string;
placeholder?: string;
readOnly?: boolean;
maxLines?: number;
showGutter?: boolean;
Expand All @@ -55,6 +56,7 @@ const FleetAce = ({
labelActionComponent,
name = "query-editor",
value,
placeholder,
readOnly,
maxLines = 20,
showGutter = true,
Expand Down Expand Up @@ -266,6 +268,7 @@ const FleetAce = ({
showPrintMargin={false}
theme="fleet"
value={value}
placeholder={placeholder}
width="100%"
wrapEnabled={wrapEnabled}
style={style}
Expand Down
10 changes: 10 additions & 0 deletions frontend/components/FleetAce/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
}
}

.ace_content {
padding-left: 4px;
}

.ace_placeholder {
font-family: "SourceCodePro", $monospace;
margin: initial;
font-size: 15px;
}

&__help-text {
@include help-text;

Expand Down
15 changes: 14 additions & 1 deletion frontend/components/forms/fields/Dropdown/Dropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ class Dropdown extends Component {
onClose: PropTypes.func,
options: PropTypes.arrayOf(dropdownOptionInterface).isRequired,
placeholder: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
/**
value must correspond to the value of a dropdown option to render
e.g. with options:
[
{
label: "Display name",
value: 1, <– the id of the thing
}
]
set value to 1, not "Display name"
*/
value: PropTypes.oneOfType([
PropTypes.array,
PropTypes.string,
Expand Down Expand Up @@ -75,7 +88,7 @@ class Dropdown extends Component {
const { multi, onChange, clearable, name, parseTarget } = this.props;

if (parseTarget) {
// Returns both name and value
// Returns both name of the Dropdown and value of the selected option
return onChange({ value: selected.value, name });
}

Expand Down
8 changes: 8 additions & 0 deletions frontend/interfaces/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export interface IPolicy {
updated_at: string;
critical: boolean;
calendar_events_enabled: boolean;
install_software?: IPolicySoftwareToInstall;
}

export interface IPolicySoftwareToInstall {
name: string;
software_title_id: number;
}

// Used on the manage hosts page and other places where aggregate stats are displayed
Expand Down Expand Up @@ -94,6 +100,8 @@ export interface IPolicyFormData {
team_id?: number | null;
id?: number;
calendar_events_enabled?: boolean;
// undefined from GET/LIST when not set, null for PATCH to unset
software_title_id?: number | null;
}

export interface IPolicyNew {
Expand Down
1 change: 1 addition & 0 deletions frontend/interfaces/software.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export interface ISoftwareTitleDetails {
source: string; // "apps" | "ios_apps" | "ipados_apps" | ?
hosts_count: number;
versions: ISoftwareTitleVersion[] | null;
versions_updated_at?: string;
bundle_identifier?: string;
browser?: string;
versions_count?: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ interface IFileDetailsProps {

// TODO: if we reuse this one more time, we should consider moving this
// into FileUploader as a default preview. Currently we have this in
// AddSoftwareForm.tsx and here.
// AddPackageForm.tsx and here.
const FileDetails = ({ details: { name, platform } }: IFileDetailsProps) => (
<div className={`${baseClass}__selected-file`}>
<ProfileGraphic baseClass={baseClass} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const AdvancedOptionsModal = ({
helpText="Fleet will run this command on hosts to install software."
label="Install script"
labelTooltip="For security agents, add the script provided by the vendor."
isFormField
/>
{preInstallQuery && (
<div className={`${baseClass}__input-field`}>
Expand Down Expand Up @@ -72,6 +73,7 @@ const AdvancedOptionsModal = ({
maxLines={10}
value={postInstallScript}
helpText="Shell (macOS and Linux) or PowerShell (Windows)."
isFormField
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import React, { useCallback, useContext } from "react";
import softwareAPI from "services/entities/software";
import { NotificationContext } from "context/notification";

import { getErrorReason } from "interfaces/errors";

import Modal from "components/Modal";
import Button from "components/buttons/Button";

const baseClass = "delete-software-modal";

const DELETE_SW_USED_BY_POLICY_ERROR_MSG =
"Couldn't delete. Policy automation uses this software. Please disable policy automation for this software and try again.";
interface IDeleteSoftwareModalProps {
softwareId: number;
teamId: number;
Expand All @@ -28,8 +32,13 @@ const DeleteSoftwareModal = ({
await softwareAPI.deleteSoftwarePackage(softwareId, teamId);
renderFlash("success", "Software deleted successfully!");
onSuccess();
} catch {
renderFlash("error", "Couldn't delete. Please try again.");
} catch (error) {
const reason = getErrorReason(error);
if (reason.includes("Policy automation uses this software")) {
renderFlash("error", DELETE_SW_USED_BY_POLICY_ERROR_MSG);
} else {
renderFlash("error", "Couldn't delete. Please try again.");
}
}
onExit();
}, [softwareId, teamId, renderFlash, onSuccess, onExit]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import React, {
useLayoutEffect,
useState,
} from "react";
import FileSaver from "file-saver";
import { parse } from "content-disposition";

import PATHS from "router/paths";
import { AppContext } from "context/app";
Expand Down Expand Up @@ -45,10 +43,15 @@ function useTruncatedElement<T extends HTMLElement>(ref: React.RefObject<T>) {

useLayoutEffect(() => {
const element = ref.current;
if (element) {
const { scrollWidth, clientWidth } = element;
setIsTruncated(scrollWidth > clientWidth);
function updateIsTruncated() {
if (element) {
const { scrollWidth, clientWidth } = element;
setIsTruncated(scrollWidth > clientWidth);
}
}
window.addEventListener("resize", updateIsTruncated);
updateIsTruncated();
return () => window.removeEventListener("resize", updateIsTruncated);
}, [ref]);

return isTruncated;
Expand Down Expand Up @@ -92,20 +95,29 @@ const STATUS_DISPLAY_OPTIONS: Record<
iconName: "success",
tooltip: (
<>
Fleet installed software on these hosts. Currently, if the software is
uninstalled, the &quot;Installed&quot; status won&apos;t be updated.
Software is installed on these hosts (install script finished
<br />
with exit code 0). Currently, if the software is uninstalled, the
<br />
&quot;installed&quot; status won&apos;t be updated.
</>
),
},
pending: {
displayName: "Pending",
iconName: "pending-outline",
tooltip: "Fleet will install software when these hosts come online.",
tooltip: "Fleet is installing or will install when the host comes online.",
},
failed: {
displayName: "Failed",
iconName: "error",
tooltip: "Fleet failed to install software on these hosts.",
tooltip: (
<>
These hosts failed to install software. Click on a host to view
<br />
error(s).
</>
),
},
};

Expand All @@ -130,16 +142,18 @@ const PackageStatusCount = ({
})}`;
return (
<DataSet
className={`${baseClass}__status`}
title={
<TooltipWrapper
position="top"
tipContent={displayData.tooltip}
underline={false}
showArrow
tipOffset={10}
>
<div className={`${baseClass}__status-title`}>
<Icon name={displayData.iconName} />
<span>{displayData.displayName}</span>
<div>{displayData.displayName}</div>
</div>
</TooltipWrapper>
}
Expand Down Expand Up @@ -305,7 +319,7 @@ const SoftwarePackageCard = ({

return (
<Card borderRadiusSize="xxlarge" includeShadow className={baseClass}>
<div className={`${baseClass}__main-content`}>
<div className={`${baseClass}__row-1`}>
{/* TODO: main-info could be a seperate component as its reused on a couple
pages already. Come back and pull this into a component */}
<div className={`${baseClass}__main-info`}>
Expand All @@ -315,46 +329,46 @@ const SoftwarePackageCard = ({
<span className={`${baseClass}__details`}>{renderDetails()}</span>
</div>
</div>
<div className={`${baseClass}__package-statuses`}>
<PackageStatusCount
softwareId={softwareId}
status="installed"
count={status.installed}
teamId={teamId}
/>
<PackageStatusCount
softwareId={softwareId}
status="pending"
count={status.pending}
teamId={teamId}
/>
<PackageStatusCount
softwareId={softwareId}
status="failed"
count={status.failed}
teamId={teamId}
/>
<div className={`${baseClass}__actions-wrapper`}>
{isSelfService && (
<div className={`${baseClass}__self-service-badge`}>
<Icon
name="install-self-service"
size="small"
color="ui-fleet-black-75"
/>
Self-service
</div>
)}
{showActions && (
<ActionsDropdown
isSoftwarePackage={!!softwarePackage}
onDownloadClick={onDownloadClick}
onDeleteClick={onDeleteClick}
onAdvancedOptionsClick={onAdvancedOptionsClick}
/>
)}
</div>
</div>
<div className={`${baseClass}__actions-wrapper`}>
{isSelfService && (
<div className={`${baseClass}__self-service-badge`}>
<Icon
name="install-self-service"
size="small"
color="ui-fleet-black-75"
/>
Self-service
</div>
)}
{showActions && (
<ActionsDropdown
isSoftwarePackage={!!softwarePackage}
onDownloadClick={onDownloadClick}
onDeleteClick={onDeleteClick}
onAdvancedOptionsClick={onAdvancedOptionsClick}
/>
)}
<div className={`${baseClass}__package-statuses`}>
<PackageStatusCount
softwareId={softwareId}
status="installed"
count={status.installed}
teamId={teamId}
/>
<PackageStatusCount
softwareId={softwareId}
status="pending"
count={status.pending}
teamId={teamId}
/>
<PackageStatusCount
softwareId={softwareId}
status="failed"
count={status.failed}
teamId={teamId}
/>
</div>
{showAdvancedOptionsModal && (
<AdvancedOptionsModal
Expand Down
Loading

0 comments on commit 09b6402

Please sign in to comment.