Design challenge · handover
Prototype: secfix-bulk-edit.vercel.app · single file, HTML + Tailwind + vanilla JS, no build step
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.
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:
| State | What it sends to the API |
|---|---|
| Untouched | Field absent from the payload |
| Set | Field with a value |
| Cleared | Field with an explicit empty value |
Untouched and cleared must be distinguishable, otherwise wiping an outdated description is impossible to express.
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.
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.
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.
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.
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.
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.
| Category | Name | ID / Identifier | Status | Owner | Classification | Description | Data stored | Delete |
|---|---|---|---|---|---|---|---|---|
| Employee Computers Device agent |
Read-only | Read-only | Derived | Bulk editable | Bulk editable | Bulk editable | Not in table | Not in table |
| Relational databases AWS / Azure / GCP |
Read-only | Read-only | Derived | Bulk, lockable | Bulk, lockable | Bulk, lockable | Read-only | Not in table |
| Compute Instances AWS / Azure / GCP |
Read-only | Read-only | Derived | Bulk, lockable | Bulk, lockable | Bulk, lockable | Read-only | Not in table |
| Other cloud assets AWS / Azure / GCP |
Read-only | Read-only | Derived | Bulk, lockable | Bulk, lockable | Bulk, lockable | Read-only | Not in table |
| Custom assets Manual / import |
Inline only | Inline only | Derived | Bulk editable | Bulk editable | Bulk editable | Bulk editable | Bulk |
Cloud categories also show Account/Regions and Tags. Both are read-only, straight from the provider.
| State | Meaning |
|---|---|
| Bulk editable | Always writable on every asset in the category. No per-asset check needed. |
| Bulk, lockable | Writable 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 only | Present and editable on the row, deliberately kept out of bulk: these must stay unique per asset, so one shared value is never valid. |
| Read-only | Present in the table but never editable on Secfix. The source system owns it. |
| Derived | Not edited directly. Status is computed from Owner: filled means Complete, empty means Incomplete. |
| Not in table | The category has no such column, so there is nothing to show or edit. |
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.
PostHog is wired to project 233325 (EU).
| Event | Properties |
|---|---|
bulk_edit_applied | category, fields, field_count, asset_count, selection_scope, skipped_locked, cleared_fields |
ai_prompt_submitted | intent, prompt, category |
selection_expanded | scope, count, category |
confirm_shown / confirm_cancelled | title |
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.
Design tokens sit in the tailwind.config block at the top, taken from the Figma file.
FIELD_DEFS and to that category in CATEGORY_FIELDS. The chip, popover, counts, confirmation and matrix all follow.S.CONFIRM_THRESHOLD.row.locked in buildRow.aiRun.POSTHOG_KEY at the top.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.