+
{title}
{value}
diff --git a/frontend/components/Editor/Editor.tsx b/frontend/components/Editor/Editor.tsx
new file mode 100644
index 0000000000..c9bd360508
--- /dev/null
+++ b/frontend/components/Editor/Editor.tsx
@@ -0,0 +1,120 @@
+import classnames from "classnames";
+import TooltipWrapper from "components/TooltipWrapper";
+import React, { ReactNode } from "react";
+import AceEditor from "react-ace";
+
+const baseClass = "editor";
+
+interface IEditorProps {
+ focus?: boolean;
+ label?: string;
+ labelTooltip?: string | JSX.Element;
+ error?: string | null;
+ readOnly?: boolean;
+ /**
+ * Help text to display below the editor.
+ */
+ helpText?: ReactNode;
+ /** Sets the value of the input. Use this if you'd like the editor
+ * to be a controlled component */
+ value?: string;
+ /** Sets the default value of the input. Use this if you'd like the editor
+ * to be an uncontrolled component */
+ defaultValue?: string;
+ /** Enabled wrapping lines.
+ * @default false
+ */
+ wrapEnabled?: boolean;
+ /** A unique name for the editor.
+ * @default "editor"
+ */
+ name?: string;
+ maxLines?: number;
+ className?: string;
+ onChange?: (value: string, event?: any) => void;
+}
+
+/**
+ * This component is a generic editor that uses the AceEditor component.
+ * TODO: We should move FleetAce and YamlAce into here and deprecate importing
+ * them directly. This component should be used for all editor components and
+ * be configurable from the props. We should look into dynmaic imports for
+ * this.
+ */
+const Editor = ({
+ helpText,
+ label,
+ labelTooltip,
+ error,
+ focus,
+ value,
+ defaultValue,
+ readOnly = false,
+ wrapEnabled = false,
+ name = "editor",
+ maxLines = 20,
+ className,
+ onChange,
+}: IEditorProps) => {
+ const classNames = classnames(baseClass, className, {
+ [`${baseClass}__error`]: !!error,
+ });
+
+ const renderLabel = () => {
+ const labelText = error || label;
+ const labelClassName = classnames(`${baseClass}__label`, {
+ [`${baseClass}__label--error`]: !!error,
+ });
+
+ if (!labelText) {
+ return null;
+ }
+
+ if (labelTooltip) {
+ return (
+
+ {labelText}
+
+ );
+ }
+
+ return
{labelText}
;
+ };
+
+ const renderHelpText = () => {
+ if (helpText) {
+ return
{helpText}
;
+ }
+ return null;
+ };
+
+ return (
+
+ {renderLabel()}
+
+ {renderHelpText()}
+
+ );
+};
+
+export default Editor;
diff --git a/frontend/components/Editor/_styles.scss b/frontend/components/Editor/_styles.scss
new file mode 100644
index 0000000000..676172697d
--- /dev/null
+++ b/frontend/components/Editor/_styles.scss
@@ -0,0 +1,22 @@
+.editor {
+
+ &__label {
+ font-size: $x-small;
+ font-weight: $bold;
+ margin-bottom: $pad-small;
+
+ &--error {
+ color: $core-vibrant-red;
+ }
+ }
+
+ &__help-text {
+ @include help-text;
+ }
+
+ &__error {
+ .ace-fleet {
+ border: 1px solid $core-vibrant-red;
+ }
+ }
+}
diff --git a/frontend/components/Editor/index.ts b/frontend/components/Editor/index.ts
new file mode 100644
index 0000000000..100d029231
--- /dev/null
+++ b/frontend/components/Editor/index.ts
@@ -0,0 +1 @@
+export { default } from "./Editor";
diff --git a/frontend/components/FileUploader/FileUploader.tsx b/frontend/components/FileUploader/FileUploader.tsx
index 52ab307656..759f9c9501 100644
--- a/frontend/components/FileUploader/FileUploader.tsx
+++ b/frontend/components/FileUploader/FileUploader.tsx
@@ -1,10 +1,11 @@
-import React from "react";
+import React, { ReactNode, useState } from "react";
import classnames from "classnames";
import Button from "components/buttons/Button";
import Card from "components/Card";
import { GraphicNames } from "components/graphics";
import Graphic from "components/Graphic";
+import Icon from "components/Icon";
const baseClass = "file-uploader";
@@ -32,9 +33,20 @@ interface IFileUploaderProps {
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
*/
accept?: string;
- /** The text to display on the upload button */
+ /** The text to display on the upload button
+ * @default "Upload"
+ */
buttonMessage?: string;
className?: string;
+ /** renders the button to open the file uploader to appear as a button or
+ * a link.
+ * @default "button"
+ */
+ buttonType?: "button" | "link";
+ /** If provided FileUploader will display this component when the file is
+ * selected. This is used for previewing the file before uploading.
+ */
+ filePreview?: ReactNode;
onFileUpload: (files: FileList | null) => void;
}
@@ -47,11 +59,26 @@ const FileUploader = ({
additionalInfo,
isLoading = false,
accept,
- buttonMessage = "Upload",
+ filePreview,
className,
+ buttonMessage = "Upload",
+ buttonType = "button",
onFileUpload,
}: IFileUploaderProps) => {
- const classes = classnames(baseClass, className);
+ const [isFileSelected, setIsFileSelected] = useState(false);
+
+ const classes = classnames(baseClass, className, {
+ [`${baseClass}__file-preview`]: filePreview !== undefined && isFileSelected,
+ });
+ const buttonVariant = buttonType === "button" ? "brand" : "text-icon";
+
+ const onFileSelect = (e: React.ChangeEvent
) => {
+ const files = e.target.files;
+ onFileUpload(files);
+ setIsFileSelected(true);
+
+ e.target.value = "";
+ };
const renderGraphics = () => {
const graphicNamesArr =
@@ -64,29 +91,36 @@ const FileUploader = ({
/>
));
};
+
return (
- {renderGraphics()}
- {message}
- {additionalInfo && (
- {additionalInfo}
+ {isFileSelected && filePreview ? (
+ filePreview
+ ) : (
+ <>
+ {renderGraphics()}
+ {message}
+ {additionalInfo && (
+ {additionalInfo}
+ )}
+
+
+ {buttonType === "link" && }
+ {buttonMessage}
+
+
+
+ >
)}
-
- {buttonMessage}
-
- {
- onFileUpload(e.target.files);
- e.target.value = "";
- }}
- />
);
};
diff --git a/frontend/components/FileUploader/_styles.scss b/frontend/components/FileUploader/_styles.scss
index 4a6835d3ec..4cb2e24539 100644
--- a/frontend/components/FileUploader/_styles.scss
+++ b/frontend/components/FileUploader/_styles.scss
@@ -10,6 +10,12 @@
text-align: center;
gap: $pad-small;
+ // when the file preview is showing, we want the padding to be
+ // slightly smaller on the top and bottom.
+ &__file-preview {
+ padding: $pad-medium $pad-large;
+ }
+
&__graphics {
display: flex;
align-items: center;
@@ -39,6 +45,7 @@
display: flex;
align-items: center;
justify-content: center;
+ gap: $pad-small;
&:hover {
cursor: pointer;
diff --git a/frontend/components/FleetAce/FleetAce.tsx b/frontend/components/FleetAce/FleetAce.tsx
index 5a6f6ba7bc..c30232cb59 100644
--- a/frontend/components/FleetAce/FleetAce.tsx
+++ b/frontend/components/FleetAce/FleetAce.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useRef } from "react";
+import React, { ReactNode, useCallback, useRef } from "react";
import AceEditor from "react-ace";
import ReactAce from "react-ace/lib/ace";
import { IAceEditor } from "react-ace/lib/types";
@@ -30,10 +30,13 @@ export interface IFleetAceProps {
name?: string;
value?: string;
readOnly?: boolean;
+ maxLines?: number;
showGutter?: boolean;
wrapEnabled?: boolean;
+ /** @deprecated use the prop `className` instead */
wrapperClassName?: string;
- helpText?: string;
+ className?: string;
+ helpText?: ReactNode;
labelActionComponent?: React.ReactNode;
style?: React.CSSProperties;
onBlur?: (editor?: IAceEditor) => void;
@@ -53,9 +56,11 @@ const FleetAce = ({
name = "query-editor",
value,
readOnly,
+ maxLines = 20,
showGutter = true,
wrapEnabled = false,
wrapperClassName,
+ className,
helpText,
style,
onBlur,
@@ -64,7 +69,7 @@ const FleetAce = ({
handleSubmit = noop,
}: IFleetAceProps): JSX.Element => {
const editorRef = useRef(null);
- const wrapperClass = classnames(wrapperClassName, baseClass, {
+ const wrapperClass = classnames(className, wrapperClassName, baseClass, {
[`${baseClass}__wrapper--error`]: !!error,
});
@@ -250,7 +255,7 @@ const FleetAce = ({
fontSize={fontSize}
mode="fleet"
minLines={2}
- maxLines={20}
+ maxLines={maxLines}
name={name}
onChange={onChange}
onBlur={onBlurHandler}
diff --git a/frontend/components/TableContainer/DataTable/LinkCell/_styles.scss b/frontend/components/TableContainer/DataTable/LinkCell/_styles.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx
new file mode 100644
index 0000000000..47c5a97a58
--- /dev/null
+++ b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx
@@ -0,0 +1,90 @@
+import React from "react";
+import { InjectedRouter } from "react-router";
+import ReactTooltip from "react-tooltip";
+
+import { uniqueId } from "lodash";
+
+import Icon from "components/Icon";
+
+import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
+
+import LinkCell from "../LinkCell";
+
+const baseClass = "software-name-cell";
+
+const InstallIconWithTooltip = () => {
+ const tooltipId = uniqueId();
+ return (
+
+
+
+
+
+
+ Software can be installed on Host details page.
+
+
+
+ );
+};
+
+interface ISoftwareNameCellProps {
+ name: string;
+ source: string;
+ path?: string;
+ router?: InjectedRouter;
+ hasPackage?: boolean;
+}
+
+const SoftwareNameCell = ({
+ name,
+ source,
+ path,
+ router,
+ hasPackage = false,
+}: ISoftwareNameCellProps) => {
+ // NO path or router means it's not clickable. return
+ // a non-clickable cell early
+ if (!router || !path) {
+ return (
+
+
+ {name}
+
+ );
+ }
+
+ const onClickSoftware = (e: React.MouseEvent) => {
+ // Allows for button to be clickable in a clickable row
+ e.stopPropagation();
+ router.push(path);
+ };
+
+ return (
+
+
+ {name}
+ {hasPackage && }
+ >
+ }
+ />
+ );
+};
+
+export default SoftwareNameCell;
diff --git a/frontend/components/TableContainer/DataTable/SoftwareNameCell/_styles.scss b/frontend/components/TableContainer/DataTable/SoftwareNameCell/_styles.scss
new file mode 100644
index 0000000000..23a3f5a6a0
--- /dev/null
+++ b/frontend/components/TableContainer/DataTable/SoftwareNameCell/_styles.scss
@@ -0,0 +1,27 @@
+.software-name-cell {
+ // TODO: we do not want to use !important but have to for now. We need to pull
+ // the .link-cell styles into the LinkCell component in order to
+ // decrease the specificity of the styles. This will allow us to remove the
+ // !important from here.
+ display: flex !important;
+ align-items: center;
+ gap: $pad-small;
+
+ .software-icon {
+ width: 24px;
+ height: 24px;
+ border: 1px solid $ui-fleet-black-10;
+ border-radius: 8px;
+ }
+
+ &__install-icon {
+ // TODO: we do not want to use !important but have to for now. This is
+ // the same issue as the .software-name-cell class display value.
+ display: inline-flex !important;
+ }
+
+ &__install-tooltip-text {
+ font-weight: $regular;
+ font-size: $xx-small;
+ }
+}
diff --git a/frontend/components/TableContainer/DataTable/SoftwareNameCell/index.ts b/frontend/components/TableContainer/DataTable/SoftwareNameCell/index.ts
new file mode 100644
index 0000000000..f87c332123
--- /dev/null
+++ b/frontend/components/TableContainer/DataTable/SoftwareNameCell/index.ts
@@ -0,0 +1 @@
+export { default } from "./SoftwareNameCell";
diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx
index bdf6f2137a..a699438c85 100644
--- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx
+++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx
@@ -51,7 +51,7 @@ const TextCell = ({
};
return (
-
+
{formatter(val) || renderEmptyCell()}
);
diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx
index 68fddb04cf..1ddcd7563a 100644
--- a/frontend/components/TableContainer/TableContainer.tsx
+++ b/frontend/components/TableContainer/TableContainer.tsx
@@ -68,6 +68,9 @@ interface ITableContainerProps {
primarySelectAction?: IActionButtonProps;
/** Secondary button/s after selecting a row */
secondarySelectActions?: IActionButtonProps[]; // TODO: Combine with primarySelectAction as these are all rendered in the same spot
+ /**
+ * @deprecated please use renderCount instead
+ * */
filteredCount?: number;
searchToolTipText?: string;
searchQueryColumn?: string;
diff --git a/frontend/components/icons/Download.tsx b/frontend/components/icons/Download.tsx
index dedd7ebe3d..96b4683c2c 100644
--- a/frontend/components/icons/Download.tsx
+++ b/frontend/components/icons/Download.tsx
@@ -6,6 +6,7 @@ interface IDownload {
color?: Colors;
size?: IconSizes;
}
+
const Download = ({
color = "ui-fleet-black-75",
size = "medium",
diff --git a/frontend/components/icons/Install.tsx b/frontend/components/icons/Install.tsx
new file mode 100644
index 0000000000..778b4fdc60
--- /dev/null
+++ b/frontend/components/icons/Install.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+
+import { COLORS, Colors } from "styles/var/colors";
+import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes";
+
+interface IInstallProps {
+ color?: Colors;
+ size?: IconSizes;
+}
+
+const Install = ({
+ color = "ui-fleet-black-50",
+ size = "medium",
+}: IInstallProps) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default Install;
diff --git a/frontend/components/icons/Settings.tsx b/frontend/components/icons/Settings.tsx
new file mode 100644
index 0000000000..bf30bb2832
--- /dev/null
+++ b/frontend/components/icons/Settings.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+
+import { COLORS, Colors } from "styles/var/colors";
+import { ICON_SIZES, IconSizes } from "styles/var/icon_sizes";
+
+interface ISettingsProps {
+ color?: Colors;
+ size?: IconSizes;
+}
+
+const Settings = ({
+ size = "medium",
+ color = "ui-fleet-black-75",
+}: ISettingsProps) => {
+ return (
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts
index f260a2408c..0a19e3b1dc 100644
--- a/frontend/components/icons/index.ts
+++ b/frontend/components/icons/index.ts
@@ -56,6 +56,8 @@ import Profile from "./Profile";
import Download from "./Download";
import Upload from "./Upload";
import Refresh from "./Refresh";
+import Install from "./Install";
+import Settings from "./Settings";
// a mapping of the usable names of icons to the icon source.
export const ICON_MAP = {
@@ -116,6 +118,8 @@ export const ICON_MAP = {
download: Download,
upload: Upload,
refresh: Refresh,
+ install: Install,
+ settings: Settings,
};
export type IconNames = keyof typeof ICON_MAP;
diff --git a/frontend/hooks/useTeamIdParam.ts b/frontend/hooks/useTeamIdParam.ts
index 51274a9e33..f9b92b6ef5 100644
--- a/frontend/hooks/useTeamIdParam.ts
+++ b/frontend/hooks/useTeamIdParam.ts
@@ -1,6 +1,6 @@
import { useCallback, useContext, useEffect, useMemo } from "react";
import { InjectedRouter } from "react-router";
-import { findLastIndex, trimStart } from "lodash";
+import { findLastIndex, over, trimStart } from "lodash";
import { AppContext } from "context/app";
import { TableContext } from "context/table";
@@ -16,6 +16,30 @@ import {
import { IUser, IUserRole } from "interfaces/user";
import permissions from "utilities/permissions";
import sort from "utilities/sort";
+import { HOSTS_QUERY_PARAMS } from "services/entities/hosts";
+
+type OnTeamChangeFuncShouldStripParam = (
+ teamIdForApi: number | undefined
+) => boolean;
+
+type OnTeamChangeFuncShouldReplaceParam = (
+ teamIdForApi: number | undefined
+) => [boolean, string];
+
+/**
+ * This type is used to define functions that determine whether a query parameter should be stripped or replaced
+ * when the team id changes.
+ *
+ * The key is the name of the query parameter and the value is a function that receives the new team
+ * id with a return type of either:
+ * - boolean indicating whether the query parameter should be stripped
+ * - tuple of a boolean and a string, where the boolean indicates whether the query parameter should be replaced
+ * and the string is the new value for the query parameter
+ */
+export type IConfigOverrideParamsOnTeamChange = Record<
+ string,
+ OnTeamChangeFuncShouldReplaceParam | OnTeamChangeFuncShouldStripParam
+>;
const splitQueryStringParts = (queryString: string) =>
trimStart(queryString, "?")
@@ -27,7 +51,8 @@ const joinQueryStringParts = (parts: string[]) =>
const rebuildQueryStringWithTeamId = (
queryString: string,
- newTeamId: number
+ newTeamId: number,
+ configAdditionalParams?: IConfigOverrideParamsOnTeamChange
) => {
const parts = splitQueryStringParts(queryString);
@@ -67,6 +92,41 @@ const rebuildQueryStringWithTeamId = (
parts.splice(teamIndex, 1); // just remove the old team part
}
+ if (configAdditionalParams) {
+ Object.entries(configAdditionalParams).forEach(([paramName, fn]) => {
+ let shouldStrip = false;
+ let shouldReplace = false;
+ let replaceString = "";
+
+ const val = fn(newTeamId);
+ if (Array.isArray(val)) {
+ [shouldReplace, replaceString] = val;
+ } else if (typeof val === "boolean") {
+ shouldStrip = val;
+ }
+
+ if (shouldStrip || shouldReplace) {
+ const paramIndex = parts.findIndex((p) =>
+ p.startsWith(`${paramName}=`)
+ );
+
+ if (shouldStrip && paramIndex !== -1) {
+ parts.splice(paramIndex, 1);
+ return;
+ }
+
+ if (shouldReplace) {
+ const newPart = `${paramName}=${replaceString}`;
+ if (paramIndex === -1) {
+ parts.splice(paramIndex, 1, newPart);
+ } else {
+ parts.push(newPart);
+ }
+ }
+ }
+ });
+ }
+
return joinQueryStringParts(parts);
};
@@ -223,6 +283,7 @@ export const useTeamIdParam = ({
includeNoTeam,
permittedAccessByTeamRole,
resetSelectedRowsOnTeamChange = true,
+ overrideParamsOnTeamChange,
}: {
location?: {
pathname: string;
@@ -235,6 +296,7 @@ export const useTeamIdParam = ({
includeNoTeam: boolean;
permittedAccessByTeamRole?: Record;
resetSelectedRowsOnTeamChange?: boolean;
+ overrideParamsOnTeamChange?: IConfigOverrideParamsOnTeamChange;
}) => {
const { hash, pathname, query, search } = location;
const {
@@ -282,11 +344,18 @@ export const useTeamIdParam = ({
router.replace(
pathname
- .concat(rebuildQueryStringWithTeamId(search, teamId))
+ .concat(
+ rebuildQueryStringWithTeamId(
+ search,
+ teamId,
+ overrideParamsOnTeamChange
+ )
+ )
.concat(hash || "")
);
},
[
+ overrideParamsOnTeamChange,
resetSelectedRowsOnTeamChange,
router,
pathname,
diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts
index 51f8fe1229..d61b327676 100644
--- a/frontend/interfaces/activity.ts
+++ b/frontend/interfaces/activity.ts
@@ -71,13 +71,17 @@ export enum ActivityType {
DeletedDeclarationProfile = "deleted_declaration_profile",
EditedDeclarationProfile = "edited_declaration_profile",
ResentConfigurationProfile = "resent_configuration_profile",
+ AddedSoftware = "added_software",
+ DeletedSoftware = "deleted_software",
+ InstalledSoftware = "installed_software",
}
// This is a subset of ActivityType that are shown only for the host past activities
-export type IHostPastActivityType =
+export type IHostActivityType =
| ActivityType.RanScript
| ActivityType.LockedHost
- | ActivityType.UnlockedHost;
+ | ActivityType.UnlockedHost
+ | ActivityType.InstalledSoftware;
export interface IActivity {
created_at: string;
@@ -90,8 +94,9 @@ export interface IActivity {
details?: IActivityDetails;
}
-export type IPastActivity = Omit & {
- type: IHostPastActivityType;
+export type IHostActivity = Omit & {
+ type: IHostActivityType;
+ details: IActivityDetails;
};
export interface IActivityDetails {
@@ -118,6 +123,7 @@ export interface IActivityDetails {
host_display_name?: string;
host_display_names?: string[];
host_ids?: number[];
+ host_id?: number;
host_platform?: string;
installed_from_dep?: boolean;
mdm_platform?: "microsoft" | "apple";
@@ -132,5 +138,8 @@ export interface IActivityDetails {
deadline_days?: number;
grace_period_days?: number;
stats?: ISchedulableQueryStats;
- host_id?: number;
+ software_title?: string;
+ software_package?: string;
+ status?: string;
+ install_uuid?: string;
}
diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts
index 3ce5f5d268..68923d47b7 100644
--- a/frontend/interfaces/software.ts
+++ b/frontend/interfaces/software.ts
@@ -50,9 +50,24 @@ export interface ISoftwareTitleVersion {
hosts_count?: number;
}
+export interface ISoftwarePackage {
+ name: string;
+ version: string;
+ uploaded_at: string;
+ install_script: string;
+ pre_install_query?: string;
+ post_install_script?: string;
+ status: {
+ installed: number;
+ pending: number;
+ failed: number;
+ };
+}
+
export interface ISoftwareTitle {
id: number;
name: string;
+ software_package: ISoftwarePackage | null;
versions_count: number;
source: string;
hosts_count: number;
@@ -123,7 +138,7 @@ export const formatSoftwareType = ({
browser,
}: {
source: string;
- browser: string;
+ browser?: string;
}) => {
let type = SOURCE_TYPE_CONVERSION[source] || "Unknown";
if (browser) {
@@ -133,3 +148,71 @@ export const formatSoftwareType = ({
}
return type;
};
+
+/**
+ * This list comprises all possible states of software install operations.
+ */
+export const SOFTWARE_INSTALL_STATUSES = [
+ "failed",
+ "installed",
+ "pending",
+] as const;
+
+/*
+ * SoftwareInstallStatus represents the possible states of software install operations.
+ */
+export type SoftwareInstallStatus = typeof SOFTWARE_INSTALL_STATUSES[number];
+
+export const isValidSoftwareInstallStatus = (
+ s: string | undefined
+): s is SoftwareInstallStatus =>
+ !!s && SOFTWARE_INSTALL_STATUSES.includes(s as SoftwareInstallStatus);
+
+/**
+ * ISoftwareInstallResult is the shape of a software install result object
+ * returned by the Fleet API.
+ */
+export interface ISoftwareInstallResult {
+ install_uuid: string;
+ software_title: string;
+ software_title_id: number;
+ software_package: string;
+ host_id: number;
+ host_display_name: string;
+ status: SoftwareInstallStatus;
+ detail: string;
+ output: string;
+ pre_install_query_output: string;
+ post_install_script_output: string;
+}
+
+export interface ISoftwareInstallResults {
+ results: ISoftwareInstallResult;
+}
+
+// ISoftwareInstallerType defines the supported installer types for
+// software uploaded by the IT admin.
+export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "exe";
+
+export interface ISoftwareLastInstall {
+ install_uuid: string;
+ installed_at: string;
+}
+
+export interface ISoftwareInstallVersion {
+ version: string;
+ last_opened_at: string | null;
+ vulnerabilities: string[] | null;
+ installed_paths: string[];
+}
+
+export interface IHostSoftware {
+ id: number;
+ name: string;
+ package_available_for_install?: string | null;
+ source: string;
+ bundle_identifier?: string;
+ status: SoftwareInstallStatus | null;
+ last_install: ISoftwareLastInstall | null;
+ installed_versions: ISoftwareInstallVersion[] | null;
+}
diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx
index ed693a2a24..e4a6f90c7b 100644
--- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx
+++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx
@@ -15,6 +15,9 @@ import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
// @ts-ignore
import FleetIcon from "components/icons/FleetIcon";
+
+import { SoftwareInstallDetailsModal } from "pages/SoftwarePage/components/SoftwareInstallDetails";
+
import ActivityItem from "./ActivityItem";
import ScriptDetailsModal from "./components/ScriptDetailsModal/ScriptDetailsModal";
@@ -35,6 +38,7 @@ const ActivityFeed = ({
const [pageIndex, setPageIndex] = useState(0);
const [showShowQueryModal, setShowShowQueryModal] = useState(false);
const [showScriptDetailsModal, setShowScriptDetailsModal] = useState(false);
+ const [installedSoftwareUuid, setInstalledSoftwareUuid] = useState("");
const queryShown = useRef("");
const queryImpact = useRef(undefined);
const scriptExecutionId = useRef("");
@@ -81,6 +85,7 @@ const ActivityFeed = ({
activityType: ActivityType,
details: IActivityDetails
) => {
+ console.log("activityType", activityType);
switch (activityType) {
case ActivityType.LiveQuery:
queryShown.current = details.query_sql ?? "";
@@ -93,6 +98,11 @@ const ActivityFeed = ({
scriptExecutionId.current = details.script_execution_id ?? "";
setShowScriptDetailsModal(true);
break;
+ case ActivityType.InstalledSoftware:
+ // installUuid.current = details.install_uuid ?? "";
+ // console.log("installUuid.current", installUuid.current);
+ setInstalledSoftwareUuid(details.install_uuid ?? "");
+ break;
default:
break;
}
@@ -184,6 +194,12 @@ const ActivityFeed = ({
onCancel={() => setShowScriptDetailsModal(false)}
/>
)}
+ {installedSoftwareUuid && (
+ setInstalledSoftwareUuid("")}
+ />
+ )}
);
};
diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
index 9827afc2ec..7378d190ee 100644
--- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
+++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx
@@ -16,6 +16,7 @@ import Icon from "components/Icon";
import ReactTooltip from "react-tooltip";
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
import { COLORS } from "styles/var/colors";
+import { getSoftwareInstallStatusPredicate } from "pages/hosts/details/cards/Activity/ActivityItems/InstalledSoftwareActivityItem/InstalledSoftwareActivityItem";
const baseClass = "activity-item";
@@ -820,6 +821,76 @@ const TAGGED_TEMPLATES = {
>
);
},
+ addedSoftware: (activity: IActivity) => {
+ return (
+ <>
+ {" "}
+ added
- Search for installed software{" "}
- {(isGlobalAdmin || isGlobalMaintainer) &&
- (!isPremiumTier || !isAnyTeamSelected) &&
- "and manage automations for detected vulnerabilities (CVEs)"}{" "}
- on{" "}
- {isPremiumTier && isAnyTeamSelected
- ? "all hosts assigned to this team"
- : "all of your hosts"}
- .
+ Manage software and search for installed software, OS and
+ vulnerabilities {isAnyTeamSelected ? "on this team" : "for all hosts"}.
);
};
@@ -342,8 +383,8 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
teamId: teamIdForApi,
// TODO: move down into the Software Titles component
query,
- showVulnerableSoftware,
showExploitedVulnerabilitiesOnly,
+ softwareFilter,
})}
);
@@ -358,15 +399,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {