Fleet UI: Multi-package Library, Add/Edit/Delete modals (#48400)#48520
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## feat/28108-multiple-custom-packages #48520 +/- ##
======================================================================
Coverage ? 68.04%
======================================================================
Files ? 3682
Lines ? 233544
Branches ? 12519
======================================================================
Hits ? 158911
Misses ? 60340
Partials ? 14293
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Foundation slice for the multi-package title-details page. Renders title.packages[] when present and falls back to [software_package] so back-compat single-package titles look identical to before. No new modals yet — the AddPackageModal and per-installer Edit/Delete wiring land in a follow-up slice. - Types: optional installer_id on ISoftwarePackage; packages[] on ISoftwareTitle and ISoftwareTitleDetails. installer_id stays optional until #48397 ships and the server populates it everywhere. - API client: addSoftwarePackage takes software_title_id (multi-package add); editSoftwarePackage and deleteSoftwareInstaller take an optional installer_id so they target one specific package. - MSW: new frontend/test/handlers/multi-package-handlers.ts with a buildTitleWithPackages helper and handlers for GET (multi/empty), POST (happy + duplicate-hash + 10-limit + FMA-conflict + VPP-conflict), PATCH per-installer, and DELETE per-installer. - Library section iterates packages instead of operating on a single software_package; description copy updated to Figma. App-store / custom-package branches stay mutually exclusive at the data layer. TODOs flagged in code: - Strip issue refs (#48396/#48397/#48400) from comments before merging to main. - Make installer_id required once #48397 ships. - Drop the software_package fallback in renderLibrarySection once packages[] is always present.
…48400) Builds on the foundation slice with the per-package modal flows and the coordinated UI states for multi-package custom titles. Hides title-level status indicators (chips, page-level Edit) in favor of per-package indicators on each Library accordion row so the surfaces stay accurate when one package on a title differs from another. Single source of truth at the page level: `canActivateMultiplePackages` flips three behaviors together — the "+ Add package" action, the per-row self-service / auto-install icons, and the summary card's chip-hide and pencil-Edit treatment. True for custom non-FMA non-iOS titles (Mac/Linux/Windows binary + script packages); false for FMA, VPP, Google Play, and iOS in-house .ipa, which stay on the single-package path. New components - AddPackageModal: real in-place modal wrapping PackageForm. File-type chooser is restricted to the existing title's platform (helper at AddPackageModal/helpers.ts maps filename -> {accept, label}, with the .tar.gz workaround mirrored). Standard mode renders the first-added-wins banner; GitOps mode renders the SHA-256/YAML banner with the "YAML docs" link. Target defaults to Custom so labels are required. - PackageForm: new multiPackageContext prop embeds the right banner under the file uploader, swaps the Save button copy, hides the deploy slider, and shows the target-label selector above Advanced options. Caught and fixed a pre-existing duplication of the submit button text across the tooltipped/non-tooltipped branches that let the copy drift. - useBlockNavigation hook: extracts the `beforeunload` pattern that was duplicated across SoftwareCustomPackage, EditSoftwareModal, and the new AddPackageModal. Per-package wiring - selectedInstallerId state on the page resolves which package the Edit / Delete modal targets; row callbacks pass `pkg.installer_id`. - EditSoftwareModal title swaps to "Edit package" via the new canActivateMultiplePackages prop; PATCH carries the installer id. - DeleteSoftwareModal DELETE carries the installer id when set. - Per-package PoliciesModal: clicking the auto-install icon navigates to the single linked policy or opens a modal scoped to that package's policies, distinct from the title-aggregate modal owned by the SoftwareSummaryCard. Library accordion row - Custom non-iOS rows hide the Latest badge and render clickable self-service / auto-install indicator icons. A renderRowActionIcon helper deduplicates the tooltip + optional button + static-fallback pattern. - Latest badge tightened to FMA-only (`isFma && badgeState === "latest"`) so VPP/Play Store/iOS rows don't surface it either. - ariaLabels: "Edit package" (matches the modal title that opens) for self-service; "View auto-install policies" / "View patch policy" for the auto-install icon. Summary card - Hides Self-service / Auto install / Patch chips when canActivateMultiplePackages — per-row icons replace them. - Collapses the Actions dropdown to a single pencil-icon Edit button that opens the Edit Appearance modal directly; per-installer editing is on the accordion. The pencil button is wrapped in GitOpsModeTooltipWrapper so GitOps mode disables it with the standard "Managed by GitOps" tooltip. - "Custom package(s)" chip pluralizes via `pluralize()` from stringUtils. InfoBanner - Wired up the existing `icon` prop (previously unused per its TODO) so banners can render a leading Icon. CSS gates the layout shift behind the existing `__icon` modifier so callers without the prop are unaffected. Tests - AddPackageModal component tests (13): title, banner copy verbatim, GitOps mode variant + YAML link, file-type restriction across .pkg /.deb/.msi/.tar.gz/.sh, Save vs Add software label, Custom radio preselected. - EditSoftwareModal title swap (3 tests). - LibraryItemAccordion custom-package row (4 tests): hidden by default, self-service icon fires onSelfServiceClick, auto-install icon fires onAutoInstallClick, patch-policy variant uses the patch aria-label. - Updated SoftwareSummaryCard chip tests to use FMA mocks so they keep exercising the chip-render path; added a new test verifying chips are hidden for multi-package titles. - New helper tests: getFileTypeRestriction (10), MSW factories (8). - 411 SoftwarePage tests passing total (up from ~211 pre-PR). Verbatim copy from Figma page 2:130 throughout (including the "it's" apostrophe typo in the GitOps banner — preserved deliberately so design and code stay in sync). TODOs before merge to main: - Strip the #48396/#48397/#48400 issue refs sprinkled in comments. - Lock the `installer_id` field name once #48397 settles.
58e70e0 to
f355e85
Compare
Applies the design decisions locked in the #48400 specs Slack thread and addresses the self-review pass: - Premium gating on `canActivateMultiplePackages` per parent story #28108 (custom multi-package feature is Premium-only). - Package row order: API returns `packages[]` sorted by `installer_id` ascending; UI renders in API order (top row = first-added = collision fallback). Comment locked in place; no re-sort. - New target-section banner per Figma 5944-4477: "If multiple packages of the same software target the same host..." with the `error-outline` icon, moved from under the file uploader into `renderTargetLabelSelector`. - Edit modal in GitOps mode: everything visible-but-disabled for both FMA and custom multi-package titles (extends the FMA pattern). No more "reduce fields" special-case. - View YAML feature cut entirely: `ViewYamlModal/` deleted, the `?gitops_yaml=true` post-add redirect param dropped from SoftwareCustomPackage, the post-Edit-save auto-open removed. Users edit through YAML via the "YAML docs" link instead. - Extracted `GitOpsCustomPackageBanner` as a parameter-less export from `SoftwareCustomPackage.tsx`; consumed by both the single-package add page and PackageForm's multi-package Add modal. Single-package flow now shows the "YAML docs" link too, matching Marko's normalization. - Removed dead `isPatchPolicyOnly` branch from the custom-package row icons — custom packages don't have patch policies. - Fixed the previously preserved `it's` → `its` typo (deliberately reverted per Marko). - Typed `defaultSoftware?: any` as `ISoftwarePackage | IAppStoreApp | undefined`; helper signatures in `SoftwarePage/helpers.tsx` widened to accept undefined to match runtime behavior. Tests - New `useBlockNavigation.tests.ts` (6 tests: block on/off, unmount cleanup, toggle-off cleanup, handler behavior). - New `SoftwareCustomPackage.tests.tsx` (2 tests on the shared `GitOpsCustomPackageBanner` export). - Updated `AddPackageModal.tests.tsx` for the new target-section banner wording and the `its` typo fix; removed the patch-policy variant test from `LibraryItemAccordion.tests.tsx`. Verification: `yarn tsc --noEmit` clean, all touched tests passing, `make lint-js` 0 errors.
| @@ -0,0 +1,26 @@ | |||
| import { useEffect } from "react"; | |||
There was a problem hiding this comment.
Currently used at:
- SoftwareCustomPackage.tsx:96 — useBlockNavigation(!!uploadDetails) — blocks while single-package add upload is uploading
- AddPackageModal.tsx:73 — useBlockNavigation(!!uploadDetails) — same for the multi-package add modal
- EditSoftwareModal.tsx:164 — useBlockNavigation(isUpdatingSoftware) — blocks while the edit PATCH is in flight
So: dedupe of a real cross-file pattern, not new behavior. If it feels tangential to the multi-package work, worth mentioning — the trigger was the third copy landing.
- `installer_id` is now required on `ISoftwarePackage` — backend #48607 (resolves #48397) landed on the feature branch and confirms the field name. Dropped the "TODO: make required once #48397 ships" and the "Optional during transition" comment. - Reframed `packages: ISoftwarePackage[] | null` comments on both `ISoftwareTitle` and `ISoftwareTitleDetails` — no longer "until the server is on the multi-package contract," now describes the actual meaning (null = no custom packages, source of truth when present). - Dropped the "TODO(48400): drop the software_package fallback once #48397 ships" in the details page. Fallback stays as defense against no-custom-packages titles (FMA / app-store branch), reframed the comment accordingly. - Added `useBlockNavigation` to the custom-hooks list in `frontend/docs/patterns.md`.
Placeholder-only file (comment-only, no rules); nothing imported it.
| gitOpsModeEnabled | ||
| ) { | ||
| // No longer flash message, we open YAML modal if editing with gitOpsModeEnabled | ||
| openViewYamlModal(); |
There was a problem hiding this comment.
The view yaml modal is being removed completely in this feature
|
@claude review |
| @@ -86,7 +91,11 @@ const DeleteSoftwareModal = ({ | |||
| const onDeleteSoftware = useCallback(async () => { | |||
There was a problem hiding this comment.
Now that this modal can delete a single package from a multi-package title, should the title ("Delete software") and the "Custom icon and display name will be deleted." warning be conditional? I'm thinking that on a title with >1 package those aren't deleted, so he copy seems inaccurate for the per-package path. EditSoftwareModal got a canActivateMultiplePackages prop to flip its title; should DeleteSoftwareModal get the same?
| const DeleteSoftwareModal = ({ | ||
| softwareId, | ||
| teamId, | ||
| installerId, |
There was a problem hiding this comment.
Do we want to test this? I just noticed it's tested in EditSoftwareModal.
| // early-returns when there are no packages and no app-store app, so we | ||
| // don't need to re-check `packages.length` here. | ||
| const showAddPackageAction = canActivateMultiplePackages && canEditSoftware; | ||
| const atPackageLimit = packages.length >= 10; |
There was a problem hiding this comment.
The backend exposes fleet.MaxPackagesPerTitle = 10 and a SoftwarePackageLimitMessage format string. Should the frontend derive the gate and tooltip from a shared constant instead of hardcoding, so they can't drift if the backend limit changes?
- DeleteSoftwareModal: add `canActivateMultiplePackages` prop mirroring EditSoftwareModal. When true, title flips to "Delete package" and the "Custom icon and display name will be deleted." warning is suppressed — on a per-package delete of a multi-package title the title stays, so those wouldn't actually be deleted. - Cover both branches of the new prop in DeleteSoftwareModal.tests.tsx. - Add `MAX_PACKAGES_PER_TITLE` in interfaces/software.ts pointing at `fleet.MaxPackagesPerTitle` (server/fleet/software_installer.go). Use it in SoftwareTitleDetailsPage for both the "+ Add package" gate and the tooltip copy so the two can't drift if the backend cap changes.
70a3206
into
feat/28108-multiple-custom-packages
Related issue: Resolves #48400
Description
Frontend implementation of the multi-package custom-package UI for #28108. The Library section iterates
title.packages[], exposes per-installer Edit / Delete / Download, and adds an in-place Add Package modal with file-type restriction and a GitOps-mode variant. Coordinated state changes on the Software title details page move title-level Self-service / Auto install indicators down to per-row icons so they stay accurate when one package on a title differs from another.Single source of truth at the page level:
canActivateMultiplePackagesgates the "+ Add package" action, the per-row self-service / auto-install icons, the summary card's chip-hide + pencil-Edit treatment, and the disabled Edit modal in GitOps mode. True for Premium + custom non-FMA non-iOS titles (Mac / Linux / Windows binary + script packages); false for FMA, VPP, Google Play, iOS in-house.ipa, and Free tier — all of which stay on the single-package path.The backend API landed on the feature branch as
#48607(resolves#48397); this PR is wired to the real endpoints andinstaller_idis the confirmed field name. MSW handlers remain for frontend tests.Screenrecording
Testing
yarn tsc --noEmitcleanmake lint-js— 0 errorsyarn test --testPathPattern="SoftwarePage|useBlockNavigation"— all passingyarn startcovering: multi-package Library render, "+ Add package" flow (standard + GitOps modes), per-row Edit / Delete, file-type restriction, GitOps-mode disabled Edit modal, 10-package limit tooltip, "Edit package" modal title swap, target-section first-added-wins banner, single-package add-page GitOps banner, YAML docs linkDesign decisions locked (specs Slack thread)
installer_idascending). Top row = first-added = collision fallback. UI does not re-sort.ViewYamlModal/deleted, the?gitops_yaml=truepost-add redirect param dropped, the post-Edit-save auto-open removed. Users edit through YAML via the "YAML docs" link on the shared GitOps banner.Out of scope
SoftwareTitleDetailsPage) end-to-end tests — heavy harness setup; the page's behaviors are covered by component-level tests