Fleet UI: Ability to update android configuration + FE cleanups (#37065)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Fleet UI: Add ability to edit Android software config
|
||||
@@ -166,6 +166,32 @@ export const createMockAppStoreApp = (overrides?: Partial<IAppStoreApp>) => {
|
||||
return { ...DEFAULT_APP_STORE_APP_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
const DEFAULT_APP_STORE_APP_ANDROID_MOCK: IAppStoreApp = {
|
||||
name: "test app",
|
||||
display_name: "Test App",
|
||||
app_store_id: "com.test.app",
|
||||
created_at: "2020-01-01T00:00:00.000Z",
|
||||
platform: "android",
|
||||
icon_url: "https://via.placeholder.com/512",
|
||||
latest_version: "1.2.3",
|
||||
self_service: true,
|
||||
status: {
|
||||
installed: 1,
|
||||
pending: 2,
|
||||
failed: 3,
|
||||
},
|
||||
categories: null,
|
||||
labels_include_any: null,
|
||||
labels_exclude_any: null,
|
||||
configuration: '{ workProfileWidgets: "WORK_PROFILE_WIDGETS_ALLOWED" }',
|
||||
};
|
||||
|
||||
export const createMockAppStoreAppAndroid = (
|
||||
overrides?: Partial<IAppStoreApp>
|
||||
) => {
|
||||
return { ...DEFAULT_APP_STORE_APP_ANDROID_MOCK, ...overrides };
|
||||
};
|
||||
|
||||
const DEFAULT_SOFTWARE_TITLE_DETAILS_MOCK: ISoftwareTitleDetails = {
|
||||
id: 1,
|
||||
name: "test.app",
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useContext, useMemo } from "react";
|
||||
import { AppContext } from "context/app";
|
||||
import { isAndroid } from "interfaces/platform";
|
||||
import {
|
||||
ISoftwareTitleDetails,
|
||||
ISoftwarePackage,
|
||||
IAppStoreApp,
|
||||
isSoftwarePackage,
|
||||
isIpadOrIphoneSoftwareSource,
|
||||
InstallerType,
|
||||
} from "interfaces/software";
|
||||
import {
|
||||
getInstallerCardInfo,
|
||||
InstallerCardInfo,
|
||||
} from "pages/SoftwarePage/SoftwareTitleDetailsPage/helpers";
|
||||
|
||||
export interface SoftwareInstallerMeta {
|
||||
installerType: InstallerType;
|
||||
isAndroidPlayStoreApp: boolean;
|
||||
isFleetMaintainedApp: boolean;
|
||||
isCustomPackage: boolean;
|
||||
isIosOrIpadosApp: boolean;
|
||||
sha256?: string;
|
||||
androidPlayStoreId?: string;
|
||||
automaticInstallPolicies:
|
||||
| ISoftwarePackage["automatic_install_policies"]
|
||||
| IAppStoreApp["automatic_install_policies"];
|
||||
gitOpsModeEnabled: boolean;
|
||||
repoURL?: string;
|
||||
canManageSoftware: boolean;
|
||||
/** Raw ISoftwarePackage | IAppStoreApp data */
|
||||
softwareInstaller: ISoftwarePackage | IAppStoreApp;
|
||||
}
|
||||
|
||||
export interface UseSoftwareInstallerResult {
|
||||
cardInfo: InstallerCardInfo;
|
||||
meta: SoftwareInstallerMeta;
|
||||
}
|
||||
|
||||
/** This is used to extract software installer data
|
||||
* (FMA, VPP, Google Playstore Apps, custom packages)
|
||||
* from ISoftwareTitleDetails to be used in the UI */
|
||||
export const useSoftwareInstaller = (
|
||||
softwareTitle: ISoftwareTitleDetails
|
||||
): UseSoftwareInstallerResult | undefined => {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!softwareTitle.software_package && !softwareTitle.app_store_app) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cardInfo = getInstallerCardInfo(softwareTitle);
|
||||
const { softwareInstaller, source } = cardInfo;
|
||||
|
||||
const isIosOrIpadosApp = isIpadOrIphoneSoftwareSource(source);
|
||||
|
||||
const installerType: InstallerType = isSoftwarePackage(softwareInstaller)
|
||||
? "package"
|
||||
: "app-store";
|
||||
|
||||
const isAndroidPlayStoreApp =
|
||||
"platform" in softwareInstaller && isAndroid(softwareInstaller.platform);
|
||||
|
||||
const isFleetMaintainedApp =
|
||||
"fleet_maintained_app_id" in softwareInstaller &&
|
||||
!!softwareInstaller.fleet_maintained_app_id;
|
||||
|
||||
const isCustomPackage =
|
||||
installerType === "package" && !isFleetMaintainedApp;
|
||||
|
||||
const sha256 =
|
||||
("hash_sha256" in softwareInstaller && softwareInstaller.hash_sha256) ||
|
||||
undefined;
|
||||
|
||||
const androidPlayStoreId =
|
||||
isAndroidPlayStoreApp && "app_store_id" in softwareInstaller
|
||||
? softwareInstaller?.app_store_id
|
||||
: undefined;
|
||||
|
||||
const {
|
||||
automatic_install_policies: automaticInstallPolicies,
|
||||
} = softwareInstaller;
|
||||
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isTeamAdmin,
|
||||
isTeamMaintainer,
|
||||
config,
|
||||
} = appContext;
|
||||
|
||||
const {
|
||||
gitops_mode_enabled: configGitOpsModeEnabled,
|
||||
repository_url: repoURL,
|
||||
} = config?.gitops || {};
|
||||
|
||||
const gitOpsModeEnabled = !!configGitOpsModeEnabled;
|
||||
|
||||
const canManageSoftware = !!(
|
||||
isGlobalAdmin ||
|
||||
isGlobalMaintainer ||
|
||||
isTeamAdmin ||
|
||||
isTeamMaintainer
|
||||
);
|
||||
|
||||
return {
|
||||
cardInfo,
|
||||
meta: {
|
||||
installerType,
|
||||
isAndroidPlayStoreApp,
|
||||
isFleetMaintainedApp,
|
||||
isCustomPackage,
|
||||
isIosOrIpadosApp,
|
||||
sha256,
|
||||
androidPlayStoreId,
|
||||
automaticInstallPolicies,
|
||||
gitOpsModeEnabled,
|
||||
repoURL,
|
||||
canManageSoftware,
|
||||
softwareInstaller,
|
||||
},
|
||||
};
|
||||
}, [softwareTitle, appContext]);
|
||||
};
|
||||
@@ -112,16 +112,11 @@ export interface ISoftwarePackage {
|
||||
install_during_setup?: boolean;
|
||||
labels_include_any: ILabelSoftwareTitle[] | null;
|
||||
labels_exclude_any: ILabelSoftwareTitle[] | null;
|
||||
categories?: SoftwareCategory[];
|
||||
categories?: SoftwareCategory[] | null;
|
||||
fleet_maintained_app_id?: number | null;
|
||||
hash_sha256?: string | null;
|
||||
}
|
||||
|
||||
export const isSoftwarePackage = (
|
||||
data: ISoftwarePackage | IAppStoreApp
|
||||
): data is ISoftwarePackage =>
|
||||
(data as ISoftwarePackage).install_script !== undefined;
|
||||
|
||||
export interface IAppStoreApp {
|
||||
name: string;
|
||||
/** Not included in SoftwareTitle software.app_store_app response, hoisted up one level
|
||||
@@ -146,9 +141,21 @@ export interface IAppStoreApp {
|
||||
version?: string;
|
||||
labels_include_any: ILabelSoftwareTitle[] | null;
|
||||
labels_exclude_any: ILabelSoftwareTitle[] | null;
|
||||
categories?: SoftwareCategory[];
|
||||
categories?: SoftwareCategory[] | null;
|
||||
configuration?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* package: includes FMA, custom packages, and are defined under software_package
|
||||
* app-store: includes VPP, Google Play Store apps and are defined under app_store_app
|
||||
*/
|
||||
export type InstallerType = "package" | "app-store";
|
||||
|
||||
export const isSoftwarePackage = (
|
||||
data: ISoftwarePackage | IAppStoreApp
|
||||
): data is ISoftwarePackage =>
|
||||
(data as ISoftwarePackage).install_script !== undefined;
|
||||
|
||||
export interface ISoftwareTitle {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -514,7 +521,7 @@ export interface IHostSoftwarePackage {
|
||||
version: string;
|
||||
last_install: ISoftwareLastInstall | null;
|
||||
last_uninstall: ISoftwareLastUninstall | null;
|
||||
categories?: SoftwareCategory[];
|
||||
categories?: SoftwareCategory[] | null;
|
||||
automatic_install_policies?: ISoftwareInstallPolicy[] | null;
|
||||
platform?: Platform;
|
||||
}
|
||||
@@ -526,7 +533,7 @@ export interface IHostAppStoreApp {
|
||||
icon_url: string;
|
||||
version: string;
|
||||
last_install: IAppLastInstall | null;
|
||||
categories?: SoftwareCategory[];
|
||||
categories?: SoftwareCategory[] | null;
|
||||
automatic_install_policies?: ISoftwareInstallPolicy[] | null;
|
||||
}
|
||||
|
||||
@@ -795,7 +802,7 @@ export interface IFleetMaintainedAppDetails {
|
||||
url: string;
|
||||
slug: string;
|
||||
software_title_id?: number; // null unless the team already has the software added (as a Fleet-maintained app, App Store (app), or custom package)
|
||||
categories: SoftwareCategory[];
|
||||
categories: SoftwareCategory[] | null;
|
||||
}
|
||||
|
||||
export const ROLLING_ARCH_LINUX_NAMES = [
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ export interface IFormValidation {
|
||||
|
||||
interface IFleetAppDetailsFormProps {
|
||||
labels: ILabelSummary[] | null;
|
||||
categories?: SoftwareCategory[];
|
||||
categories?: SoftwareCategory[] | null;
|
||||
name: string;
|
||||
defaultInstallScript: string;
|
||||
defaultPostInstallScript: string;
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { InstallerType } from "interfaces/software";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
@@ -8,7 +10,7 @@ const baseClass = "save-changes-modal";
|
||||
export interface IConfirmSaveChangesModalProps {
|
||||
onSaveChanges: () => void;
|
||||
softwareInstallerName?: string;
|
||||
installerType: "package" | "app-store";
|
||||
installerType: InstallerType;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
import { createMockAppStoreAppAndroid } from "__mocks__/softwareMock";
|
||||
import softwareAPI from "services/entities/software";
|
||||
import EditConfigurationModal from "./EditConfigurationModal";
|
||||
|
||||
const softwareInstaller = createMockAppStoreAppAndroid();
|
||||
|
||||
const MOCK_PROPS = {
|
||||
softwareId: 123,
|
||||
teamId: 456,
|
||||
softwareInstaller,
|
||||
onExit: jest.fn(),
|
||||
refetchSoftwareTitle: jest.fn(),
|
||||
};
|
||||
|
||||
describe("EditConfigurationModal", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders modal title, configuration editor, help text, and save button", () => {
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
render(<EditConfigurationModal {...MOCK_PROPS} />);
|
||||
|
||||
expect(screen.getByText("Edit configuration")).toBeInTheDocument();
|
||||
|
||||
// Editor label
|
||||
expect(screen.getByText("Configuration")).toBeInTheDocument();
|
||||
|
||||
// Help text / learn more link
|
||||
expect(
|
||||
screen.getByText(/The Android app's configuration in JSON format/i)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Learn more")).toBeInTheDocument();
|
||||
|
||||
const save = screen.getByRole("button", { name: "Save" });
|
||||
expect(save).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the installer details widget with software name", () => {
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
render(<EditConfigurationModal {...MOCK_PROPS} />);
|
||||
|
||||
// InstallerDetailsWidget should show the software name somewhere
|
||||
expect(screen.getAllByText(softwareInstaller.name).length).toBeGreaterThan(
|
||||
0
|
||||
);
|
||||
|
||||
// CustomDetails is "Android" in your props
|
||||
expect(screen.getByText("Android")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("initializes the configuration editor with valid JSON (Save enabled)", async () => {
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
render(<EditConfigurationModal {...MOCK_PROPS} />);
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(saveButton).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("calls onExit handler when modal close is triggered via Escape key", async () => {
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
const { user } = render(<EditConfigurationModal {...MOCK_PROPS} />);
|
||||
|
||||
await user.keyboard("{Escape}");
|
||||
|
||||
expect(MOCK_PROPS.onExit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("disables Save button when configuration JSON is invalid and shows the error", async () => {
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
const { user } = render(<EditConfigurationModal {...MOCK_PROPS} />);
|
||||
|
||||
const configInput = screen.getByRole<HTMLTextAreaElement>("textbox", {
|
||||
name: "",
|
||||
});
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
|
||||
// Type some invalid JSON
|
||||
await user.clear(configInput);
|
||||
await user.type(configInput, "{{ invalid json");
|
||||
|
||||
// Error is rendered and Save disabled
|
||||
await waitFor(() => {
|
||||
expect(saveButton).toBeDisabled();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(/Expected property name or '}'/i)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("enables Save button when an empty object when configuration field is cleared", async () => {
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
const { user } = render(<EditConfigurationModal {...MOCK_PROPS} />);
|
||||
|
||||
const configInput = screen.getByRole<HTMLTextAreaElement>("textbox", {
|
||||
name: "",
|
||||
});
|
||||
const saveButton = screen.getByRole("button", { name: "Save" });
|
||||
|
||||
await user.clear(configInput);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { IAppStoreApp } from "interfaces/software";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import softwareAPI from "services/entities/software";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import ModalFooter from "components/ModalFooter";
|
||||
import Editor from "components/Editor";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
import InstallerDetailsWidget from "../SoftwareInstallerCard/InstallerDetailsWidget";
|
||||
import { getErrorMessage } from "./helpers";
|
||||
|
||||
const baseClass = "edit-configuration-modal";
|
||||
|
||||
// Used to surface error.message in UI of unknown error type
|
||||
type ErrorWithMessage = {
|
||||
message: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
|
||||
return (error as ErrorWithMessage).message !== undefined;
|
||||
};
|
||||
|
||||
export interface ISoftwareConfigurationFormData {
|
||||
configuration: string;
|
||||
}
|
||||
|
||||
interface EditConfigurationModal {
|
||||
softwareId: number;
|
||||
teamId: number;
|
||||
softwareInstaller: IAppStoreApp;
|
||||
refetchSoftwareTitle: () => void;
|
||||
onExit: () => void;
|
||||
}
|
||||
|
||||
const EditConfigurationModal = ({
|
||||
softwareInstaller,
|
||||
softwareId,
|
||||
teamId,
|
||||
refetchSoftwareTitle,
|
||||
onExit,
|
||||
}: EditConfigurationModal) => {
|
||||
const { renderFlash, renderMultiFlash } = useContext(NotificationContext);
|
||||
|
||||
const [isUpdatingConfiguration, setIsUpdatingConfiguration] = useState(false);
|
||||
const [canSaveForm, setCanSaveForm] = useState(true);
|
||||
const [jsonFormData, setJsonFormData] = useState<string>(
|
||||
JSON.stringify(softwareInstaller.configuration, null, "\t") || "{}"
|
||||
);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const validateForm = (curFormData: string) => {
|
||||
let error = null;
|
||||
|
||||
if (curFormData) {
|
||||
try {
|
||||
JSON.parse(curFormData);
|
||||
} catch (e: unknown) {
|
||||
if (isErrorWithMessage(e)) {
|
||||
error = e.message.toString();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
// Edit package API call
|
||||
const onEditConfiguration = async (
|
||||
evt: React.MouseEvent<HTMLFormElement>
|
||||
) => {
|
||||
setIsUpdatingConfiguration(true);
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
// Format for API
|
||||
const formDataToSubmit =
|
||||
jsonFormData === ""
|
||||
? { configuration: {} } // Send empty object if no keys are set
|
||||
: {
|
||||
configuration: (jsonFormData && JSON.parse(jsonFormData)) || null,
|
||||
};
|
||||
try {
|
||||
await softwareAPI.editAppStoreApp(softwareId, teamId, formDataToSubmit);
|
||||
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
<strong>{softwareInstaller.name}</strong> configuration updated.
|
||||
</>
|
||||
);
|
||||
|
||||
refetchSoftwareTitle();
|
||||
onExit();
|
||||
} catch (e) {
|
||||
renderFlash(
|
||||
"error",
|
||||
getErrorMessage(e, softwareInstaller as IAppStoreApp)
|
||||
);
|
||||
}
|
||||
setIsUpdatingConfiguration(false);
|
||||
};
|
||||
|
||||
const onInputChange = (value: string) => {
|
||||
setJsonFormData(value);
|
||||
|
||||
const error = validateForm(value);
|
||||
setFormError(error);
|
||||
setCanSaveForm(!error);
|
||||
};
|
||||
|
||||
const renderHelpText = () => {
|
||||
return (
|
||||
<div className={`${baseClass}__help-text`}>
|
||||
The Android app's configuration in JSON format.{" "}
|
||||
<CustomLink
|
||||
newTab
|
||||
text="Learn more"
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/ui-gitops-mode`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderForm = () => (
|
||||
<>
|
||||
<Editor
|
||||
mode="json"
|
||||
value={jsonFormData as string}
|
||||
helpText={renderHelpText()}
|
||||
onChange={onInputChange}
|
||||
error={formError}
|
||||
label="Configuration"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal className={baseClass} title="Edit configuration" onExit={onExit}>
|
||||
<>
|
||||
<InstallerDetailsWidget
|
||||
softwareName={softwareInstaller.name}
|
||||
androidPlayStoreId={softwareInstaller.app_store_id}
|
||||
customDetails="Android"
|
||||
installerType="app-store"
|
||||
isFma={false}
|
||||
isScriptPackage={false}
|
||||
/>
|
||||
{renderForm()}
|
||||
<ModalFooter
|
||||
primaryButtons={
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={onEditConfiguration}
|
||||
isLoading={isUpdatingConfiguration}
|
||||
disabled={!canSaveForm || isUpdatingConfiguration}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditConfigurationModal;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.edit-configuration-modal {
|
||||
.modal__content {
|
||||
@include vertical-modal-layout;
|
||||
}
|
||||
|
||||
.modal-footer__content-wrapper {
|
||||
padding-top: $pad-large;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import { isAxiosError } from "axios";
|
||||
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
import { IAppStoreApp, ISoftwarePackage } from "interfaces/software";
|
||||
|
||||
import { generateSecretErrMsg } from "pages/SoftwarePage/helpers";
|
||||
|
||||
const DEFAULT_ERROR_MESSAGE =
|
||||
"Couldn't update configuration. Please try again.";
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getErrorMessage = (err: unknown, software: IAppStoreApp) => {
|
||||
const reason = getErrorReason(err);
|
||||
|
||||
if (
|
||||
reason.includes("managedConfiguration") ||
|
||||
reason.includes("workProfileWidgets")
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
Couldn't update configuration. Only
|
||||
"managedConfiguration" and "workProfileWidgets" are
|
||||
supported as top-level keys.
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return reason || DEFAULT_ERROR_MESSAGE;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./EditConfigurationModal";
|
||||
+2
-1
@@ -5,6 +5,7 @@ import {
|
||||
createMockSoftwarePackage,
|
||||
createMockSoftwareTitle,
|
||||
} from "__mocks__/softwareMock";
|
||||
import { InstallerType } from "interfaces/software";
|
||||
import softwareAPI from "services/entities/software";
|
||||
import EditIconModal from "./EditIconModal";
|
||||
|
||||
@@ -18,7 +19,7 @@ const MOCK_PROPS = {
|
||||
refetchSoftwareTitle: jest.fn(),
|
||||
iconUploadedAt: "2025-09-03T12:00:00Z",
|
||||
setIconUploadedAt: jest.fn(),
|
||||
installerType: "package" as "package" | "vpp",
|
||||
installerType: "package" as InstallerType,
|
||||
previewInfo: {
|
||||
type: "apps",
|
||||
versions: software.versions?.length,
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import {
|
||||
IAppStoreApp,
|
||||
isIpadOrIphoneSoftwareSource,
|
||||
ISoftwarePackage,
|
||||
InstallerType,
|
||||
} from "interfaces/software";
|
||||
import { IInputFieldParseTarget } from "interfaces/form_field";
|
||||
|
||||
@@ -124,7 +125,7 @@ interface IEditIconModalProps {
|
||||
iconUploadedAt: string;
|
||||
/** Updates the icon upload timestamp, triggering UI refetches to ensure a new custom icon appears called after successful icon update. */
|
||||
setIconUploadedAt: (timestamp: string) => void;
|
||||
installerType: "package" | "vpp";
|
||||
installerType: InstallerType;
|
||||
previewInfo: {
|
||||
type?: string;
|
||||
versions?: number;
|
||||
|
||||
+33
-22
@@ -8,8 +8,10 @@ import {
|
||||
IAppStoreApp,
|
||||
ISoftwarePackage,
|
||||
isSoftwarePackage,
|
||||
InstallerType,
|
||||
} from "interfaces/software";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { AppContext } from "context/app";
|
||||
import softwareAPI, {
|
||||
MAX_FILE_SIZE_BYTES,
|
||||
MAX_FILE_SIZE_MB,
|
||||
@@ -46,12 +48,11 @@ export type IEditPackageFormData = Omit<IPackageFormData, "installType">;
|
||||
interface IEditSoftwareModalProps {
|
||||
softwareId: number;
|
||||
teamId: number;
|
||||
software: ISoftwarePackage | IAppStoreApp;
|
||||
softwareInstaller: ISoftwarePackage | IAppStoreApp;
|
||||
refetchSoftwareTitle: () => void;
|
||||
onExit: () => void;
|
||||
installerType: "package" | "app-store";
|
||||
installerType: InstallerType;
|
||||
router: InjectedRouter;
|
||||
gitOpsModeEnabled?: boolean;
|
||||
openViewYamlModal: () => void;
|
||||
isIosOrIpadosApp?: boolean;
|
||||
}
|
||||
@@ -59,16 +60,18 @@ interface IEditSoftwareModalProps {
|
||||
const EditSoftwareModal = ({
|
||||
softwareId,
|
||||
teamId,
|
||||
software,
|
||||
softwareInstaller,
|
||||
onExit,
|
||||
refetchSoftwareTitle,
|
||||
installerType,
|
||||
router,
|
||||
gitOpsModeEnabled = false,
|
||||
openViewYamlModal,
|
||||
isIosOrIpadosApp = false,
|
||||
}: IEditSoftwareModalProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const gitOpsModeEnabled = config?.gitops.gitops_mode_enabled || false;
|
||||
|
||||
const [editSoftwareModalClasses, setEditSoftwareModalClasses] = useState(
|
||||
baseClass
|
||||
@@ -180,7 +183,7 @@ const EditSoftwareModal = ({
|
||||
try {
|
||||
await softwareAPI.editSoftwarePackage({
|
||||
data: formData,
|
||||
orignalPackage: software as ISoftwarePackage,
|
||||
orignalPackage: softwareInstaller as ISoftwarePackage,
|
||||
softwareId,
|
||||
teamId,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
@@ -192,8 +195,8 @@ const EditSoftwareModal = ({
|
||||
});
|
||||
|
||||
if (
|
||||
isSoftwarePackage(software) &&
|
||||
software.title_id &&
|
||||
isSoftwarePackage(softwareInstaller) &&
|
||||
softwareInstaller.title_id &&
|
||||
gitOpsModeEnabled
|
||||
) {
|
||||
// No longer flash message, we open YAML modal if editing with gitOpsModeEnabled
|
||||
@@ -212,7 +215,10 @@ const EditSoftwareModal = ({
|
||||
refetchSoftwareTitle();
|
||||
onExit();
|
||||
} catch (e) {
|
||||
renderFlash("error", getErrorMessage(e, software as IAppStoreApp));
|
||||
renderFlash(
|
||||
"error",
|
||||
getErrorMessage(e, softwareInstaller as IAppStoreApp)
|
||||
);
|
||||
}
|
||||
setIsUpdatingSoftware(false);
|
||||
};
|
||||
@@ -222,7 +228,7 @@ const EditSoftwareModal = ({
|
||||
};
|
||||
|
||||
const onClickSavePackage = (formData: IPackageFormData) => {
|
||||
const softwarePackage = software as ISoftwarePackage;
|
||||
const softwarePackage = softwareInstaller as ISoftwarePackage;
|
||||
|
||||
const currentData = {
|
||||
software: null,
|
||||
@@ -258,7 +264,7 @@ const EditSoftwareModal = ({
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
Successfully edited <b>{software.name}</b>.
|
||||
Successfully edited <b>{softwareInstaller.name}</b>.
|
||||
{formData.selfService
|
||||
? " The end user can install from Fleet Desktop."
|
||||
: ""}
|
||||
@@ -267,18 +273,21 @@ const EditSoftwareModal = ({
|
||||
onExit();
|
||||
refetchSoftwareTitle();
|
||||
} catch (e) {
|
||||
renderFlash("error", getErrorMessage(e, software as IAppStoreApp));
|
||||
renderFlash(
|
||||
"error",
|
||||
getErrorMessage(e, softwareInstaller as IAppStoreApp)
|
||||
);
|
||||
}
|
||||
setIsUpdatingSoftware(false);
|
||||
};
|
||||
|
||||
const onClickSaveVpp = async (formData: ISoftwareVppFormData) => {
|
||||
const currentData = {
|
||||
selfService: software.self_service || false,
|
||||
automaticInstall: software.automatic_install || false,
|
||||
targetType: getTargetType(software),
|
||||
customTarget: getCustomTarget(software),
|
||||
labelTargets: generateSelectedLabels(software),
|
||||
selfService: softwareInstaller.self_service || false,
|
||||
automaticInstall: softwareInstaller.automatic_install || false,
|
||||
targetType: getTargetType(softwareInstaller),
|
||||
customTarget: getCustomTarget(softwareInstaller),
|
||||
labelTargets: generateSelectedLabels(softwareInstaller),
|
||||
};
|
||||
|
||||
setPendingVppUpdates(formData);
|
||||
@@ -302,7 +311,7 @@ const EditSoftwareModal = ({
|
||||
|
||||
const renderForm = () => {
|
||||
if (installerType === "package") {
|
||||
const softwarePackage = software as ISoftwarePackage;
|
||||
const softwarePackage = softwareInstaller as ISoftwarePackage;
|
||||
return (
|
||||
<PackageForm
|
||||
labels={labels || []}
|
||||
@@ -311,7 +320,7 @@ const EditSoftwareModal = ({
|
||||
onCancel={onExit}
|
||||
onSubmit={onClickSavePackage}
|
||||
onClickPreviewEndUserExperience={togglePreviewEndUserExperienceModal}
|
||||
defaultSoftware={software}
|
||||
defaultSoftware={softwareInstaller}
|
||||
defaultInstallScript={softwarePackage.install_script}
|
||||
defaultPreInstallQuery={softwarePackage.pre_install_query}
|
||||
defaultPostInstallScript={softwarePackage.post_install_script}
|
||||
@@ -325,7 +334,7 @@ const EditSoftwareModal = ({
|
||||
return (
|
||||
<SoftwareVppForm
|
||||
labels={labels || []}
|
||||
softwareVppForEdit={software as IAppStoreApp}
|
||||
softwareVppForEdit={softwareInstaller as IAppStoreApp}
|
||||
onSubmit={onClickSaveVpp}
|
||||
onCancel={onExit}
|
||||
isLoading={isUpdatingSoftware}
|
||||
@@ -338,7 +347,9 @@ const EditSoftwareModal = ({
|
||||
<>
|
||||
<Modal
|
||||
className={editSoftwareModalClasses}
|
||||
title={isSoftwarePackage(software) ? "Edit package" : "Edit app"}
|
||||
title={
|
||||
isSoftwarePackage(softwareInstaller) ? "Edit package" : "Edit app"
|
||||
}
|
||||
onExit={onExit}
|
||||
width="large"
|
||||
>
|
||||
@@ -347,7 +358,7 @@ const EditSoftwareModal = ({
|
||||
{showConfirmSaveChangesModal && (
|
||||
<ConfirmSaveChangesModal
|
||||
onClose={toggleConfirmSaveChangesModal}
|
||||
softwareInstallerName={software?.name}
|
||||
softwareInstallerName={softwareInstaller?.name}
|
||||
installerType={installerType}
|
||||
onSaveChanges={onClickConfirmChanges}
|
||||
/>
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
import { InstallerType } from "interfaces/software";
|
||||
|
||||
import InstallerDetailsWidget from "./InstallerDetailsWidget";
|
||||
|
||||
@@ -19,7 +20,7 @@ const render = createCustomRenderer({ withBackendMock: true });
|
||||
describe("InstallerDetailsWidget", () => {
|
||||
const defaultProps = {
|
||||
softwareName: "Test Software",
|
||||
installerType: "package" as const,
|
||||
installerType: "package" as InstallerType,
|
||||
addedTimestamp: "2024-05-06T10:00:00Z",
|
||||
version: "v1.2.3",
|
||||
isFma: false,
|
||||
|
||||
+8
-1
@@ -9,6 +9,7 @@ import { internationalTimeFormat } from "utilities/helpers";
|
||||
import { addedFromNow } from "utilities/date_format";
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
import { useCheckTruncatedElement } from "hooks/useCheckTruncatedElement";
|
||||
import { InstallerType } from "interfaces/software";
|
||||
|
||||
import Graphic from "components/Graphic";
|
||||
import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon";
|
||||
@@ -60,13 +61,14 @@ const renderInstallerDisplayText = (
|
||||
interface IInstallerDetailsWidgetProps {
|
||||
className?: string;
|
||||
softwareName: string;
|
||||
installerType: "package" | "app-store";
|
||||
installerType: InstallerType;
|
||||
addedTimestamp?: string;
|
||||
version?: string | null;
|
||||
sha256?: string | null;
|
||||
isFma: boolean;
|
||||
isScriptPackage: boolean;
|
||||
androidPlayStoreId?: string;
|
||||
customDetails?: string;
|
||||
}
|
||||
|
||||
const InstallerDetailsWidget = ({
|
||||
@@ -79,6 +81,7 @@ const InstallerDetailsWidget = ({
|
||||
isFma,
|
||||
isScriptPackage,
|
||||
androidPlayStoreId,
|
||||
customDetails,
|
||||
}: IInstallerDetailsWidgetProps) => {
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
||||
@@ -108,6 +111,10 @@ const InstallerDetailsWidget = ({
|
||||
};
|
||||
|
||||
const renderDetails = () => {
|
||||
if (customDetails) {
|
||||
return <>{customDetails}</>;
|
||||
}
|
||||
|
||||
const renderVersionInfo = () => {
|
||||
if (isScriptPackage) {
|
||||
return null;
|
||||
|
||||
+49
-102
@@ -5,14 +5,15 @@ import { InjectedRouter } from "react-router";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { isAndroid } from "interfaces/platform";
|
||||
import {
|
||||
ISoftwareTitleDetails,
|
||||
ISoftwarePackage,
|
||||
IAppStoreApp,
|
||||
isSoftwarePackage,
|
||||
InstallerType,
|
||||
} from "interfaces/software";
|
||||
import softwareAPI from "services/entities/software";
|
||||
|
||||
import { useSoftwareInstaller } from "hooks/useSoftwareInstallerMeta";
|
||||
|
||||
import { getSelfServiceTooltip } from "pages/SoftwarePage/helpers";
|
||||
|
||||
import Card from "components/Card";
|
||||
@@ -28,7 +29,6 @@ import CustomLink from "components/CustomLink";
|
||||
import InstallerDetailsWidget from "pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/InstallerDetailsWidget";
|
||||
|
||||
import DeleteSoftwareModal from "../DeleteSoftwareModal";
|
||||
import EditSoftwareModal from "../EditSoftwareModal";
|
||||
import ViewYamlModal from "../ViewYamlModal";
|
||||
|
||||
import {
|
||||
@@ -42,17 +42,10 @@ import InstallerPoliciesTable from "./InstallerPoliciesTable";
|
||||
|
||||
const baseClass = "software-installer-card";
|
||||
|
||||
interface IStatusDisplayOption {
|
||||
displayName: string;
|
||||
iconName: "success" | "pending-outline" | "error";
|
||||
tooltip: React.ReactNode;
|
||||
}
|
||||
|
||||
interface IActionsDropdownProps {
|
||||
installerType: "package" | "app-store";
|
||||
installerType: InstallerType;
|
||||
onDownloadClick: () => void;
|
||||
onDeleteClick: () => void;
|
||||
onEditSoftwareClick: () => void;
|
||||
gitOpsModeEnabled?: boolean;
|
||||
repoURL?: string;
|
||||
isFMA?: boolean;
|
||||
@@ -63,7 +56,6 @@ export const SoftwareActionButtons = ({
|
||||
installerType,
|
||||
onDownloadClick,
|
||||
onDeleteClick,
|
||||
onEditSoftwareClick,
|
||||
gitOpsModeEnabled,
|
||||
repoURL,
|
||||
isFMA,
|
||||
@@ -96,11 +88,10 @@ export const SoftwareActionButtons = ({
|
||||
</>
|
||||
);
|
||||
options = options.map((option) => {
|
||||
// edit is disabled in gitOpsMode for VPP only
|
||||
// delete is disabled in gitOpsMode for software types that can't be added in GitOps mode (FMA, VPP)
|
||||
if (
|
||||
(option.value === "edit" && installerType === "app-store") ||
|
||||
(option.value === "delete" && (installerType === "app-store" || isFMA))
|
||||
option.value === "delete" &&
|
||||
(installerType === "app-store" || isFMA)
|
||||
) {
|
||||
return {
|
||||
...option,
|
||||
@@ -116,7 +107,6 @@ export const SoftwareActionButtons = ({
|
||||
const actionHandlers = {
|
||||
download: onDownloadClick,
|
||||
delete: onDeleteClick,
|
||||
edit: onEditSoftwareClick,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -154,106 +144,82 @@ export const SoftwareActionButtons = ({
|
||||
};
|
||||
|
||||
interface ISoftwareInstallerCardProps {
|
||||
softwareTitleName: string;
|
||||
softwareDisplayName: string;
|
||||
isScriptPackage?: boolean;
|
||||
isIosOrIpadosApp?: boolean;
|
||||
name: string;
|
||||
version: string | null;
|
||||
addedTimestamp: string;
|
||||
status: {
|
||||
installed: number;
|
||||
pending: number;
|
||||
failed: number;
|
||||
};
|
||||
isSelfService: boolean;
|
||||
softwareId: number;
|
||||
iconUrl?: string | null;
|
||||
displayName?: string;
|
||||
teamId: number;
|
||||
teamIdForApi?: number;
|
||||
softwareInstaller: ISoftwarePackage | IAppStoreApp;
|
||||
onDelete: () => void;
|
||||
refetchSoftwareTitle: () => void;
|
||||
isLoading: boolean;
|
||||
router: InjectedRouter;
|
||||
gitOpsYamlParam?: boolean;
|
||||
onToggleViewYaml: () => void;
|
||||
showViewYamlModal: boolean;
|
||||
softwareTitle: ISoftwareTitleDetails;
|
||||
}
|
||||
|
||||
// NOTE: This component is dependent on having either a software package
|
||||
// (ISoftwarePackage) or an app store app (IAppStoreApp). If we add more types
|
||||
// of packages we should consider refactoring this to be more dynamic.
|
||||
const SoftwareInstallerCard = ({
|
||||
softwareTitleName,
|
||||
softwareDisplayName,
|
||||
isScriptPackage = false,
|
||||
isIosOrIpadosApp = false,
|
||||
name,
|
||||
version,
|
||||
addedTimestamp,
|
||||
status,
|
||||
isSelfService,
|
||||
softwareInstaller,
|
||||
softwareId,
|
||||
iconUrl,
|
||||
displayName,
|
||||
teamId,
|
||||
teamIdForApi,
|
||||
onDelete,
|
||||
refetchSoftwareTitle,
|
||||
isLoading,
|
||||
router,
|
||||
gitOpsYamlParam = false,
|
||||
onToggleViewYaml,
|
||||
showViewYamlModal,
|
||||
softwareTitle,
|
||||
}: ISoftwareInstallerCardProps) => {
|
||||
const installerType = isSoftwarePackage(softwareInstaller)
|
||||
? "package"
|
||||
: "app-store";
|
||||
const isAndroidPlayStoreApp =
|
||||
"platform" in softwareInstaller && isAndroid(softwareInstaller.platform);
|
||||
const isFleetMaintainedApp =
|
||||
"fleet_maintained_app_id" in softwareInstaller &&
|
||||
!!softwareInstaller.fleet_maintained_app_id;
|
||||
const isCustomPackage = installerType === "package" && !isFleetMaintainedApp;
|
||||
const sha256 =
|
||||
"hash_sha256" in softwareInstaller
|
||||
? softwareInstaller.hash_sha256
|
||||
: undefined;
|
||||
const softwareInstallerMetaData = useSoftwareInstaller(softwareTitle);
|
||||
|
||||
if (!softwareInstallerMetaData) {
|
||||
// This should never happen for SoftwareInstallerCard; fail fast in dev.
|
||||
throw new Error(
|
||||
"useSoftwareInstaller: called with a softwareTitle that has no installer"
|
||||
);
|
||||
}
|
||||
|
||||
const { cardInfo, meta: softwareInstallerMeta } = softwareInstallerMetaData;
|
||||
|
||||
const {
|
||||
automatic_install_policies: automaticInstallPolicies,
|
||||
} = softwareInstaller;
|
||||
softwareTitleName,
|
||||
softwareDisplayName,
|
||||
softwareInstaller,
|
||||
name,
|
||||
version,
|
||||
addedTimestamp,
|
||||
status,
|
||||
iconUrl,
|
||||
displayName,
|
||||
isSelfService,
|
||||
isScriptPackage,
|
||||
} = cardInfo;
|
||||
|
||||
const {
|
||||
installerType,
|
||||
isAndroidPlayStoreApp,
|
||||
isFleetMaintainedApp,
|
||||
isCustomPackage,
|
||||
isIosOrIpadosApp,
|
||||
sha256,
|
||||
androidPlayStoreId,
|
||||
automaticInstallPolicies,
|
||||
gitOpsModeEnabled,
|
||||
repoURL,
|
||||
} = softwareInstallerMeta;
|
||||
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isTeamAdmin,
|
||||
isTeamMaintainer,
|
||||
config,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const { gitops_mode_enabled: gitOpsModeEnabled, repository_url: repoURL } =
|
||||
config?.gitops || {};
|
||||
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
// gitOpsYamlParam URL Param controls whether the View Yaml modal is opened on page load
|
||||
// as it automatically opens from adding flow of custom software in gitOps mode
|
||||
const [showViewYamlModal, setShowViewYamlModal] = useState(gitOpsYamlParam);
|
||||
const [showEditSoftwareModal, setShowEditSoftwareModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
const onEditSoftwareClick = () => {
|
||||
setShowEditSoftwareModal(true);
|
||||
};
|
||||
|
||||
const onDeleteClick = () => {
|
||||
setShowDeleteModal(true);
|
||||
};
|
||||
|
||||
const onToggleViewYaml = () => {
|
||||
setShowViewYamlModal(!showViewYamlModal);
|
||||
};
|
||||
|
||||
const onDeleteSuccess = useCallback(() => {
|
||||
setShowDeleteModal(false);
|
||||
onDelete();
|
||||
@@ -296,11 +262,7 @@ const SoftwareInstallerCard = ({
|
||||
sha256={sha256}
|
||||
isFma={isFleetMaintainedApp}
|
||||
isScriptPackage={isScriptPackage}
|
||||
androidPlayStoreId={
|
||||
isAndroidPlayStoreApp
|
||||
? softwareInstaller?.app_store_id
|
||||
: undefined
|
||||
}
|
||||
androidPlayStoreId={androidPlayStoreId}
|
||||
/>
|
||||
<div className={`${baseClass}__tags-wrapper`}>
|
||||
{Array.isArray(automaticInstallPolicies) &&
|
||||
@@ -339,7 +301,6 @@ const SoftwareInstallerCard = ({
|
||||
installerType={installerType}
|
||||
onDownloadClick={onDownloadClick}
|
||||
onDeleteClick={onDeleteClick}
|
||||
onEditSoftwareClick={onEditSoftwareClick}
|
||||
gitOpsModeEnabled={gitOpsModeEnabled}
|
||||
repoURL={repoURL}
|
||||
isFMA={isFleetMaintainedApp}
|
||||
@@ -374,20 +335,6 @@ const SoftwareInstallerCard = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showEditSoftwareModal && (
|
||||
<EditSoftwareModal
|
||||
router={router}
|
||||
gitOpsModeEnabled={gitOpsModeEnabled}
|
||||
softwareId={softwareId}
|
||||
teamId={teamId}
|
||||
software={softwareInstaller}
|
||||
onExit={() => setShowEditSoftwareModal(false)}
|
||||
refetchSoftwareTitle={refetchSoftwareTitle}
|
||||
installerType={installerType}
|
||||
openViewYamlModal={onToggleViewYaml}
|
||||
isIosOrIpadosApp={isIosOrIpadosApp}
|
||||
/>
|
||||
)}
|
||||
{showDeleteModal && (
|
||||
<DeleteSoftwareModal
|
||||
gitOpsModeEnabled={gitOpsModeEnabled}
|
||||
|
||||
+1
-10
@@ -14,11 +14,6 @@ const DOWNLOAD_OPTION: ISoftwareOption = {
|
||||
iconName: "download",
|
||||
};
|
||||
|
||||
const EDIT_OPTION: ISoftwareOption = {
|
||||
value: "edit",
|
||||
disabled: false,
|
||||
iconName: "pencil",
|
||||
};
|
||||
const DELETE_OPTION: ISoftwareOption = {
|
||||
value: "delete",
|
||||
disabled: false,
|
||||
@@ -27,14 +22,10 @@ const DELETE_OPTION: ISoftwareOption = {
|
||||
|
||||
export const SOFTWARE_PACKAGE_ACTION_OPTIONS = [
|
||||
DOWNLOAD_OPTION,
|
||||
EDIT_OPTION,
|
||||
DELETE_OPTION,
|
||||
] as const;
|
||||
|
||||
export const APP_STORE_APP_ACTION_OPTIONS = [
|
||||
EDIT_OPTION,
|
||||
DELETE_OPTION,
|
||||
] as const;
|
||||
export const APP_STORE_APP_ACTION_OPTIONS = [DELETE_OPTION] as const;
|
||||
|
||||
export const ANDROID_PLAY_STORE_APP_ACTION_OPTIONS = [DELETE_OPTION] as const;
|
||||
|
||||
|
||||
+121
-51
@@ -1,105 +1,155 @@
|
||||
/** software/titles/:id > First section */
|
||||
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AppContext } from "context/app";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { InjectedRouter } from "react-router";
|
||||
|
||||
import { useSoftwareInstaller } from "hooks/useSoftwareInstallerMeta";
|
||||
import {
|
||||
formatSoftwareType,
|
||||
isIpadOrIphoneSoftwareSource,
|
||||
ISoftwareTitleDetails,
|
||||
isSoftwarePackage,
|
||||
ISoftwarePackage,
|
||||
IAppStoreApp,
|
||||
NO_VERSION_OR_HOST_DATA_SOURCES,
|
||||
IAppStoreApp,
|
||||
} from "interfaces/software";
|
||||
|
||||
import Card from "components/Card";
|
||||
import SoftwareDetailsSummary from "pages/SoftwarePage/components/cards/SoftwareDetailsSummary";
|
||||
import TitleVersionsTable from "./TitleVersionsTable";
|
||||
import EditIconModal from "../EditIconModal";
|
||||
import EditSoftwareModal from "../EditSoftwareModal";
|
||||
import EditConfigurationModal from "../EditConfigurationModal";
|
||||
|
||||
interface ISoftwareSummaryCard {
|
||||
title: ISoftwareTitleDetails;
|
||||
softwareTitle: ISoftwareTitleDetails;
|
||||
softwareId: number;
|
||||
teamId?: number;
|
||||
isAvailableForInstall?: boolean;
|
||||
isLoading?: boolean;
|
||||
router: InjectedRouter;
|
||||
refetchSoftwareTitle: () => void;
|
||||
softwareInstaller?: ISoftwarePackage | IAppStoreApp;
|
||||
onToggleViewYaml: () => void;
|
||||
}
|
||||
|
||||
const baseClass = "software-summary-card";
|
||||
|
||||
const SoftwareSummaryCard = ({
|
||||
teamId,
|
||||
softwareTitle,
|
||||
softwareId,
|
||||
teamId,
|
||||
isAvailableForInstall,
|
||||
title,
|
||||
isLoading = false,
|
||||
router,
|
||||
softwareInstaller,
|
||||
refetchSoftwareTitle,
|
||||
onToggleViewYaml,
|
||||
}: ISoftwareSummaryCard) => {
|
||||
const { source } = title;
|
||||
|
||||
const {
|
||||
isGlobalAdmin,
|
||||
isGlobalMaintainer,
|
||||
isTeamMaintainerOrTeamAdmin,
|
||||
} = useContext(AppContext);
|
||||
const installerResult = useSoftwareInstaller(softwareTitle);
|
||||
|
||||
const [iconUploadedAt, setIconUploadedAt] = useState("");
|
||||
const [showEditIconModal, setShowEditIconModal] = useState(false);
|
||||
const [showEditSoftwareModal, setShowEditSoftwareModal] = useState(false);
|
||||
const [showEditConfigurationModal, setShowEditConfigurationModal] = useState(
|
||||
false
|
||||
);
|
||||
|
||||
// Hide versions table for tgz_packages, sh_packages, & ps1_packages and when no hosts have the
|
||||
// software installed
|
||||
const showVersionsTable =
|
||||
!!title.hosts_count && !NO_VERSION_OR_HOST_DATA_SOURCES.includes(source);
|
||||
!!softwareTitle.hosts_count &&
|
||||
!NO_VERSION_OR_HOST_DATA_SOURCES.includes(softwareTitle.source);
|
||||
|
||||
const hasEditPermissions =
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamMaintainerOrTeamAdmin;
|
||||
const canEditIcon =
|
||||
softwareInstaller &&
|
||||
typeof teamId === "number" &&
|
||||
teamId >= 0 &&
|
||||
hasEditPermissions;
|
||||
// If there is no installer (no package/app), bail out of installer‑related UI.
|
||||
if (!installerResult) {
|
||||
// when no installer, no edit actions:
|
||||
return (
|
||||
<>
|
||||
<Card borderRadiusSize="xxlarge" className={baseClass}>
|
||||
<SoftwareDetailsSummary
|
||||
displayName={softwareTitle.display_name || softwareTitle.name}
|
||||
type={formatSoftwareType(softwareTitle)}
|
||||
versions={softwareTitle.versions?.length ?? 0}
|
||||
hostCount={softwareTitle.hosts_count}
|
||||
countsUpdatedAt={softwareTitle.counts_updated_at}
|
||||
queryParams={{ software_title_id: softwareId, team_id: teamId }}
|
||||
name={softwareTitle.name}
|
||||
source={softwareTitle.source}
|
||||
iconUrl={softwareTitle.icon_url}
|
||||
iconUploadedAt={iconUploadedAt}
|
||||
/>
|
||||
{showVersionsTable && (
|
||||
<TitleVersionsTable
|
||||
router={router}
|
||||
data={softwareTitle.versions ?? []}
|
||||
isLoading={isLoading}
|
||||
teamIdForApi={teamId}
|
||||
isIPadOSOrIOSApp={isIpadOrIphoneSoftwareSource(
|
||||
softwareTitle.source
|
||||
)}
|
||||
isAvailableForInstall={isAvailableForInstall}
|
||||
countsUpdatedAt={softwareTitle.counts_updated_at}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const [showEditIconModal, setShowEditIconModal] = useState(false);
|
||||
const { meta } = installerResult;
|
||||
const {
|
||||
softwareInstaller,
|
||||
installerType,
|
||||
isIosOrIpadosApp,
|
||||
isAndroidPlayStoreApp,
|
||||
canManageSoftware,
|
||||
} = meta;
|
||||
|
||||
const onClickEditIcon = () => {
|
||||
setShowEditIconModal(!showEditIconModal);
|
||||
};
|
||||
const canEditAppearance = canManageSoftware;
|
||||
|
||||
const canEditSoftware = canManageSoftware;
|
||||
|
||||
const canEditConfiguration = canManageSoftware && isAndroidPlayStoreApp;
|
||||
|
||||
const onClickEditAppearance = () => setShowEditIconModal(true);
|
||||
const onClickEditSoftware = () => setShowEditSoftwareModal(true);
|
||||
const onClickEditConfiguration = () => setShowEditConfigurationModal(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card borderRadiusSize="xxlarge" className={baseClass}>
|
||||
<SoftwareDetailsSummary
|
||||
displayName={title.display_name || title.name}
|
||||
type={formatSoftwareType(title)}
|
||||
versions={title.versions?.length ?? 0}
|
||||
hostCount={title.hosts_count}
|
||||
countsUpdatedAt={title.counts_updated_at}
|
||||
displayName={softwareTitle.display_name || softwareTitle.name}
|
||||
type={formatSoftwareType(softwareTitle)}
|
||||
versions={softwareTitle.versions?.length ?? 0}
|
||||
hostCount={softwareTitle.hosts_count}
|
||||
countsUpdatedAt={softwareTitle.counts_updated_at}
|
||||
queryParams={{
|
||||
software_title_id: softwareId,
|
||||
team_id: teamId,
|
||||
}}
|
||||
name={title.name}
|
||||
source={title.source}
|
||||
iconUrl={title.icon_url}
|
||||
name={softwareTitle.name}
|
||||
source={softwareTitle.source}
|
||||
iconUrl={softwareTitle.icon_url}
|
||||
iconUploadedAt={iconUploadedAt}
|
||||
onClickEditIcon={canEditIcon ? onClickEditIcon : undefined}
|
||||
canManageSoftware={canManageSoftware}
|
||||
onClickEditAppearance={
|
||||
canEditAppearance ? onClickEditAppearance : undefined
|
||||
}
|
||||
onClickEditSoftware={
|
||||
canEditSoftware ? onClickEditSoftware : undefined
|
||||
}
|
||||
onClickEditConfiguration={
|
||||
canEditConfiguration ? onClickEditConfiguration : undefined
|
||||
}
|
||||
/>
|
||||
{showVersionsTable && (
|
||||
<TitleVersionsTable
|
||||
router={router}
|
||||
data={title.versions ?? []}
|
||||
data={softwareTitle.versions ?? []}
|
||||
isLoading={isLoading}
|
||||
teamIdForApi={teamId}
|
||||
isIPadOSOrIOSApp={isIpadOrIphoneSoftwareSource(title.source)}
|
||||
isIPadOSOrIOSApp={isIosOrIpadosApp}
|
||||
isAvailableForInstall={isAvailableForInstall}
|
||||
countsUpdatedAt={title.counts_updated_at}
|
||||
countsUpdatedAt={softwareTitle.counts_updated_at}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
@@ -115,21 +165,41 @@ const SoftwareSummaryCard = ({
|
||||
refetchSoftwareTitle={refetchSoftwareTitle}
|
||||
iconUploadedAt={iconUploadedAt}
|
||||
setIconUploadedAt={setIconUploadedAt}
|
||||
installerType={
|
||||
isSoftwarePackage(softwareInstaller) ? "package" : "vpp"
|
||||
}
|
||||
installerType={installerType}
|
||||
previewInfo={{
|
||||
name: title.display_name || title.name,
|
||||
titleName: title.name,
|
||||
type: formatSoftwareType(title),
|
||||
source: title.source,
|
||||
currentIconUrl: title.icon_url,
|
||||
versions: title.versions?.length ?? 0,
|
||||
countsUpdatedAt: title.counts_updated_at,
|
||||
name: softwareTitle.display_name || softwareTitle.name,
|
||||
titleName: softwareTitle.name,
|
||||
type: formatSoftwareType(softwareTitle),
|
||||
source: softwareTitle.source,
|
||||
currentIconUrl: softwareTitle.icon_url,
|
||||
versions: softwareTitle.versions?.length ?? 0,
|
||||
countsUpdatedAt: softwareTitle.counts_updated_at,
|
||||
selfServiceVersion: softwareInstaller.version,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showEditSoftwareModal && softwareInstaller && teamId && (
|
||||
<EditSoftwareModal
|
||||
router={router}
|
||||
softwareId={softwareId}
|
||||
teamId={teamId}
|
||||
softwareInstaller={softwareInstaller}
|
||||
onExit={() => setShowEditSoftwareModal(false)}
|
||||
refetchSoftwareTitle={refetchSoftwareTitle}
|
||||
installerType={installerType}
|
||||
openViewYamlModal={onToggleViewYaml}
|
||||
isIosOrIpadosApp={isIosOrIpadosApp}
|
||||
/>
|
||||
)}
|
||||
{showEditConfigurationModal && softwareInstaller && teamId && (
|
||||
<EditConfigurationModal
|
||||
softwareInstaller={softwareInstaller as IAppStoreApp}
|
||||
softwareId={softwareId}
|
||||
teamId={teamId}
|
||||
refetchSoftwareTitle={refetchSoftwareTitle}
|
||||
onExit={() => setShowEditConfigurationModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** software/titles/:id */
|
||||
|
||||
import React, { useCallback, useContext } from "react";
|
||||
import React, { useCallback, useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
@@ -10,10 +10,7 @@ import paths from "router/paths";
|
||||
import useTeamIdParam from "hooks/useTeamIdParam";
|
||||
import { AppContext } from "context/app";
|
||||
import { ignoreAxiosError } from "interfaces/errors";
|
||||
import {
|
||||
ISoftwareTitleDetails,
|
||||
isIpadOrIphoneSoftwareSource,
|
||||
} from "interfaces/software";
|
||||
import { ISoftwareTitleDetails } from "interfaces/software";
|
||||
import {
|
||||
APP_CONTEXT_ALL_TEAMS_ID,
|
||||
APP_CONTEXT_NO_TEAM_ID,
|
||||
@@ -32,7 +29,6 @@ import TeamsHeader from "components/TeamsHeader";
|
||||
import DetailsNoHosts from "../components/cards/DetailsNoHosts";
|
||||
import SoftwareSummaryCard from "./SoftwareSummaryCard";
|
||||
import SoftwareInstallerCard from "./SoftwareInstallerCard";
|
||||
import { getInstallerCardInfo } from "./helpers";
|
||||
|
||||
const baseClass = "software-title-details-page";
|
||||
|
||||
@@ -77,6 +73,12 @@ const SoftwareTitleDetailsPage = ({
|
||||
includeNoTeam: true,
|
||||
});
|
||||
|
||||
// gitOpsYamlParam URL Param controls whether the View Yaml modal is opened on page load
|
||||
// as it automatically opens from adding flow of custom software in gitOps mode
|
||||
const [showViewYamlModal, setShowViewYamlModal] = useState(
|
||||
autoOpenGitOpsYamlModal || false
|
||||
);
|
||||
|
||||
const {
|
||||
data: softwareTitle,
|
||||
isLoading: isSoftwareTitleLoading,
|
||||
@@ -105,6 +107,10 @@ const SoftwareTitleDetailsPage = ({
|
||||
const isAvailableForInstall =
|
||||
!!softwareTitle?.software_package || !!softwareTitle?.app_store_app;
|
||||
|
||||
const onToggleViewYaml = () => {
|
||||
setShowViewYamlModal(!showViewYamlModal);
|
||||
};
|
||||
|
||||
const onDeleteInstaller = useCallback(() => {
|
||||
if (softwareTitle?.versions?.length) {
|
||||
refetchSoftwareTitle();
|
||||
@@ -140,43 +146,16 @@ const SoftwareTitleDetailsPage = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
softwareTitleName,
|
||||
softwareDisplayName,
|
||||
softwarePackage,
|
||||
name,
|
||||
version,
|
||||
addedTimestamp,
|
||||
status,
|
||||
isSelfService,
|
||||
isScriptPackage,
|
||||
source,
|
||||
} = getInstallerCardInfo(title);
|
||||
|
||||
const isIosOrIpadosApp = isIpadOrIphoneSoftwareSource(source);
|
||||
|
||||
return (
|
||||
<SoftwareInstallerCard
|
||||
softwareTitleName={softwareTitleName}
|
||||
softwareDisplayName={softwareDisplayName}
|
||||
isScriptPackage={isScriptPackage}
|
||||
isIosOrIpadosApp={isIosOrIpadosApp}
|
||||
softwareInstaller={softwarePackage}
|
||||
name={name}
|
||||
version={version}
|
||||
iconUrl={title.icon_url}
|
||||
displayName={title.display_name}
|
||||
addedTimestamp={addedTimestamp}
|
||||
status={status}
|
||||
isSelfService={isSelfService}
|
||||
softwareTitle={title}
|
||||
softwareId={softwareId}
|
||||
teamId={currentTeamId ?? APP_CONTEXT_NO_TEAM_ID}
|
||||
teamIdForApi={teamIdForApi}
|
||||
onDelete={onDeleteInstaller}
|
||||
refetchSoftwareTitle={refetchSoftwareTitle}
|
||||
isLoading={isSoftwareTitleLoading}
|
||||
router={router}
|
||||
gitOpsYamlParam={autoOpenGitOpsYamlModal}
|
||||
onToggleViewYaml={onToggleViewYaml}
|
||||
showViewYamlModal={showViewYamlModal}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -184,18 +163,14 @@ const SoftwareTitleDetailsPage = ({
|
||||
const renderSoftwareSummaryCard = (title: ISoftwareTitleDetails) => {
|
||||
return (
|
||||
<SoftwareSummaryCard
|
||||
title={title}
|
||||
softwareTitle={title}
|
||||
softwareId={softwareId}
|
||||
teamId={teamIdForApi}
|
||||
isAvailableForInstall={isAvailableForInstall}
|
||||
isLoading={isSoftwareTitleLoading}
|
||||
router={router}
|
||||
refetchSoftwareTitle={refetchSoftwareTitle}
|
||||
softwareInstaller={
|
||||
isAvailableForInstall
|
||||
? getInstallerCardInfo(title).softwarePackage
|
||||
: undefined
|
||||
}
|
||||
onToggleViewYaml={onToggleViewYaml}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,7 +37,9 @@ describe("SoftwareTitleDetailsPage helpers", () => {
|
||||
};
|
||||
const packageCardInfo = getInstallerCardInfo(softwareTitle);
|
||||
expect(packageCardInfo).toEqual({
|
||||
softwarePackage: softwareTitle.software_package,
|
||||
softwareInstaller: softwareTitle.software_package,
|
||||
displayName: undefined,
|
||||
iconUrl: "https://example.com/icon.png",
|
||||
name: "TestPackage.pkg", // packages should display the package name not the software title name
|
||||
softwareDisplayName: "Test Software",
|
||||
version: "1.0.0",
|
||||
@@ -83,9 +85,11 @@ describe("SoftwareTitleDetailsPage helpers", () => {
|
||||
};
|
||||
const packageCardInfo = getInstallerCardInfo(softwareTitle);
|
||||
expect(packageCardInfo).toEqual({
|
||||
softwarePackage: softwareTitle.app_store_app,
|
||||
softwareInstaller: softwareTitle.app_store_app,
|
||||
name: "Test Software", // apps should display the software title name (backend should ensure the app name and software title name match)
|
||||
softwareDisplayName: "Test App",
|
||||
displayName: "Test App",
|
||||
iconUrl: "https://example.com/icon.png",
|
||||
version: "1.0.1",
|
||||
addedTimestamp: "2020-01-01T00:00:00.000Z",
|
||||
softwareTitleName: "Test Software",
|
||||
|
||||
@@ -4,20 +4,32 @@ import {
|
||||
isSoftwarePackage,
|
||||
aggregateInstallStatusCounts,
|
||||
SCRIPT_PACKAGE_SOURCES,
|
||||
ISoftwarePackage,
|
||||
} from "interfaces/software";
|
||||
|
||||
/**
|
||||
* Generates the data needed to render the installer card. It differentiates between
|
||||
* software packages and app store apps and returns the appropriate data.
|
||||
*
|
||||
* FIXME: This function ought to be refactored or renamed to better reflect its purpose.
|
||||
* "PackageCard" is a bit ambiguous in this context (it refers to the card that displays
|
||||
* package or app information, as applicable).
|
||||
*/
|
||||
export interface InstallerCardInfo {
|
||||
softwareTitleName: string;
|
||||
softwareDisplayName: string;
|
||||
softwareInstaller: ISoftwarePackage | IAppStoreApp;
|
||||
name: string;
|
||||
version: string | null;
|
||||
source: ISoftwareTitleDetails["source"];
|
||||
addedTimestamp: string;
|
||||
status: {
|
||||
installed: number;
|
||||
pending: number;
|
||||
failed: number;
|
||||
};
|
||||
isSelfService: boolean;
|
||||
isScriptPackage: boolean;
|
||||
iconUrl?: string | null;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const getInstallerCardInfo = (softwareTitle: ISoftwareTitleDetails) => {
|
||||
// we know at this point that softwareTitle.software_package or
|
||||
// softwareTitle.app_store_app is not null so we will do a type assertion.
|
||||
export const getInstallerCardInfo = (
|
||||
softwareTitle: ISoftwareTitleDetails
|
||||
): InstallerCardInfo => {
|
||||
const installerData = softwareTitle.software_package
|
||||
? softwareTitle.software_package
|
||||
: (softwareTitle.app_store_app as IAppStoreApp);
|
||||
@@ -27,12 +39,14 @@ export const getInstallerCardInfo = (softwareTitle: ISoftwareTitleDetails) => {
|
||||
return {
|
||||
softwareTitleName: softwareTitle.name,
|
||||
softwareDisplayName: softwareTitle.display_name || softwareTitle.name,
|
||||
softwarePackage: installerData,
|
||||
softwareInstaller: installerData,
|
||||
name: (isPackage && installerData.name) || softwareTitle.name,
|
||||
version:
|
||||
(isPackage ? installerData.version : installerData.latest_version) ||
|
||||
null,
|
||||
source: softwareTitle.source,
|
||||
iconUrl: softwareTitle.icon_url,
|
||||
displayName: softwareTitle.display_name,
|
||||
addedTimestamp: isPackage
|
||||
? installerData.uploaded_at
|
||||
: installerData.created_at,
|
||||
|
||||
+114
-25
@@ -4,29 +4,83 @@ software/versions/:id > Top section
|
||||
software/os/:id > Top section
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { SingleValue } from "react-select-5";
|
||||
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
||||
import { TooltipContent } from "interfaces/dropdownOption";
|
||||
|
||||
import { getPathWithQueryParams, QueryParams } from "utilities/url";
|
||||
import { getGitOpsModeTipContent } from "utilities/helpers";
|
||||
import paths from "router/paths";
|
||||
import {
|
||||
NO_VERSION_OR_HOST_DATA_SOURCES,
|
||||
ROLLING_ARCH_LINUX_VERSIONS,
|
||||
} from "interfaces/software";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import DataSet from "components/DataSet";
|
||||
import LastUpdatedHostCount from "components/LastUpdatedHostCount";
|
||||
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
import { isSafeImagePreviewUrl } from "pages/SoftwarePage/helpers";
|
||||
import TooltipWrapperArchLinuxRolling from "components/TooltipWrapperArchLinuxRolling";
|
||||
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
|
||||
|
||||
import SoftwareIcon from "../../icons/SoftwareIcon";
|
||||
import OSIcon from "../../icons/OSIcon";
|
||||
|
||||
const buildActionOptions = (
|
||||
gitOpsModeEnabled: boolean | undefined,
|
||||
repoURL: string | undefined,
|
||||
source: string | undefined,
|
||||
androidSoftwareAvailableForInstall: boolean
|
||||
): CustomOptionType[] => {
|
||||
let disableEditAppearanceTooltipContent: TooltipContent | undefined;
|
||||
let disableEditSoftwareTooltipContent: TooltipContent | undefined;
|
||||
let disabledEditConfigurationTooltipContent: TooltipContent | undefined;
|
||||
|
||||
if (gitOpsModeEnabled) {
|
||||
const gitOpsModeTooltipContent =
|
||||
repoURL && getGitOpsModeTipContent(repoURL);
|
||||
|
||||
disableEditAppearanceTooltipContent = gitOpsModeTooltipContent;
|
||||
disabledEditConfigurationTooltipContent = gitOpsModeTooltipContent;
|
||||
|
||||
if (source === "vpp_apps") {
|
||||
disableEditSoftwareTooltipContent = gitOpsModeTooltipContent;
|
||||
}
|
||||
}
|
||||
|
||||
const options: CustomOptionType[] = [
|
||||
{
|
||||
label: "Edit appearance",
|
||||
value: "edit_appearance",
|
||||
isDisabled: !!disableEditAppearanceTooltipContent,
|
||||
tooltipContent: disableEditAppearanceTooltipContent,
|
||||
},
|
||||
{
|
||||
label: "Edit software",
|
||||
value: "edit_software",
|
||||
isDisabled: !!disableEditSoftwareTooltipContent,
|
||||
tooltipContent: disableEditSoftwareTooltipContent,
|
||||
},
|
||||
];
|
||||
|
||||
if (androidSoftwareAvailableForInstall) {
|
||||
options.push({
|
||||
label: "Edit configuration",
|
||||
value: "edit_configuration",
|
||||
isDisabled: !!disabledEditConfigurationTooltipContent,
|
||||
tooltipContent: disabledEditConfigurationTooltipContent,
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const baseClass = "software-details-summary";
|
||||
|
||||
interface ISoftwareDetailsSummaryProps {
|
||||
@@ -46,10 +100,18 @@ interface ISoftwareDetailsSummaryProps {
|
||||
iconUrl?: string | null;
|
||||
/** Displays OS icon instead of Software icon */
|
||||
isOperatingSystem?: boolean;
|
||||
/** Shows Actions dropdown allowing user to edit software */
|
||||
canManageSoftware?: boolean;
|
||||
/** Displays an edit CTA to edit the software's icon and display name
|
||||
* Should only be defined for team view of an installable software */
|
||||
onClickEditAppearance?: () => void;
|
||||
/** Displays an edit CTA to edit the software installer
|
||||
* Should only be defined for team view of an installable software */
|
||||
onClickEditSoftware?: () => void;
|
||||
/** undefined unless previewing icon, in which case is string or null */
|
||||
/** Displays an edit CTA to edit the software's icon
|
||||
* Should only be defined for team view of an installable software */
|
||||
onClickEditIcon?: () => void;
|
||||
/** undefined unless previewing icon, in which case is string or null */
|
||||
onClickEditConfiguration?: () => void;
|
||||
iconPreviewUrl?: string | null;
|
||||
/** timestamp of when icon was last uploaded, used to force refresh of cached icon */
|
||||
iconUploadedAt?: string;
|
||||
@@ -66,12 +128,36 @@ const SoftwareDetailsSummary = ({
|
||||
versions,
|
||||
iconUrl,
|
||||
isOperatingSystem,
|
||||
onClickEditIcon,
|
||||
canManageSoftware = false,
|
||||
onClickEditAppearance,
|
||||
onClickEditSoftware,
|
||||
onClickEditConfiguration,
|
||||
iconPreviewUrl,
|
||||
iconUploadedAt,
|
||||
}: ISoftwareDetailsSummaryProps) => {
|
||||
const hostCountPath = getPathWithQueryParams(paths.MANAGE_HOSTS, queryParams);
|
||||
|
||||
const { config } = useContext(AppContext);
|
||||
|
||||
const gitOpsModeEnabled = config?.gitops.gitops_mode_enabled;
|
||||
const repoURL = config?.gitops.repository_url;
|
||||
const isRollingArch = ROLLING_ARCH_LINUX_VERSIONS.includes(displayName);
|
||||
|
||||
const onSelectSoftwareAction = (option: SingleValue<CustomOptionType>) => {
|
||||
switch (option?.value) {
|
||||
case "edit_appearance":
|
||||
onClickEditAppearance && onClickEditAppearance();
|
||||
break;
|
||||
case "edit_software":
|
||||
onClickEditSoftware && onClickEditSoftware();
|
||||
break;
|
||||
case "edit_configuration":
|
||||
onClickEditConfiguration && onClickEditConfiguration();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
// Remove host count for tgz_packages, sh_packages, and ps1_packages only
|
||||
// or if viewing details summary from edit icon preview modal
|
||||
const showHostCount =
|
||||
@@ -102,6 +188,13 @@ const SoftwareDetailsSummary = ({
|
||||
);
|
||||
};
|
||||
|
||||
const actionOptions = buildActionOptions(
|
||||
gitOpsModeEnabled,
|
||||
repoURL,
|
||||
source,
|
||||
!!onClickEditConfiguration
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={baseClass}>
|
||||
@@ -111,9 +204,9 @@ const SoftwareDetailsSummary = ({
|
||||
renderSoftwareIcon()
|
||||
)}
|
||||
<dl className={`${baseClass}__info`}>
|
||||
<div className={`${baseClass}__title-edit-icon`}>
|
||||
<div className={`${baseClass}__title-actions`}>
|
||||
<h1>
|
||||
{ROLLING_ARCH_LINUX_VERSIONS.includes(displayName) ? (
|
||||
{isRollingArch ? (
|
||||
// wrap a tooltip around the "rolling" suffix
|
||||
<>
|
||||
{displayName.slice(0, -8)}
|
||||
@@ -123,22 +216,18 @@ const SoftwareDetailsSummary = ({
|
||||
<TooltipTruncatedText value={displayName} />
|
||||
)}
|
||||
</h1>
|
||||
|
||||
{onClickEditIcon && (
|
||||
<GitOpsModeTooltipWrapper
|
||||
renderChildren={(disableChildren) => (
|
||||
<div className={`${baseClass}__edit-icon`}>
|
||||
<Button
|
||||
onClick={onClickEditIcon}
|
||||
className={`${baseClass}__edit-icon-btn`}
|
||||
disabled={disableChildren}
|
||||
variant="icon"
|
||||
>
|
||||
<Icon name="pencil" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{canManageSoftware && (
|
||||
<div className={`${baseClass}__actions-wrapper`}>
|
||||
<DropdownWrapper
|
||||
className={`${baseClass}__actions-dropdown`}
|
||||
name="software-actions"
|
||||
onChange={onSelectSoftwareAction}
|
||||
placeholder="Actions"
|
||||
options={actionOptions}
|
||||
variant="button"
|
||||
nowrapMenu
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<dl className={`${baseClass}__description-list`}>
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__title-edit-icon {
|
||||
&__title-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
@@ -84,7 +84,7 @@ interface IPackageFormProps {
|
||||
defaultPostInstallScript?: string;
|
||||
defaultUninstallScript?: string;
|
||||
defaultSelfService?: boolean;
|
||||
defaultCategories?: SoftwareCategory[];
|
||||
defaultCategories?: SoftwareCategory[] | null;
|
||||
className?: string;
|
||||
/** Indicates that this PackageForm deals with an entity that can be managed by GitOps, and so should be disabled when gitops mode is enabled */
|
||||
gitopsCompatible?: boolean;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ISoftwareDisplayNameFormData } from "pages/SoftwarePage/SoftwareTitleDe
|
||||
import { IAddFleetMaintainedData } from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage";
|
||||
import { listNamesFromSelectedLabels } from "components/TargetLabelSelector/TargetLabelSelector";
|
||||
import { ISoftwareAndroidFormData } from "pages/SoftwarePage/components/forms/SoftwareAndroidForm/SoftwareAndroidForm";
|
||||
import { ISoftwareConfigurationFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/EditConfigurationModal";
|
||||
|
||||
export interface ISoftwareApiParams {
|
||||
page?: number;
|
||||
@@ -179,6 +180,7 @@ export interface IEditAppStoreAppPostBody {
|
||||
labels_exclude_any?: string[];
|
||||
categories?: SoftwareCategory[];
|
||||
display_name?: string;
|
||||
configuration?: string;
|
||||
}
|
||||
|
||||
const ORDER_KEY = "name";
|
||||
@@ -303,6 +305,13 @@ const handleDisplayNameAppStoreAppForm = (
|
||||
body.display_name = formData.displayName || "";
|
||||
};
|
||||
|
||||
const handleConfigurationAppStoreAppForm = (
|
||||
formData: ISoftwareConfigurationFormData,
|
||||
body: IEditAppStoreAppPostBody
|
||||
) => {
|
||||
body.configuration = formData.configuration || "{}";
|
||||
};
|
||||
|
||||
const handleEditAppStoreAppForm = (
|
||||
formData: ISoftwareVppFormData,
|
||||
body: IEditAppStoreAppPostBody
|
||||
@@ -557,6 +566,7 @@ export default {
|
||||
| ISoftwareVppFormData
|
||||
| ISoftwareAndroidFormData
|
||||
| ISoftwareDisplayNameFormData
|
||||
| ISoftwareConfigurationFormData
|
||||
) => {
|
||||
const { EDIT_SOFTWARE_APP_STORE_APP } = endpoints;
|
||||
|
||||
@@ -568,6 +578,12 @@ export default {
|
||||
formData as ISoftwareDisplayNameFormData,
|
||||
body
|
||||
);
|
||||
} else if ("configuration" in formData) {
|
||||
// Handles Edit configuration form only
|
||||
handleConfigurationAppStoreAppForm(
|
||||
formData as ISoftwareConfigurationFormData,
|
||||
body
|
||||
);
|
||||
} else {
|
||||
// Handles primary Edit AppStoreApp form
|
||||
// 4.77 Currently, only VPP apps can be edited, not Google Play apps
|
||||
|
||||
Reference in New Issue
Block a user