+
- {installButtonText ? (
+
+
+
+ {canUninstallSoftware && (
- ) : (
- DEFAULT_EMPTY_CELL_VALUE
)}
@@ -258,6 +356,7 @@ interface ISelfServiceTableHeaders {
deviceToken: string;
onInstall: () => void;
onShowInstallerDetails: (uuid?: InstallOrCommandUuid) => void;
+ onClickUninstallAction: (software: IHostSoftware) => void;
}
// NOTE: cellProps come from react-table
@@ -266,6 +365,7 @@ export const generateSoftwareTableHeaders = ({
deviceToken,
onInstall,
onShowInstallerDetails,
+ onClickUninstallAction,
}: ISelfServiceTableHeaders): ISoftwareTableConfig[] => {
const tableHeaders: ISoftwareTableConfig[] = [
{
@@ -320,7 +420,10 @@ export const generateSoftwareTableHeaders = ({
+ onClickUninstallAction(cellProps.row.original)
+ }
/>
);
},
diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/UninstallSoftwareModal.tests.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/UninstallSoftwareModal.tests.tsx
new file mode 100644
index 0000000000..708bd090ff
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/UninstallSoftwareModal.tests.tsx
@@ -0,0 +1,78 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { noop } from "lodash";
+import UninstallSoftwareModal from "./UninstallSoftwareModal";
+
+describe("UninstallSoftwareModal", () => {
+ it("renders the generic uninstall message with software name", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(
+ /Uninstalling this software will remove it and may remove Slack data from your device/i
+ )
+ ).toBeVisible();
+ expect(screen.getByRole("button", { name: /Uninstall/i })).toBeVisible();
+ expect(screen.getByRole("button", { name: /Cancel/i })).toBeVisible();
+ });
+
+ it("renders the generic uninstall message with default software name", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(/Uninstalling this software will remove it/i)
+ ).toBeVisible();
+ });
+
+ it("renders the MSI-specific message when installer type is 'msi'", () => {
+ render(
+
+ );
+
+ expect(
+ screen.getByText(/this will only uninstall version 5.0.0/i)
+ ).toBeVisible();
+ expect(screen.getByRole("link", { name: /click here/i })).toBeVisible();
+ });
+
+ it("does not render the MSI-specific message for non-msi installer types", () => {
+ render(
+
+ );
+
+ expect(
+ screen.queryByText(/this will only uninstall version/i)
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/UninstallSoftwareModal.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/UninstallSoftwareModal.tsx
new file mode 100644
index 0000000000..e3398a8f88
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/UninstallSoftwareModal.tsx
@@ -0,0 +1,93 @@
+import React, { useCallback, useContext, useState } from "react";
+
+import deviceUserAPI from "services/entities/device_user";
+import { NotificationContext } from "context/notification";
+import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
+
+import Modal from "components/Modal";
+import Button from "components/buttons/Button";
+import CustomLink from "components/CustomLink";
+
+const baseClass = "uninstall-software-modal";
+
+interface IUninstallSoftwareModalProps {
+ softwareId: number;
+ softwareName?: string;
+ softwareInstallerType?: string;
+ version?: string;
+ token: string;
+ onExit: () => void;
+ onSuccess: () => void;
+}
+
+const UninstallSoftwareModal = ({
+ softwareId,
+ softwareName,
+ softwareInstallerType,
+ version,
+ token,
+ onExit,
+ onSuccess,
+}: IUninstallSoftwareModalProps) => {
+ const { renderFlash } = useContext(NotificationContext);
+ const [isUninstalling, setIsUninstalling] = useState(false);
+
+ const onUninstallSoftware = useCallback(async () => {
+ setIsUninstalling(true);
+ try {
+ await deviceUserAPI.uninstallSelfServiceSoftware(token, softwareId);
+ // TODO: Change this toast message or hide it all together?
+ // renderFlash("success", "Software uninstalled successfully!");
+ onSuccess();
+ } catch (error) {
+ renderFlash("error", "Couldn't uninstall. Please try again.");
+ }
+ setIsUninstalling(false);
+ onExit();
+ }, [softwareId, renderFlash, onSuccess, onExit]);
+
+ const displaySoftwareName = softwareName || "software";
+ const msiInstaller = softwareInstallerType === "msi";
+
+ return (
+
+ <>
+
+ Uninstalling this software will remove it and may remove{" "}
+ {softwareName} data from your device. You can always reinstall it
+ again later.
+
+ {msiInstaller && (
+
+ By default, this will only uninstall version {version}. To learn how
+ to uninstall other versions,{" "}
+
+
+ )}
+
+
+
+
+ >
+
+ );
+};
+
+export default UninstallSoftwareModal;
diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/_styles.scss b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/_styles.scss
new file mode 100644
index 0000000000..40f0f497e2
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/_styles.scss
@@ -0,0 +1,3 @@
+.uninstall-software-modal {
+ overflow-wrap: anywhere; // Prevent long software name overflow
+}
diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/index.ts b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/index.ts
new file mode 100644
index 0000000000..ee67a882d6
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Software/SelfService/UninstallSoftwareModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./UninstallSoftwareModal";
diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/_styles.scss b/frontend/pages/hosts/details/cards/Software/SelfService/_styles.scss
index 6b81e8fb19..f185897d12 100644
--- a/frontend/pages/hosts/details/cards/Software/SelfService/_styles.scss
+++ b/frontend/pages/hosts/details/cards/Software/SelfService/_styles.scss
@@ -33,9 +33,23 @@
gap: $pad-small;
}
+ .self-service-table__status-content {
+ min-width: 100px; // No table layout shift when changed to pending status
+ }
+
.self-service-table__item-status-button {
height: auto;
}
+
+ .self-service-table__item-actions {
+ display: flex;
+ flex-direction: row;
+ gap: $pad-large;
+ }
+
+ .self-service-table__item-action {
+ min-width: 82px; // Second action buttons align between rows
+ }
}
.categories-menu {
diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts
index 0e5b20cbef..b798311256 100644
--- a/frontend/services/entities/device_user.ts
+++ b/frontend/services/entities/device_user.ts
@@ -90,6 +90,17 @@ export default {
return sendRequest("POST", path);
},
+ uninstallSelfServiceSoftware: (
+ deviceToken: string,
+ softwareTitleId: number
+ ) => {
+ const { DEVICE_SOFTWARE_UNINSTALL } = endpoints;
+ return sendRequest(
+ "POST",
+ DEVICE_SOFTWARE_UNINSTALL(deviceToken, softwareTitleId)
+ );
+ },
+
/** Gets more info on FMA/custom package install for device user */
getSoftwareInstallResult: (deviceToken: string, uuid: string) => {
const { DEVICE_SOFTWARE_INSTALL_RESULTS } = endpoints;
diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index 053070da6e..460de70148 100644
--- a/frontend/utilities/endpoints.ts
+++ b/frontend/utilities/endpoints.ts
@@ -41,6 +41,8 @@ export default {
`/${API_VERSION}/fleet/device/${token}/software/install/${softwareTitleId}`,
DEVICE_SOFTWARE_INSTALL_RESULTS: (token: string, uuid: string) =>
`/${API_VERSION}/fleet/device/${token}/software/install/${uuid}/results`,
+ DEVICE_SOFTWARE_UNINSTALL: (token: string, softwareTitleId: number) =>
+ `/${API_VERSION}/fleet/device/${token}/software/uninstall/${softwareTitleId}`,
DEVICE_VPP_COMMAND_RESULTS: (token: string, uuid: string) =>
`/${API_VERSION}/fleet/device/${token}/software/commands/${uuid}/results`,
DEVICE_USER_MDM_ENROLLMENT_PROFILE: (token: string): string => {