Skip to content

Fleet UI: Multi-package Library, Add/Edit/Delete modals (#48400)#48520

Merged
RachelElysia merged 8 commits into
feat/28108-multiple-custom-packagesfrom
48400-multi-package-library-pr
Jul 9, 2026
Merged

Fleet UI: Multi-package Library, Add/Edit/Delete modals (#48400)#48520
RachelElysia merged 8 commits into
feat/28108-multiple-custom-packagesfrom
48400-multi-package-library-pr

Conversation

@RachelElysia

@RachelElysia RachelElysia commented Jun 30, 2026

Copy link
Copy Markdown
Member

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: canActivateMultiplePackages gates 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 and installer_id is the confirmed field name. MSW handlers remain for frontend tests.

Screenrecording

Testing

  • QA'd all new/changed functionality manually
  • yarn tsc --noEmit clean
  • make lint-js — 0 errors
  • yarn test --testPathPattern="SoftwarePage|useBlockNavigation" — all passing
  • Manual browser test via yarn start covering: 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 link

Design decisions locked (specs Slack thread)

  • Package row order = API response order (installer_id ascending). Top row = first-added = collision fallback. UI does not re-sort.
  • Non-custom layouts (FMA, VPP, Google Play, iOS): keep the existing summary-card chips, Actions dropdown, and Latest / Pinned badges. FMA does not use per-package Edit modals — VersionsModal still handles version switching.
  • Custom-package summary card: chips hide, Actions dropdown collapses to a pencil-icon Edit-appearance button; per-row icons on the Library accordion carry the self-service / auto-install signals.
  • Custom packages don't support patch policies — patch icon variant stripped from the row-icon path.
  • Edit modal in GitOps mode: everything visible-but-disabled for both FMA and custom multi-package titles. Save carries the standard GitOps tooltip linking to the repo.
  • View YAML feature cut entirely: ViewYamlModal/ deleted, the ?gitops_yaml=true post-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.
  • 10-package limit UX: Add-package button visible-but-disabled with an explanatory tooltip (matches the API error copy). Flagged to Marko for confirmation.

Out of scope

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 37 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (feat/28108-multiple-custom-packages@7124b27). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...tleDetailsPage/AddPackageModal/AddPackageModal.tsx 48.71% 20 Missing ⚠️
frontend/services/entities/software.ts 0.00% 6 Missing ⚠️
...etailsPage/EditSoftwareModal/EditSoftwareModal.tsx 55.55% 4 Missing ⚠️
...ePage/components/forms/PackageForm/PackageForm.tsx 89.65% 3 Missing ⚠️
...ge/SoftwareCustomPackage/SoftwareCustomPackage.tsx 50.00% 2 Missing ⚠️
...lsPage/DeleteSoftwareModal/DeleteSoftwareModal.tsx 80.00% 1 Missing ⚠️
...Page/LibraryItemAccordion/LibraryItemAccordion.tsx 95.23% 1 Missing ⚠️
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           
Flag Coverage Δ
frontend 59.36% <75.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
@RachelElysia RachelElysia force-pushed the 48400-multi-package-library-pr branch from 58e70e0 to f355e85 Compare July 7, 2026 21:27
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";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`.
Backend #48607 (resolves #48397) landed on the feature branch; tests
now hit the real API contract. The `multi-package-handlers.ts` file
had no consumers beyond its own tests (self-testing MSW stubs), so
both files are deleted.
Placeholder-only file (comment-only, no rules); nothing imported it.
gitOpsModeEnabled
) {
// No longer flash message, we open YAML modal if editing with gitOpsModeEnabled
openViewYamlModal();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view yaml modal is being removed completely in this feature

@RachelElysia

Copy link
Copy Markdown
Member Author

@claude review

@RachelElysia RachelElysia marked this pull request as ready for review July 8, 2026 20:58
@RachelElysia RachelElysia requested a review from a team as a code owner July 8, 2026 20:58
@@ -86,7 +91,11 @@ const DeleteSoftwareModal = ({
const onDeleteSoftware = useCallback(async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100% nice catch!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

const DeleteSoftwareModal = ({
softwareId,
teamId,
installerId,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to test this? I just noticed it's tested in EditSoftwareModal.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

// 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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

- 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.
…ments

Comments, prop docstrings, and describe/it labels leaned on `(#48400)` /
`(#48397)` / `#28108` as an audit trail. That's what git blame is for —
remove the citations without changing the load-bearing prose.
@RachelElysia RachelElysia merged commit 70a3206 into feat/28108-multiple-custom-packages Jul 9, 2026
18 checks passed
@RachelElysia RachelElysia deleted the 48400-multi-package-library-pr branch July 9, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple packages: software title details Library page and package modals

2 participants