Add installed and available versions to self-service (#47526)

**Related issue:** Resolves #45729

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.


## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Self-service software table on My device page now shows both installed
and available version columns for each app, making it easier to see
current and updateable versions at a glance.
* **Tests**
* Added/updated tests covering installed and available version display
scenarios, including cases with missing installed versions and when both
versions are present to ensure correct UI rendering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Steven Palmesano
2026-07-16 06:33:05 -05:00
committed by GitHub
parent 3e695c79fe
commit 40f5dc3358
3 changed files with 87 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
- Added installed version and available version columns to the self-service software table on the My device page.
@@ -289,6 +289,55 @@ describe("SelfService", () => {
expect(moreDropdown).toBeDisabled();
});
it("shows empty cell for installed version and package version for available version when installed_versions is null", async () => {
mockServer.use(
customDeviceSoftwareHandler({
software: [
createMockDeviceSoftware({
installed_versions: null,
software_package: createMockHostSoftwarePackage({
version: "1.1.0",
}),
}),
],
})
);
const render = createCustomRenderer({ withBackendMock: true });
render(<SelfService {...TEST_PROPS} />);
await screen.findAllByText("mock software 1.app");
expect(screen.getAllByText("---")).toHaveLength(2);
// TooltipTruncatedTextCell renders the value twice (visible + tooltip div)
expect(screen.getAllByText("1.1.0")).toHaveLength(2);
});
it("shows installed version and available version when both are present", async () => {
mockServer.use(
customDeviceSoftwareHandler({
software: [
createMockDeviceSoftware({
installed_versions: [DEFAULT_INSTALLED_VERSION], // "1.0.0"
software_package: createMockHostSoftwarePackage({
version: "1.1.0",
}),
}),
],
})
);
const render = createCustomRenderer({ withBackendMock: true });
render(<SelfService {...TEST_PROPS} />);
await screen.findAllByText("mock software 1.app");
// TooltipTruncatedTextCell renders each value twice (visible + tooltip div);
// available version also appears in the update card above the table
expect(screen.getAllByText("1.0.0")).toHaveLength(2);
expect(screen.getAllByText("1.1.0")).toHaveLength(3);
});
it("renders the self-service list for BYOD Account-Driven User Enrollment on mobile view", async () => {
mockServer.use(
customDeviceSoftwareHandler({
@@ -12,6 +12,7 @@ import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell";
import VersionCell from "pages/SoftwarePage/components/tables/VersionCell";
import { ISWUninstallDetailsParentState } from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal";
import InstallStatusCell from "../../../InstallStatusCell/InstallStatusCell";
@@ -25,6 +26,15 @@ type IStatusCellProps = CellProps<
IDeviceSoftwareWithUiStatus,
IDeviceSoftwareWithUiStatus["ui_status"]
>;
type IVersionsCellProps = CellProps<
IDeviceSoftwareWithUiStatus,
IDeviceSoftwareWithUiStatus["installed_versions"]
>;
type IAvailableVersionCellProps = CellProps<
IDeviceSoftwareWithUiStatus,
| IDeviceSoftwareWithUiStatus["software_package"]
| IDeviceSoftwareWithUiStatus["app_store_app"]
>;
type IActionCellProps = CellProps<
IDeviceSoftwareWithUiStatus,
IDeviceSoftwareWithUiStatus["status"]
@@ -114,6 +124,33 @@ export const generateSoftwareTableHeaders = ({
/>
),
},
{
Header: "Installed version",
id: "version",
disableSortBy: true,
// we use function as accessor because we have two columns that
// need to access the same data. This is not supported with a string
// accessor.
accessor: (originalRow) => originalRow.installed_versions,
Cell: (cellProps: IVersionsCellProps) => {
return <VersionCell versions={cellProps.cell.value} />;
},
},
{
Header: "Available version",
id: "available_version",
disableSortBy: true,
accessor: (originalRow) =>
originalRow.software_package || originalRow.app_store_app,
Cell: (cellProps: IAvailableVersionCellProps) => {
const softwareTitle = cellProps.row.original;
const installerData =
softwareTitle.software_package ?? softwareTitle.app_store_app;
return (
<VersionCell versions={[{ version: installerData?.version || "" }]} />
);
},
},
{
Header: "Actions",
accessor: "status",