feat(web): grouped look-alike UI across the account tables - #8354
feat(web): grouped look-alike UI across the account tables#8354tongyaosafe wants to merge 22 commits into
Conversation
…orybook) Add a groupBySimilarity capability to the shared SafeAccountsTable: contiguous rows sharing a cluster id render inside a light-yellow "Address poisoning warning" band, each member as its own rounded yellow-bordered card, with a small inline⚠️ after the name for flagged look-alikes (replaces the old "High similarity" pill). New props: similarityGroups (Map<addr, clusterId>); SafeInfoDisplay gains nameAdornment. Styling lives in the Table sx (keyed off data attributes) so it composes with the cell-level hover/separator machinery. Storybook only — no real surface wired yet (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Branch preview✅ Deploy successful! Storybook: |
|
@claude review |
|
Claude finished @tongyaosafe's task in 3m 5s —— View job Claude PR Review
Focused on the new similarity-band / grouping code. Overall this is a clean, well-commented visual layer — most feedback is minor. A couple of items worth a second look: 1)
|
📦 Next.js Bundle Analysis for @safe-global/webThis analysis was generated by the Next.js Bundle Analysis action. 🤖
|
| Page | Size (compressed) |
|---|---|
global |
1.5 MB (🟡 +2.54 KB) |
Details
The global bundle is the javascript bundle that loads alongside every page. It is in its own category because its impact is much higher - an increase to its size means that every page on your website loads slower, and a decrease means every page loads faster.
Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis
If you want further insight into what is behind the changes, give @next/bundle-analyzer a try!
One Page Changed Size
The following page changed size from the code in this PR compared to its base branch:
| Page | Size (compressed) | First Load |
|---|---|---|
/new-safe/load |
9.48 KB (🟢 -40 B) |
1.51 MB |
Details
Only the gzipped size is provided here based on an expert tip.
First Load is the size of the global bundle plus the bundle for the individual page. If a user were to show up to your website and land on a given page, the first load size represents the amount of javascript that user would need to download. If next/link is used, subsequent page loads would only need to download that page's bundle (the number in the "Size" column), since the global bundle has already been downloaded.
Any third party scripts you have added directly to your app using the <script> tag are not accounted for in this analysis
Next to the size is how much the size has increased or decreased compared with the base branch of this PR. If this percentage has increased by 20% or more, there will be a red status indicator applied, indicating that special attention should be given to this.
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🟢 | Statements | 83.63% (+0.01% 🔼) |
32059/38333 |
| 🟡 | Branches | 67.33% (-0.04% 🔻) |
10212/15168 |
| 🟡 | Functions | 70.93% (-0.04% 🔻) |
4844/6829 |
| 🟢 | Lines | 84.93% (-0% 🔻) |
28711/33806 |
Show new covered files 🐣
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🟢 | ... / useCopyToClipboard.ts |
100% | 100% | 100% | 100% |
| 🟢 | ... / crossListClusters.ts |
100% | 100% | 100% | 100% |
| 🔴 | ... / SimilarityBand.tsx |
58.33% | 16.67% | 12.5% | 57.14% |
| 🟢 | ... / orderGroupsBySimilarity.ts |
100% | 93.75% | 100% | 100% |
Show files with reduced coverage 🔻
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🟢 | ... / CopyAddressButton.tsx |
100% (+5.56% 🔼) |
50% (-50% 🔻) |
100% (+20% 🔼) |
100% |
| 🟢 | ... / index.tsx |
84.21% (-2.52% 🔻) |
87.76% (-0.79% 🔻) |
75% (-5.65% 🔻) |
84.21% (-0.97% 🔻) |
| 🟢 | ... / SafeAccountTableRow.tsx |
89.23% | 80.21% (-1.31% 🔻) |
76.92% | 91.8% |
| 🟡 | ... / ReorderableBody.tsx |
66.67% (-7.14% 🔻) |
42.11% (-23.28% 🔻) |
52.17% (-12.11% 🔻) |
67.92% (-8.55% 🔻) |
Test suite run success
6641 tests passing in 738 suites.
Report generated by 🧪jest coverage report action from 630dde1
…act table sx - Fix light-mode band regression: --band-surface/--band-border referenced themselves; restore the yellow-50/yellow-400 light values (dark keeps the warning-token override). - Extract the ~90-line body-row sx into tableStyles.ts (bodyRowSx) with ROW/CELL selector-prefix constants to kill the repeated selector chains. - Dedup the band-opening header into bandHeaderAt(), shared by the non-reorder body and the reorderable pinned block. - Suppress the grey row divider inside a band so no line paints between cards. - Export orderGroupsBySimilarity + add unit tests (interleaved / anchor lead / no-anchor / multi-cluster). - Refresh stale "High similarity" / "Approach A" comments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ng step 2 [WA-2912] Detection already ran on the combined trusted+owned pool, but the result was filtered to owned rows only, so a look-alike pair where the impostor is pinned (both rows in My accounts) showed no warning at all. Surface every flagged row: badge on trusted and owned tables alike, one security banner above the sections, and the selection confirm-gate now covers trusted rows too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t [WA-2912] Review follow-up: trusted+owned is a partition of allSafes, so the combined address pool can map allSafes directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ounts table The alert and the table were adjacent fragment children with no spacing; wrap them in the same flex-col gap-4 used by the onboarding list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stacks the grouped similarity-band UI on top of WA-2912's flag policy and extends grouping to both onboarding lists: - Adopt WA-2912 flagging: flag against the full trusted+owned pool, surface⚠️ on trusted rows too (a pinned impostor must warn), single top banner. - Generalize onboarding banding from owned-only to per-list via groupsWithinList (counted by distinct address): trusted and owned each band their own same-list clusters; a cluster split across the two sections stays per-row⚠️ . - Tests: route each list its own similarity groups; trusted/owned same-list cluster + cross-list no-band coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single model for all SafeAccountsTable surfaces: - Band + tinted card + title conveys same-list look-alikes; NO per-row⚠️ there. - The inline⚠️ (with a peers tooltip listing full addresses + copy) appears ONLY for cross-list clusters — onboarding's trusted↔owned split, the one case a band can't box. All single-list surfaces (manage-trusted, add-accounts, safe-accounts) now show bands only, no icon. Table API: drop the icon's `flaggedAddresses` prop; add `similarWarnings` (address → cross-list peers, drives⚠️ +tooltip) and `anchorAddresses` (vetted/ pinned member leads its band, ordering-only). similarityGroups still drives the band. Onboarding hook: each list bands all its own cluster members (dropped the ≥2 gate, so a lone cross-list member is a single boxed card); buildSimilarWarnings emits the cross-list⚠️ payload. `flagged` still gates the select-confirm dialog. Peers tooltip uses a bare copy button (theme-adaptive hover) rather than the shared CopyAddressButton, which hard-codes a light-surface tint and nests its own tooltip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-up: the variable maps allSafes directly and no longer combines two lists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oning-protection-to-onboarding-select-safe' into feat/address-poisoning-grouped-ui # Conflicts: # apps/web/src/features/spaces/components/SelectSafesOnboarding/hooks/useOnboardingSafes.ts
|
@claude review |
|
Claude finished @tongyaosafe's task in 2m 49s —— View job Claude PR Review
Focused review of the new similarity-band / pinning code. Nothing security-critical — the band is a rendering layer over existing data — but a few things worth polishing before merge. 1)
|
…-list cluster policy, split orderGroupsBySimilarity - useCopyToClipboard: shared copied-flag state machine, used by CopyAddressButton and the band tooltip's PeerAddress (was a line-for-line duplicate) - bandGroupsForList/buildSimilarWarnings + SimilarWarning move from the spaces onboarding hook / myAccounts barrel into features/address-poisoning, decoupled from safe-item shapes — the cross-list presentation rule now has one owner - orderGroupsBySimilarity gets its own file (tests no longer load the component graph), plus micro cleanups: unreachable ?? fallback dropped, single-pass anchor partition, orphaned doc comment removed - drop debug console.logs from useOnboardingSafes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xplain the band via Info tooltip A drop used to persist the pinned-first display order, so one drag during a similarity warning permanently hoisted cluster members in the user's manual order. ReorderableBody now reports only the draggable rows; the table weaves them back into the stored order (weaveReorderedKeys, unit-tested). The band header's Info icon now opens a tooltip instead of being decorative. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep the constraints (rule-order dependence, weave contract, WA-2912 intent), drop the narration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fixed |
…-grouped-ui # Conflicts: # apps/web/src/features/spaces/components/SelectSafesOnboarding/components/OnboardingSafesList.tsx # apps/web/src/features/spaces/components/SelectSafesOnboarding/components/__tests__/OnboardingSafesList.test.tsx # apps/web/src/features/spaces/components/SelectSafesOnboarding/hooks/__tests__/useOnboardingSafes.test.ts # apps/web/src/features/spaces/components/SelectSafesOnboarding/hooks/useOnboardingSafes.ts # apps/web/src/features/spaces/components/SelectSafesOnboarding/index.tsx
Its only consumer was the High similarity badge, replaced by the inline nameAdornment warning icon; the remaining badge= call sites in the app belong to other components' own props. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only bandHeaderAt in the same file renders it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| export type SimilarWarning = { trusted: string[]; owned: string[] } | ||
|
|
||
| /** Address → cluster id for one list's look-alikes, deduped so a multi-chain safe isn't listed twice. */ | ||
| export const bandGroupsForList = (items: Addressed[], groupIdByAddress: Map<string, string>): Map<string, string> => { |
There was a problem hiding this comment.
Maybe we can define a new type and reuse it everywhere possible. Also, it would be nice if we can make the address type more strict. Something like
type GroupIdByAddress = Map<`0x{string}`, string>
WDYT?
There was a problem hiding this comment.
Need consider doing it by a following refactoring ticket. Otherwise too big change here.
| * Full address + inline copy for the warning tooltip. A bare button, not CopyAddressButton — its | ||
| * hover tint and nested Tooltip are both wrong inside this dark hover popup. | ||
| */ | ||
| const PeerAddress = ({ address }: { address: string }) => { |
There was a problem hiding this comment.
Also, here we could make it more type-safe.
| const PeerAddress = ({ address }: { address: string }) => { | |
| const PeerAddress = ({ address }: { address: `0x{string}` }) => { |
| '--band-surface': 'var(--color-yellow-50)', | ||
| '--band-border': 'var(--color-yellow-400)', |
There was a problem hiding this comment.
Lets use our theme color variables only -> apps/web/src/styles/vars.css
We have warning color versions there for both light and dark mode
- useCopyToClipboard: await navigator.clipboard.writeText inside try/catch; a denied write logs (Errors._110) and leaves `copied` false instead of silently pretending success. Add unit tests (success reset + reject path). - Add ErrorCodes._110 "Failed to copy to clipboard". - reorder tests: fold the weaveReorderedKeys cases into src/utils/reorder.test.ts and drop the duplicate __tests__/reorder.test.ts (reviewer flag). - crossListClusters: unit tests for bandGroupsForList + buildSimilarWarnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-up (#8354): replace the two complementary `.filter(isClustered)` passes with a single `partition(groups, isClustered)`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-up (#8354): pull the two inline map bodies into GroupChildRows, PinnedGroupRows and DraggableGroupRows, with a SharedRowProps bundle for the repeated row wiring. Behaviour-neutral — the body is now two one-line maps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-up (#8354): port bodyRowSx (hover pill, separators, similarity band) from an inline sx object to tableStyles.module.css for repo consistency. Mui classes wrapped in :global(); dark kept as :global(.dark) .body; rule order preserved (band after hover). Applied via className on the <Table>; dynamic minWidth stays in sx. Behaviour-neutral. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-grouped-ui # Conflicts: # apps/web/src/features/myAccounts/components/SafeAccountsTable/index.tsx
Match the 16px row radius dev landed for the account rows (was 8px); colours unchanged (yellow-50 surface / yellow-400 border, warning tokens in dark). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tx-builder Preview✅ Deploy successful! Preview URL: |





What it solves
Resolves: WA-2916
Stacked on #8363 (WA-2912).
Look-alike (address poisoning) warnings previously rendered as isolated per-row badges: a flagged row told you it resembled something, but not what, and visually similar accounts could sit far apart in a long list. This PR groups them.
How this PR fixes it
One clustering source (
useSimilarityClusters), three presentation primitives onSafeAccountsTable:Wired into all four account-table surfaces: onboarding Select Safe accounts, Manage trusted Safes modal, workspace Safe accounts, and workspace Add accounts.
Supporting changes:
bandGroupsForList,buildSimilarWarnings,SimilarWarning) lives infeatures/address-poisoning— one owner for the rule, decoupled from safe-item shapes.weaveReorderedKeys, unit-tested) — the clusters' pin-to-top stays display-only and can't permanently rewrite the manual arrangement.useCopyToClipboardhook (copy state machine deduped fromCopyAddressButton);orderGroupsBySimilarityextracted to its own module with tests.How to test it
SafeAccountsTablesimilarity stories).Affected flows
SafeAccountsTablewith similarity props: onboarding step 2, Manage trusted Safes modal, workspace Safe accounts, workspace Add accounts.Blast radius
SafeAccountsTablefamily (index,ReorderableBody,SafeAccountTableRow, newSimilarityBand,tableStyles,orderGroupsBySimilarity) — consumed by the four surfaces above; the new similarity props are optional, so prop-less usages render unchanged.features/address-poisoninggains pure presentation utils; detection engine, thresholds, and theADDRESS_POISONING_PROTECTIONflag gating are untouched.SafeInfoDisplaygains an optionalnameAdornmentslot (existingbadgeconsumers unaffected).useTrustedSafesModal:flaggednow excludes pinned anchors (modal-only policy); onboarding keeps flagging the full set.useCopyToClipboard+weaveReorderedKeys(unit-tested);CopyAddressButtonbehavior unchanged.Risks / not checked
Visual summary
flowchart LR A["useSimilarityClusters(all addresses)"] --> G["groupIdByAddress"] A --> F["flagged"] G --> O["orderGroupsBySimilarity\ncluster contiguous, anchor first"] O --> B["SimilarityBand\nheader + carded rows"] G --> X["bandGroupsForList / buildSimilarWarnings\n(features/address-poisoning)"] X --> W["inline ⚠️ + peers tooltip\n(cross-list clusters)"] F --> C["SimilarityConfirmDialog on select\n(modal: pinned anchors exempt)"]Before:


After:


Checklist
CLA signature
With the submission of this Pull Request, I confirm that I have read and agree to the terms of the Contributor License Agreement.