diff --git a/changes/28379-vpp-app-install-status b/changes/28379-vpp-app-install-status
new file mode 100644
index 0000000000..8ccad0f8a6
--- /dev/null
+++ b/changes/28379-vpp-app-install-status
@@ -0,0 +1 @@
+Fleet UI: Install Status correctly displays available for self-service for VPP apps
diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx
new file mode 100644
index 0000000000..daa311abdb
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tests.tsx
@@ -0,0 +1,160 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { renderWithSetup } from "test/test-utils";
+import {
+ createMockHostAppStoreApp,
+ createMockHostSoftwarePackage,
+} from "__mocks__/hostMock";
+
+import InstallStatusCell from "./InstallStatusCell";
+
+// Mock lodash uniqueId to always return the same id for stable tests
+jest.mock("lodash", () => ({
+ ...jest.requireActual("lodash"),
+ uniqueId: jest.fn(() => "test-tooltip-id"),
+}));
+
+const testSoftwarePackage = createMockHostSoftwarePackage();
+
+describe("InstallStatusCell - component", () => {
+ it("renders 'Installed' status with tooltip", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getByText("Installed")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Installed"));
+ expect(screen.getByText(/Software is installed/i)).toBeInTheDocument();
+ });
+
+ it("renders 'Installing (pending)' status with tooltip", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getByText("Installing (pending)")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Installing (pending)"));
+ expect(
+ screen.getByText(/Fleet is installing or will install/i)
+ ).toBeInTheDocument();
+ });
+
+ it("renders 'Uninstalling (pending)' status with tooltip", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getByText("Uninstalling (pending)")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Uninstalling (pending)"));
+ expect(
+ screen.getByText(/Fleet is uninstalling or will uninstall/i)
+ ).toBeInTheDocument();
+ });
+
+ it("renders 'Install (failed)' status with tooltip", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getByText("Install (failed)")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Install (failed)"));
+ expect(
+ screen.getByText(/The host failed to install software/i)
+ ).toBeInTheDocument();
+ });
+
+ it("renders 'Uninstall (failed)' status with tooltip", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getByText("Uninstall (failed)")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Uninstall (failed)"));
+ expect(
+ screen.getByText(/The host failed to uninstall software/i)
+ ).toBeInTheDocument();
+ });
+
+ it("renders 'Available for install' for package", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getByText("Available for install")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Available for install"));
+ expect(screen.getByText(/can be installed/i)).toBeInTheDocument();
+ });
+
+ it("renders 'Available for install' for App Store app", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getByText("Available for install")).toBeInTheDocument();
+
+ await user.hover(screen.getByText("Available for install"));
+ expect(screen.getByText(/can be installed/i)).toBeInTheDocument();
+ });
+
+ it("renders 'Self-service' for package with self_service true", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getAllByText("Self-service").length).toBeGreaterThan(0);
+
+ await user.hover(screen.getAllByText("Self-service")[0]);
+ expect(screen.getByText(/can be installed/i)).toBeInTheDocument();
+ });
+
+ it("renders 'Self-service' for App Store app with self_service true", async () => {
+ const { user } = renderWithSetup(
+
+ );
+
+ expect(screen.getAllByText("Self-service").length).toBeGreaterThan(0);
+
+ await user.hover(screen.getAllByText("Self-service")[0]);
+ expect(screen.getByText(/Software can be installed/i)).toBeInTheDocument();
+ });
+
+ it("renders placeholder for missing status and packages", () => {
+ render();
+
+ expect(screen.getByText("---")).toBeInTheDocument();
+ });
+});
diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx
index f27df2fd38..b6c74df851 100644
--- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx
+++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx
@@ -123,7 +123,8 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record<
},
};
-type IInstallStatusCellProps = IHostSoftware;
+type IInstallStatusCellProps = Pick &
+ Partial>;
const InstallStatusCell = ({
status,
@@ -138,8 +139,7 @@ const InstallStatusCell = ({
if (status !== null) {
displayStatus = status;
- } else if (software_package?.self_service) {
- // currently only software packages can be self-service
+ } else if (software_package?.self_service || app_store_app?.self_service) {
displayStatus = "selfService";
} else if (hasPackage || hasAppStoreApp) {
displayStatus = "avaiableForInstall";