Design challenge · handover

Bulk editing for Secfix Inventory

Prototype: secfix-bulk-edit.vercel.app · single file, HTML + Tailwind + vanilla JS, no build step

The problem this solves

Selecting assets and editing several of their fields in one action, across four categories that each expose different fields, where individual fields on individual rows can be locked by cloud tags.

The action bar

Every bulk-editable field for the current category appears as a chip next to the selection count, with Apply and Clear on the right. A chip shows its pending value once set, so the whole staged change is readable in one row without opening anything.

Three states per field, and the third is the one that usually gets missed:

StateWhat it sends to the API
UntouchedField absent from the payload
SetField with a value
ClearedField with an explicit empty value

Untouched and cleared must be distinguishable, otherwise wiping an outdated description is impossible to express.

Selection

Row checkboxes select individually. The header checkbox selects the current page only. When a whole page is selected, a banner offers Select all N matching — deliberately a separate, explicit gesture, so nobody assigns 9,000 assets by clicking a header checkbox.

Selection survives an apply. Changing two fields on the same set is a single action, and even after applying, the same rows stay selected for a follow-up.

Locking

A cloud asset's field is locked only where a cloud tag supplied its value. The lock is per field, per asset: Owner can be locked while Description stays editable on the same row.

Each chip carries a live count against the current selection, for example Owner 16/20. Fields with zero editable rows disable outright. The unlock path, per the help article, is removing the tag on the cloud asset.

This is why filtering matters more than it looks. Filter to Unassigned and nothing in the result can be locked, because a lock only exists where a tag wrote a value. The skip path becomes a fallback rather than the main road.

Confirmation

Small batches apply straight away. Confirmation appears when the batch exceeds 50, when all matching is selected, or when the change would move assets backwards from Complete to Incomplete. That last case confirms at any size, because it is the only bulk action that regresses compliance state.

Large batches

Above roughly 200 assets the write is treated as a job: progress bar with a live count, affected rows dimmed, and a Stop control. Stopping keeps what already landed and reports how far it got. Completion returns a partial-failure count with Retry. Undo is offered after completion, and survives a retry.

Assistant (scripted)

A natural-language input that resolves to filters plus a selection plus a pre-filled change, then hands off to the normal action bar and confirmation. It can narrow; only the person can commit. Four scripted intents, labelled Scripted in the UI. No model runs.

Field capability matrix

Also available in the prototype under Field matrix, top right. Two separate questions: whether the column exists in that table at all, and whether its value can be changed.

CategoryNameID / IdentifierStatus OwnerClassificationDescriptionData storedDelete
Employee Computers
Device agent
Read-onlyRead-only Derived Bulk editableBulk editableBulk editable Not in tableNot in table
Relational databases
AWS / Azure / GCP
Read-onlyRead-only Derived Bulk, lockableBulk, lockableBulk, lockable Read-onlyNot in table
Compute Instances
AWS / Azure / GCP
Read-onlyRead-only Derived Bulk, lockableBulk, lockableBulk, lockable Read-onlyNot in table
Other cloud assets
AWS / Azure / GCP
Read-onlyRead-only Derived Bulk, lockableBulk, lockableBulk, lockable Read-onlyNot in table
Custom assets
Manual / import
Inline onlyInline only Derived Bulk editableBulk editableBulk editable Bulk editableBulk

Cloud categories also show Account/Regions and Tags. Both are read-only, straight from the provider.

StateMeaning
Bulk editableAlways writable on every asset in the category. No per-asset check needed.
Bulk, lockableWritable unless a cloud tag supplied the value on that asset. The lock is per field, per asset. Unlock path: remove the tag on the cloud asset.
Inline onlyPresent and editable on the row, deliberately kept out of bulk: these must stay unique per asset, so one shared value is never valid.
Read-onlyPresent in the table but never editable on Secfix. The source system owns it.
DerivedNot edited directly. Status is computed from Owner: filled means Complete, empty means Incomplete.
Not in tableThe category has no such column, so there is nothing to show or edit.

Decisions worth knowing

One bar of fields, not one button per field. Per-field buttons replicate the table header and stop scaling past three fields.

Unassigned is how you clear an owner. No separate clear control for Owner, because the list entry already does that job. Description and Classification keep an explicit clear, since a blank text box has no equivalent list entry.

Nothing is preselected. Ticking a field selects no value and Apply stays disabled until one is chosen. No bulk write should have a default, because the cost of an accidental one scales with the selection.

Owner drives Status. Setting Owner flips Incomplete to Complete. Clearing it moves the asset back, and the UI says how many will regress before you confirm.

Open questions for engineering

  1. Does the API accept a filter, or a list of IDs? Select all 9,831 either sends a query the server resolves, or the client fetches and posts 9,831 IDs. This decides whether cross-page selection is buildable as designed.
  2. Synchronous or a job? At several thousand assets a synchronous call will time out. The prototype assumes a job above roughly 200 and designs the progress, stop, and partial-failure states accordingly.
  3. Per-field skip reporting. The response should return written and skipped counts per field so the UI can report honestly rather than guess.
  4. Set drift on filter-based writes. With a filter-based API, the matching set can change between the count the user reads and the moment they confirm. In a compliance tool that gap needs a decision.
  5. Interim ownership. "Assign to me until we find a replacement" is a real request the schema cannot express. Either interim owners get marked as such, or placeholders quietly become permanent.

Analytics

PostHog is wired to project 233325 (EU).

EventProperties
bulk_edit_appliedcategory, fields, field_count, asset_count, selection_scope, skipped_locked, cleared_fields
ai_prompt_submittedintent, prompt, category
selection_expandedscope, count, category
confirm_shown / confirm_cancelledtitle
undo_clicked, job_stopped

Three things worth watching: the ratio of manual selection to select-all-matching, the confirmation cancel rate (high means the threshold earns its keep, near zero means it is a click for nothing), and the undo rate as a proxy for how dangerous the action feels.

Editing the prototype

Design tokens sit in the tailwind.config block at the top, taken from the Figma file.

Two traps if you edit it. Never put hidden and a display utility like grid on the same element, because the Tailwind CDN emits them in scan order and the result is unpredictable. And use closest() rather than comparing event.target.id on any button that wraps an icon, or clicks on the icon fall through.