UI – Follow-ups for iPadOS/iPadOS VPP (#20916)

## Follow ups to #20467, part 4 of #20917
- Use combination of apps' fields to uniquely identify them – [bug and
fix
demo/explanation](https://www.loom.com/share/2e5f088677604f04927bce8d9dacf8fe?sid=d946bea5-11a9-419a-b946-962829a53adc)
- Add new field to vpp `POST` requests for correct add VPP software
functionality

![Screenshot-2024-07-31-at-50131PM](https://github.com/user-attachments/assets/57c925f9-53cb-4860-b6b6-475b6d5cb2a5)

- Implement desired states for software action dropdown

![Screenshot-2024-07-31-at-45556PM](https://github.com/user-attachments/assets/b6d3db97-dea2-43bb-9662-29256e87fbf0)


- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
This commit is contained in:
jacobshandling 2024-08-01 09:17:57 -07:00 committed by GitHub
parent 08d08d5602
commit 2ccc0f79e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 71 additions and 43 deletions

View file

@ -22,7 +22,7 @@ import { NotificationContext } from "context/notification";
import { getErrorReason } from "interfaces/errors";
import { buildQueryStringFromParams } from "utilities/url";
import SoftwareIcon from "../icons/SoftwareIcon";
import { getErrorMessage } from "./helpers";
import { getErrorMessage, getUniqueAppId } from "./helpers";
const baseClass = "app-store-vpp";
@ -64,10 +64,16 @@ const NoVppAppsCard = () => (
interface IVppAppListItemProps {
app: IVppApp;
selected: boolean;
uniqueAppId: string;
onSelect: (software: IVppApp) => void;
}
const VppAppListItem = ({ app, selected, onSelect }: IVppAppListItemProps) => {
const VppAppListItem = ({
app,
selected,
uniqueAppId,
onSelect,
}: IVppAppListItemProps) => {
return (
<li className={`${baseClass}__list-item`}>
<Radio
@ -77,9 +83,9 @@ const VppAppListItem = ({ app, selected, onSelect }: IVppAppListItemProps) => {
<span>{app.name}</span>
</div>
}
id={`vppApp-${app.app_store_id}`}
id={`vppApp-${uniqueAppId}`}
checked={selected}
value={app.app_store_id.toString()}
value={uniqueAppId}
name="vppApp"
onChange={() => onSelect(app)}
/>
@ -98,20 +104,27 @@ interface IVppAppListProps {
onSelect: (app: IVppApp) => void;
}
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => (
<div className={`${baseClass}__list-container`}>
<ul className={`${baseClass}__list`}>
{apps.map((app) => (
<VppAppListItem
key={app.app_store_id}
app={app}
selected={selectedApp?.app_store_id === app.app_store_id}
onSelect={onSelect}
/>
))}
</ul>
</div>
);
const VppAppList = ({ apps, selectedApp, onSelect }: IVppAppListProps) => {
const uniqueSelectedAppId = selectedApp ? getUniqueAppId(selectedApp) : null;
return (
<div className={`${baseClass}__list-container`}>
<ul className={`${baseClass}__list`}>
{apps.map((app) => {
const uniqueAppId = getUniqueAppId(app);
return (
<VppAppListItem
key={uniqueAppId}
app={app}
selected={uniqueSelectedAppId === uniqueAppId}
uniqueAppId={uniqueAppId}
onSelect={onSelect}
/>
);
})}
</ul>
</div>
);
};
interface IAppStoreVppProps {
teamId: number;
@ -160,7 +173,11 @@ const AppStoreVpp = ({ teamId, router, onExit }: IAppStoreVppProps) => {
}
try {
await mdmAppleAPI.addVppApp(teamId, selectedApp.app_store_id);
await mdmAppleAPI.addVppApp(
teamId,
selectedApp.app_store_id,
selectedApp.platform
);
renderFlash(
"success",
<>

View file

@ -1,5 +1,6 @@
import React from "react";
import { getErrorReason } from "interfaces/errors";
import { IVppApp } from "services/entities/mdm_apple";
const ADD_SOFTWARE_ERROR_PREFIX = "Couldnt add software.";
const DEFAULT_ERROR_MESSAGE = `${ADD_SOFTWARE_ERROR_PREFIX} Please try again.`;
@ -40,3 +41,6 @@ export const getErrorMessage = (e: unknown) => {
}
return DEFAULT_ERROR_MESSAGE;
};
export const getUniqueAppId = (app: IVppApp) =>
`${app.app_store_id}_${app.platform}`;

View file

@ -415,7 +415,7 @@ const DeviceUserPage = ({
<SoftwareCard
id={deviceAuthToken}
softwareUpdatedAt={host.software_updated_at}
isFleetdHost={!!host.orbit_version}
hostCanInstallSoftware={!!host.orbit_version}
router={router}
pathname={location.pathname}
queryParams={parseHostSoftwareQueryParams(location.query)}

View file

@ -922,7 +922,9 @@ const HostDetailsPage = ({
<SoftwareCard
id={host.id}
softwareUpdatedAt={host.software_updated_at}
isFleetdHost={!!host.orbit_version}
hostCanInstallSoftware={
!!host.orbit_version || isIosOrIpadosHost
}
isSoftwareEnabled={featuresConfig?.enable_software_inventory}
router={router}
queryParams={parseHostSoftwareQueryParams(location.query)}

View file

@ -35,7 +35,7 @@ interface IHostSoftwareProps {
/** This is the host id or the device token */
id: number | string;
softwareUpdatedAt?: string;
isFleetdHost: boolean;
hostCanInstallSoftware: boolean;
router: InjectedRouter;
queryParams: ReturnType<typeof parseHostSoftwareQueryParams>;
pathname: string;
@ -83,7 +83,7 @@ export const parseHostSoftwareQueryParams = (queryParams: {
const HostSoftware = ({
id,
softwareUpdatedAt,
isFleetdHost,
hostCanInstallSoftware,
router,
queryParams,
pathname,
@ -169,7 +169,7 @@ const HostSoftware = ({
[isMyDevicePage, refetchDeviceSoftware, refetchHostSoftware]
);
const canInstallSoftware = Boolean(
const userHasSWInstallPermission = Boolean(
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer
);
@ -213,19 +213,19 @@ const HostSoftware = ({
: generateHostSoftwareTableConfig({
router,
installingSoftwareId,
canInstall: canInstallSoftware,
userHasSWInstallPermission,
onSelectAction,
teamId: hostTeamId,
isFleetdHost,
hostCanInstallSoftware,
});
}, [
isMyDevicePage,
router,
installingSoftwareId,
canInstallSoftware,
userHasSWInstallPermission,
onSelectAction,
hostTeamId,
isFleetdHost,
hostCanInstallSoftware,
]);
const isLoading = isMyDevicePage

View file

@ -50,17 +50,17 @@ type IVulnerabilitiesCellProps = IInstalledVersionsCellProps;
// type IActionsCellProps = CellProps<IHostSoftware, IHostSoftware["id"]>;
const generateActions = ({
canInstall,
userHasSWInstallPermission,
hostCanInstallSoftware,
installingSoftwareId,
isFleetdHost,
softwareId,
status,
software_package,
app_store_app,
}: {
canInstall: boolean;
userHasSWInstallPermission: boolean;
hostCanInstallSoftware: boolean;
installingSoftwareId: number | null;
isFleetdHost: boolean;
softwareId: number;
status: SoftwareInstallStatus | null;
software_package: IHostSoftwarePackage | null;
@ -78,14 +78,18 @@ const generateActions = ({
}
const hasSoftwareToInstall = !!software_package || !!app_store_app;
// remove install if there is no package to install
if (!hasSoftwareToInstall || !canInstall) {
// remove install if there is no package to install or if the software is already installed
if (
!hasSoftwareToInstall ||
!userHasSWInstallPermission ||
status === "installed"
) {
actions.splice(indexInstallAction, 1);
return actions;
}
// disable install option if not a fleetd host
if (!isFleetdHost) {
// disable install option if not a fleetd, iPad, or iOS host
if (!hostCanInstallSoftware) {
actions[indexInstallAction].disabled = true;
actions[indexInstallAction].tooltipContent =
"To install software on this host, deploy the fleetd agent with --enable-scripts and refetch host vitals.";
@ -102,9 +106,9 @@ const generateActions = ({
};
interface ISoftwareTableHeadersProps {
canInstall: boolean;
userHasSWInstallPermission: boolean;
hostCanInstallSoftware: boolean;
installingSoftwareId: number | null;
isFleetdHost: boolean;
router: InjectedRouter;
teamId: number;
onSelectAction: (software: IHostSoftware, action: string) => void;
@ -113,9 +117,9 @@ interface ISoftwareTableHeadersProps {
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
export const generateSoftwareTableHeaders = ({
canInstall,
userHasSWInstallPermission,
hostCanInstallSoftware,
installingSoftwareId,
isFleetdHost,
router,
teamId,
onSelectAction,
@ -202,8 +206,8 @@ export const generateSoftwareTableHeaders = ({
<DropdownCell
placeholder="Actions"
options={generateActions({
canInstall,
isFleetdHost,
userHasSWInstallPermission,
hostCanInstallSoftware,
installingSoftwareId,
softwareId,
status,

View file

@ -69,11 +69,12 @@ export default {
return sendRequest("GET", path);
},
addVppApp: (teamId: number, appStoreId: string) => {
addVppApp: (teamId: number, appStoreId: string, platform: ApplePlatform) => {
const { MDM_APPLE_VPP_APPS } = endpoints;
return sendRequest("POST", MDM_APPLE_VPP_APPS, {
app_store_id: appStoreId,
team_id: teamId,
platform,
});
},
};