diff --git a/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.module.css b/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.module.css new file mode 100644 index 00000000000..9d9a78e3980 --- /dev/null +++ b/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.module.css @@ -0,0 +1,71 @@ +.RealisticTimeline { + /* GitHub renders the timeline at most 1012px wide in product surfaces. */ + max-width: 1012px; +} + +/* Story-only scaffolding: each variant is wrapped in its own
with a + small caption heading ABOVE the event, so the event row itself (badge + + inline actor + body) renders exactly as it would in product — the caption + never sits inside Timeline.Body. Variants stay scannable like a Figma + component set. Not part of the faithful event. */ +.Variant { + margin-bottom: var(--base-size-24); +} + +.VariantLabel { + margin: 0 0 var(--base-size-4); + font-size: var(--text-body-size-small); + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +/* System "GitHub" actor — live `AlertTimeline.tsx` `TimelineItemBody` renders + ` GitHub` (no avatar) + when `isGitHubActor`. This class spaces the leading octicon. */ +.ActorIcon { + vertical-align: middle; + margin-right: var(--base-size-4); +} + +/* Bold actor name, mirroring the live `text-bold` span. Shared by the system + "GitHub" label and the user `display_login` (live `UserComponent` renders the + login as bold TEXT, not a link). */ +.ActorName { + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-default); + margin-right: var(--base-size-4); +} + +/* Inline user-actor avatar in the body. Live `UserComponent` renders a 16px + CIRCLE `GitHubAvatar` followed by the bold login. */ +.InlineAvatar { + vertical-align: middle; + margin-right: var(--base-size-4); +} + +/* Optional comment sub-row. Live `TimelineItemBody` renders a small indented + block below the body: a 12px muted `CommentIcon` + f6 comment text. Driven by + `resolution.comment` / `exemption_request.requester_comment` / + `exemption_response.reviewer_comment`. */ +.CommentRow { + display: flex; + align-items: center; + margin-top: var(--base-size-4); + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); +} + +.CommentRowIcon { + margin-right: var(--base-size-4); + color: var(--fgColor-muted); +} + +/* Muted relative timestamp. The live secret-scanning `TimelineItemBody` renders + a plain `RelativeTime` (no link wrapper) — muted text only, matching the + Dependabot timeline (and a deliberate difference from the Issues `Ago` + deep-link). */ +.Timestamp { + color: var(--fgColor-muted); +} diff --git a/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.tsx b/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.tsx new file mode 100644 index 00000000000..cbe5952617d --- /dev/null +++ b/packages/react/src/Timeline/Timeline.secret-scanning.features.stories.tsx @@ -0,0 +1,816 @@ +import type {Meta} from '@storybook/react-vite' +import type {ComponentProps} from '../utils/types' +import {FeatureFlags} from '../FeatureFlags' +import Timeline from './Timeline' +import { + AlertIcon, + CheckCircleIcon, + CommentIcon, + MarkGithubIcon, + PersonIcon, + ShieldCheckIcon, + ShieldIcon, + ShieldSlashIcon, + SkipIcon, + SyncIcon, + XIcon, +} from '@primer/octicons-react' +import type React from 'react' +import Avatar from '../Avatar' +import {Button} from '../Button' +import Octicon from '../Octicon' +import RelativeTime from '../RelativeTime' +import classes from './Timeline.secret-scanning.features.stories.module.css' + +/** + * Secret Scanning alert Timeline event examples (Phase 2 of github/primer#6663). + * + * These stories recreate GitHub's live secret-scanning-alert-timeline events + * using the Primer `Timeline` compositional slots. They mirror the established + * Issues (`Timeline.issues.features.stories.tsx`) and Dependabot + * (`Timeline.dependabot.features.stories.tsx`) stories and are sourced from the + * `timeline-audit` inventory (`secret-scanning-timeline-events-for-figma.md`), + * verified against the live React implementation. + * + * SOURCE OF TRUTH — Secret Scanning is FULLY React (NOT ERB). The alert show + * page is a React SPA in `github/github-ui`, package + * `packages/secret-scanning-alerts/`. The timeline is rendered by + * `components/show/AlertTimeline.tsx`, whose `switch (event.type)` is the + * authoritative dispatch for every event's badge variant + octicon + copy. It + * already composes Primer React `Timeline` + `Timeline.Badge variant=`, so the + * badge colors map directly. The actor is rendered by + * `components/shared/UserComponent.tsx` (16px circle avatar + bold login); the + * system actor is `MarkGithubIcon` + bold "GitHub". (No ERB secret-scanning + * timeline exists — the migration to React is complete.) The exact event list + * built below is the full set of cases the live `switch` actually renders; the + * defined-but-never-dispatched `REVOCATION` event type (no `case`) renders + * nothing and is intentionally NOT built. + * + * SCOPE: These are Storybook-only examples by design. They are intentionally + * NOT wired into components-json / the primer.style docs page (do NOT add this + * file to `Timeline.docs.json` or `build.ts`). Individual timeline events are + * not consumer-facing components — the primer.style Timeline page reflects the + * base `Timeline` component's own stories, and any docs-site representation is a + * Phase 3 consideration via base-component story changes, out of scope here. + * + * FUTURE FILTERING (taxonomy still open — github/primer#6663): category + * `data-*` attributes (e.g. `data-event-category="created"`) will attach to each + * `Timeline.Item` below so stories can be filtered/grouped by event family. We + * intentionally do NOT add them yet to avoid baking in a taxonomy. + * + * SLOT USAGE (Phase 1 slots — same convention as the Issues / Dependabot groups): + * - `Timeline.Avatar` (gutter slot, #6677): the 40px LEFT-GUTTER avatar. + * Reserved for comment-style events. The badge-row events here do NOT use it — + * the live `AlertTimeline.tsx` renders the actor INLINE in the body + * (`UserComponent`, or the system `MarkGithubIcon` + "GitHub"), not in the + * gutter. We mirror that: actor inline in `Timeline.Body`. + * - `Timeline.Actions` (right-controls slot, #6678): for buttons on the right + * edge. Only the delegated-closure "requested to dismiss" event has right + * controls (a "Review request" / "Cancel request" button), so it is the only + * group that uses this slot. + * + * BADGE COLORS (live `Timeline.Badge variant=`): success (green) — Creation, + * Reopened; done (purple) — Closed as revoked; danger (red) — Validity active; + * attention (amber) — Validity unknown; default (gray) — everything else. The + * live code passes `variant={undefined}` for the gray events, which renders the + * DEFAULT `Timeline.Badge` (a muted icon on a subtle/borderless circle, NO solid + * fill). We render those as a BARE `` to match exactly — none of + * the secret-scanning gray events is a solid-gray badge, so (unlike the Issues + * "not planned" variant) the `--timelineBadge-bgColor` hook is not used here. + * + * ACCESSIBILITY NOTE: none of the secret-scanning events render an in-text + * `` — actors and resolution reasons are BOLD TEXT (the live + * `UserComponent` uses a `text-bold` span, not a profile link, and reasons are + * bolded plain text). So the axe `link-in-text-block` rule (which failed the + * Dependabot CI for underline-less in-text links) is never exercised by this + * surface. Any future in-text link added here must use `inline`/bold. + */ + +const MONALISA_AVATAR = 'https://avatars.githubusercontent.com/u/583231?v=4' +const SIX7_AVATAR = 'https://avatars.githubusercontent.com/u/4548309?v=4' +const HUBOT_AVATAR = 'https://avatars.githubusercontent.com/u/480938?v=4' + +/** + * System "GitHub" actor — live `TimelineItemBody` (`AlertTimeline.tsx`) in + * `isGitHubActor` mode renders ` GitHub` (no avatar). Used by the Created event and by + * automated validity changes. + */ +const GitHubActor = () => ( + <> + + GitHub + +) + +/** + * User actor — live `UserComponent` (`components/shared/UserComponent.tsx`): + * a 16px CIRCLE `GitHubAvatar` + bold `display_login` (``). Note the login is bold TEXT, not a link (hovercard attrs only), + * so there is no in-text-link a11y concern. Used by every non-system event. + */ +const UserActor = ({login = 'monalisa', src = MONALISA_AVATAR}: {login?: string; src?: string}) => ( + <> + + {login} + +) + +// Muted relative timestamp. The live secret-scanning `TimelineItemBody` renders +// a plain `RelativeTime` with no link wrapper — muted text only (matching the +// Dependabot timeline, unlike the Issues `Ago` deep-link). +const Time = ({date}: {date: string}) => ( + + + +) + +/** + * Optional comment sub-row — live `TimelineItemBody` renders a + * `
{comment}
` below the body whenever any of + * `resolution.comment` / `exemption_request.requester_comment` / + * `exemption_response.reviewer_comment` is present. Shared by the Resolution + * closures and the delegated-closure request/approve/deny events. + */ +const CommentSubRow = ({children}: {children: React.ReactNode}) => ( +
+ + {children} +
+) + +export default { + title: 'Components/Timeline/Events/Secret Scanning', + component: Timeline, + subcomponents: { + 'Timeline.Item': Timeline.Item, + 'Timeline.Avatar': Timeline.Avatar, + 'Timeline.Badge': Timeline.Badge, + 'Timeline.Body': Timeline.Body, + 'Timeline.Break': Timeline.Break, + 'Timeline.Actions': Timeline.Actions, + }, + decorators: [ + // File-scoped: render every story in the future-state list semantics + // (`
    `/`
  1. `). The `primer_react_timeline_list_semantics` flag is merged + // on main; this opts these stories into the DOM the timeline will ship. + Story => ( + + + + ), + ], +} as Meta> + +/** + * The Created event group — `SecretScanTimeline.eventCreated` (audit § 1). + * + * Source: `case TimelineEventType.Creation` in `AlertTimeline.tsx` — the only + * event that uses `isGitHubActor` unconditionally. The actor is ALWAYS the + * GitHub system actor. Badge: `ShieldIcon` on `success` (green) — the live code + * renders `}>`. Copy is a + * fixed `"opened this alert"` + the relative time. + * + * Single variant — the live `Creation` case has exactly one rendering (no + * source / from-PR / from-push branches like Dependabot's Opened). + */ +export const EventCreated = () => ( +
    + {/* Created — GitHub system actor, ShieldIcon on success (green) */} +
    +

    Created

    + + + + + + + + {'opened this alert '} + + + +
    +
    +) + +/** + * The Resolution event group — `SecretScanTimeline.eventResolution` (audit § 2). + * + * Source: `case TimelineEventType.Resolution` in `AlertTimeline.tsx`. Two + * shapes share this event type: + * - REOPENED (`resolution.type === 'reopened'`): `SyncIcon` on `success` + * (green), copy "reopened this". The live code renders a `Timeline.Break` + * immediately BEFORE this item (sibling-selector CSS) — reproduced here. + * - CLOSED (any other `resolution.type`): copy "closed this as {reason}", where + * `{reason}` is `resolutionText(resolution.type)` rendered as BOLD TEXT (not a + * link). Only `revoked` gets the special `ShieldCheckIcon` on `done` (purple); + * every other reason uses `ShieldSlashIcon` on the default (gray) badge. + * + * The seven closed reasons are exactly the `resolutionText()` outputs (live + * `helper.ts`): revoked, false positive, won't fix, used in tests, pattern + * deleted, pattern edited, ignored by configuration. Each may carry an optional + * `(comment)` sub-row from `resolution.comment` — demonstrated on the revoked + * variant. Actor is always the user. + */ +export const EventResolution = () => ( +
    + {/* Closed as revoked — the only reason with the purple `done` / ShieldCheck + badge. Shown WITH an optional resolution-comment sub-row. */} +
    +

    Closed as revoked

    + + + + + + + + {'closed this as '} + revoked + + +
    + + {/* Closed as false positive — gray (default) ShieldSlash badge. */} +
    +

    Closed as false positive

    + + + + + + + + {'closed this as '} + false positive + + +
    + + {/* Closed as won't fix */} +
    +

    Closed as won't fix

    + + + + + + + + {'closed this as '} + won't fix + + +
    + + {/* Closed as used in tests */} +
    +

    Closed as used in tests

    + + + + + + + + {'closed this as '} + used in tests + + +
    + + {/* Closed as pattern deleted */} +
    +

    Closed as pattern deleted

    + + + + + + + + {'closed this as '} + pattern deleted + + +
    + + {/* Closed as pattern edited */} +
    +

    Closed as pattern edited

    + + + + + + + + {'closed this as '} + pattern edited + + +
    + + {/* Closed as ignored by configuration (resolution type `hidden_by_config`) */} +
    +

    Closed as ignored by configuration

    + + + + + + + + {'closed this as '} + ignored by configuration + + +
    + + {/* Reopened — SyncIcon on success (green), preceded by a Timeline.Break. + The live code emits the Break as a sibling immediately BEFORE the + reopened Item so the sibling-selector CSS applies. We include the + preceding (closed) Item here so the Break renders BETWEEN two items, as + it does in product — mirroring the live "break between events" + placement rather than leaving the Break as a stray first child. */} +
    +

    Reopened

    + + + + + + + + {'closed this as '} + false positive + + + + + + + + + {'reopened this '} + + + +
    +
    +) + +/** + * The Push-protection / bypass event group — + * `SecretScanTimeline.eventPushProtection` (audit § 3). + * + * Source: `case TimelineEventType.Bypass`, + * `DelegatedBypassRequestOpened`, `DelegatedBypassRequestApproved` in + * `AlertTimeline.tsx`. All three use the user actor and the default (gray) + * badge; only the icon + copy differ. The two delegated variants render only + * for repos with delegated bypass enabled (backend-gated org feature). + */ +export const EventBypass = () => ( +
    + {/* Bypassed — AlertIcon, default (gray) badge */} +
    +

    Bypassed push protection

    + + + + + + + + {'bypassed push protection '} + + + +
    + + {/* Bypass requested — CommentIcon. Delegated bypass: only renders when the + repo has delegated bypass enabled. */} +
    +

    Bypass requested (delegated bypass enabled)

    + + + + + + + + {'requested bypass privileges '} + + + +
    + + {/* Bypass approved — CheckCircleIcon. Delegated bypass: gated as above. */} +
    +

    Bypass approved (delegated bypass enabled)

    + + + + + + + + {'approved a bypass '} + + + +
    +
    +) + +/** + * The Validity-change event group — `SecretScanTimeline.eventValidity` + * (audit § 4). + * + * Source: `ValidityChangeTimelineEvent` in `AlertTimeline.tsx`. The validity + * bucket drives the badge + icon: active -> `AlertIcon` on `danger` (red); + * inactive -> `SkipIcon` on default (gray); unknown -> `AlertIcon` on + * `attention` (amber). Whether the change is AUTOMATED (no `event.actor` -> + * GitHub system actor + "verified this secret is …" / "is unable to determine + * …") or MANUAL (`event.actor` present -> user actor + "set validity to …") is + * decided purely by `!event.actor`. Both forms are shown per bucket. + */ +export const EventValidityChange = () => ( +
    + {/* Active — automated (GitHub), AlertIcon on danger (red) */} +
    +

    Validity: active (automated)

    + + + + + + + + {'verified this secret is active '} + + + +
    + + {/* Active — manual (user), same danger badge */} +
    +

    Validity: active (manual)

    + + + + + + + + {'set validity to active '} + + + +
    + + {/* Inactive — automated (GitHub), SkipIcon on default (gray) */} +
    +

    Validity: inactive (automated)

    + + + + + + + + {'verified this secret is inactive '} + + + +
    + + {/* Inactive — manual (user) */} +
    +

    Validity: inactive (manual)

    + + + + + + + + {'set validity to inactive '} + + + +
    + + {/* Unknown — automated (GitHub), AlertIcon on attention (amber) */} +
    +

    Validity: unknown (automated)

    + + + + + + + + {'is unable to determine the validity of this secret '} + + + +
    + + {/* Unknown — manual (user) */} +
    +

    Validity: unknown (manual)

    + + + + + + + + {'set validity to unknown '} + + + +
    +
    +) + +/** + * The Report event group — `SecretScanTimeline.eventReport` (audit § 5). + * + * Source: `case TimelineEventType.Report` in `AlertTimeline.tsx`. Badge: + * `ShieldCheckIcon` on the default (gray) badge. Copy: "reported this secret". + * The case does NOT pass `isGitHubActor`, so the actor is the USER (not the + * GitHub system actor) — confirmed against the live switch. + */ +export const EventReport = () => ( +
    + {/* Reported — ShieldCheckIcon, default (gray) badge, user actor */} +
    +

    Reported

    + + + + + + + + {'reported this secret '} + + + +
    +
    +) + +/** + * The delegated-closure (dismissal) event group — + * `SecretScanTimeline.eventClosureRequest` (audit § 6). + * + * Source: `case TimelineEventType.DelegatedClosureRequestOpened` / + * `…Approved` / `…Rejected` / `…Cancelled` in `AlertTimeline.tsx`. This is the + * org-level delegated-dismissal feature (backend-gated). All four use the user + * actor and the default (gray) badge; icon + copy differ: + * - Opened -> `CommentIcon`, "requested to dismiss this[ as {reason}]" + * - Approved -> `CheckCircleIcon`, "approved dismissal" + * - Rejected -> `XIcon`, "denied dismissal" + * - Cancelled -> `SkipIcon`, "cancelled request to dismiss" + * + * RIGHT CONTROLS (`Timeline.Actions`): the Opened event shows, while the request + * is pending & not expired, EITHER a small primary "Review request" button + * (shown to reviewers via the `show_closure_request_review_buttons` payload + * flag, which opens `ClosureRequestReviewButtons`' review dialog) OR a small + * invisible "Cancel request" button (shown to the requester via + * `show_closure_request_cancel_button`). They are driven by two independent, + * viewer-specific payload flags — mutually exclusive per viewer — so both + * variants are shown below. The optional `(comment)` sub-row carries the + * requester comment (Opened) or reviewer comment (Approved / Denied). + */ +export const EventClosureRequest = () => ( +
    + {/* Requested — with the reviewer-facing "Review request" primary button and + a requester comment sub-row. The `[ as {reason}]` clause is the + un-bolded `resolutionText(exemption_request.reason)`. */} +
    +

    Dismissal requested (reviewer view)

    + + + + + + + + {'requested to dismiss this as false positive '} + + + + + + +
    + + {/* Requested — requester view: the invisible "Cancel request" button. */} +
    +

    Dismissal requested (requester view)

    + + + + + + + + {'requested to dismiss this as false positive '} + + + + + + +
    + + {/* Approved — CheckCircleIcon, with a reviewer comment sub-row. */} +
    +

    Dismissal approved

    + + + + + + + + {'approved dismissal '} + + + +
    + + {/* Denied — XIcon */} +
    +

    Dismissal denied

    + + + + + + + + {'denied dismissal '} + + + +
    + + {/* Cancelled — SkipIcon */} +
    +

    Dismissal request cancelled

    + + + + + + + + {'cancelled request to dismiss '} + + + +
    +
    +) + +/** + * The Assignment event group — `SecretScanTimeline.eventAssignments` + * (audit § 7). + * + * Source: `AssignmentChangeTimelineEvent` in `AlertTimeline.tsx`. Badge: + * `PersonIcon` on the default (gray) badge. Unlike the avatar-less PR/Issue + * assignment pattern, the actor AND the assignee/unassignee are each rendered + * with an avatar via `UserComponent` (matching Dependabot). The five shapes are + * derived from which of `assigned_user` / `unassigned_user` is present and + * whether the actor equals the (un)assignee (self vs other). + */ +export const EventAssignment = () => ( +
    + {/* Self-assigned — actor === assignee */} +
    +

    Self-assigned

    + + + + + + + + {'self-assigned this '} + + + +
    + + {/* Assigned someone else — both actor + assignee avatars */} +
    +

    Assigned another user

    + + + + + + + + {'assigned '} + + + +
    + + {/* Self-unassigned — actor removed their own assignment */} +
    +

    Removed own assignment

    + + + + + + + + {'removed their assignment '} + + + +
    + + {/* Unassigned someone else */} +
    +

    Unassigned another user

    + + + + + + + + {'unassigned '} + + + +
    + + {/* Assigned one user and unassigned another in a single event */} +
    +

    Assigned and unassigned

    + + + + + + + + {'assigned '} + + {' and unassigned '} + + + +
    +
    +)