From 8b0847ebaf64587ea36e18ddf3cae6942531ccb0 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 17 Aug 2026 14:35:56 -0300 Subject: [PATCH 1/4] refactor(forms): Let Switch and FieldLabel take an id A FieldLabel can only point at a control that has an id, and Switch did not accept one, so its label could not be associated; rc-switch already spreads unknown props onto its {' '} + {' '} to pick repositories from a list. @@ -227,13 +225,8 @@ const GithubTrustRelationshipForm: FC = ({ repositoryFields = ( - } + value={`Pinned by repository ID ${pinnedRepoId}`} + inputProps={{ className: 'full-width', readOnly: true }} /> ) } else if (installationId) { @@ -281,9 +274,8 @@ const GithubTrustRelationshipForm: FC = ({ <> - } + value={audience} + inputProps={{ className: 'full-width', readOnly: true }} /> = ({
diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipModal/TrustRelationshipModal.tsx b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipModal/TrustRelationshipModal.tsx index 26376920edb9..1410d94b456c 100644 --- a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipModal/TrustRelationshipModal.tsx +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipModal/TrustRelationshipModal.tsx @@ -1,4 +1,4 @@ -import React, { FC, useMemo, useState } from 'react' +import React, { FC, useId, useMemo, useState } from 'react' import Button from 'components/base/forms/Button' import ErrorMessage from 'components/ErrorMessage' import FieldLabel from 'components/base/forms/FieldLabel' @@ -32,6 +32,8 @@ const TrustRelationshipModal: FC = ({ trustRelationship, }) => { const isEdit = !!trustRelationship + const claimRulesLabelId = useId() + const valuesHintId = useId() const [name, setName] = useState(trustRelationship?.name || '') const [issuer, setIssuer] = useState(trustRelationship?.issuer || '') const [audience, setAudience] = useState(trustRelationship?.audience || '') @@ -148,53 +150,58 @@ const TrustRelationshipModal: FC = ({ onChange={(e: InputEvent) => setAudience(Utils.safeParseEventValue(e))} placeholder='e.g. https://github.com/YourOrg' /> - Claim matching rules - {claimRules.map((rule, index) => ( - - - - setClaimRules((rules) => - rules.map((r, i) => - i === index - ? { ...r, claim: Utils.safeParseEventValue(e) } - : r, - ), - ) + Claim matching rules +
+ {claimRules.map((rule, index) => ( + + + + setClaimRules((rules) => + rules.map((r, i) => + i === index + ? { ...r, claim: Utils.safeParseEventValue(e) } + : r, + ), + ) + } + placeholder='Claim, e.g. repository' + /> + + + + setClaimRules((rules) => + rules.map((r, i) => + i === index + ? { ...r, values: Utils.safeParseEventValue(e) } + : r, + ), + ) + } + placeholder='Values, e.g. YourOrg/your-repo' + aria-describedby={valuesHintId} + /> + + - - ))} -
+ aria-label={`Remove rule ${index + 1}`} + > + + + + ))} +
+
Values support * wildcards; separate alternatives with commas.
diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipPermissionsFields/TrustRelationshipPermissionsFields.tsx b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipPermissionsFields/TrustRelationshipPermissionsFields.tsx index e074ec4168a8..cfd357f30278 100644 --- a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipPermissionsFields/TrustRelationshipPermissionsFields.tsx +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipPermissionsFields/TrustRelationshipPermissionsFields.tsx @@ -1,7 +1,7 @@ -import React, { FC, useState } from 'react' -import { IonIcon } from '@ionic/react' -import { close as closeIcon } from 'ionicons/icons' +import { FC, useId, useState } from 'react' import Button from 'components/base/forms/Button' +import Chip from 'components/base/Chip' +import FieldLabel from 'components/base/forms/FieldLabel' import PlanBasedBanner from 'components/PlanBasedAccess' import Switch from 'components/Switch' import MyRoleSelect from 'components/MyRoleSelect' @@ -29,12 +29,17 @@ const TrustRelationshipPermissionsFields: FC< roles, }) => { const [showRoles, setShowRoles] = useState(false) + const isAdminId = useId() + const rolesLabelId = useId() return ( <> - + + Is admin + {!isAdmin && ( <> - - - {roles.map((role) => ( - onRemoveRole(role.id)} - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault() - onRemoveRole(role.id) - } - }} - className='chip' - > - {role.name} - - - - - ))} + + + Roles + +
+ {roles.map((role) => ( + onRemoveRole(role.id)} + > + {role.name} + + ))} +
From 41f5321759dc22f9d6115608fbff05de4099f9d8 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 18 Aug 2026 13:10:53 -0300 Subject: [PATCH 4/4] feat(OIDC): Rebuild the provider chooser and show the chosen provider The chooser cards become ProviderCard: leading icon tile, an inline Recommended chip, and a trailing chevron. Its own component rather than an extension of SelectableCard, which stacks its icon above the title and has four consumers, two of which would move. Step two used to give no sign of which provider was picked and no way to change it, since the modal title reads the same on both steps. It now opens with a Provider summary naming the choice, with Change to go back, and focus lands on the first editable field. The provider registry carries the chooser's copy too, so a preset is one entry rather than a label and icon in the registry and the same pair again in the chooser. Its icon became a function of size, since the list badge and the card want different ones, and the GitHub label moved next to GITHUB_ISSUER. Co-Authored-By: Claude Opus 5 (1M context) --- .../NewTrustRelationshipModal.scss | 3 + .../NewTrustRelationshipModal.tsx | 106 +++++++++++------- .../ProviderCard/ProviderCard.scss | 19 ++++ .../ProviderCard/ProviderCard.tsx | 50 +++++++++ .../trust-relationships/ProviderCard/index.ts | 2 + .../tabs/trust-relationships/github.ts | 1 + .../tabs/trust-relationships/providers.tsx | 38 +++++-- 7 files changed, 170 insertions(+), 49 deletions(-) create mode 100644 frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.scss create mode 100644 frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.scss create mode 100644 frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.tsx create mode 100644 frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/index.ts diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.scss b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.scss new file mode 100644 index 000000000000..8daf2f6a7c92 --- /dev/null +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.scss @@ -0,0 +1,3 @@ +.new-trust-relationship__provider { + border: 1px solid var(--color-border-default); +} diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.tsx b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.tsx index fa474a2cc5f3..eee24d03f65a 100644 --- a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.tsx +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.tsx @@ -1,8 +1,15 @@ -import React, { FC, useState } from 'react' +import { FC, useEffect, useRef, useState } from 'react' +import Button from 'components/base/forms/Button' +import ProviderCard from 'components/pages/organisation-settings/tabs/trust-relationships/ProviderCard' +import { + TRUST_RELATIONSHIP_PROVIDERS, + TrustRelationshipProvider, +} from 'components/pages/organisation-settings/tabs/trust-relationships/providers' import GithubTrustRelationshipForm from 'components/pages/organisation-settings/tabs/trust-relationships/GithubTrustRelationshipForm' import TrustRelationshipModal from 'components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipModal' +import './NewTrustRelationshipModal.scss' -type Provider = 'github' | 'other' +const ICON_SIZE = 40 type NewTrustRelationshipModalProps = { organisationId: number @@ -13,55 +20,70 @@ const NewTrustRelationshipModal: FC = ({ existingAudiences, organisationId, }) => { - const [provider, setProvider] = useState(null) + const [provider, setProvider] = useState( + null, + ) + const formRef = useRef(null) + + useEffect(() => { + // Land on the first field, which depends on the provider and, for GitHub, on + // whether the integration is installed. + formRef.current + ?.querySelector( + 'input:not([readonly]):not([type=hidden]), textarea', + ) + ?.focus() + }, [provider]) - if (provider === 'github') { + if (!provider) { return ( - +
+

+ Choose how your CI will authenticate. +

+
+ {TRUST_RELATIONSHIP_PROVIDERS.map((option) => ( + setProvider(option)} + icon={option.icon(ICON_SIZE)} + title={option.label} + description={option.description} + badge={option.badge} + /> + ))} +
+
) } - if (provider === 'other') { - return - } return ( -
-
setProvider('github')} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') setProvider('github') - }} - > -
GitHub Actions
-
- Let workflows in a GitHub repository authenticate with their OIDC job - token. Recommended if your CI runs on GitHub Actions. + <> +
+
+ Provider
-
-
setProvider('other')} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') setProvider('other') - }} - > -
Other OIDC provider
-
- Configure a custom issuer, audience and claim matching rules for any - OIDC identity provider, such as GitLab CI or Kubernetes. +
+ + {provider.icon(ICON_SIZE - 12)} + +
{provider.label}
+
-
+
+ {provider.key === 'github' ? ( + + ) : ( + + )} +
+ ) } diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.scss b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.scss new file mode 100644 index 000000000000..3c9d39dc7bb8 --- /dev/null +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.scss @@ -0,0 +1,19 @@ +// Layout, spacing and radius come from utilities; this is what they cannot +// express. `border-1` is not an option here: it uses a black alpha, so it all +// but disappears in dark mode. +.provider-card { + border: 1px solid var(--color-border-default); + + &:hover { + border-color: var(--color-border-strong); + background: var(--color-surface-subtle); + } + + &__body { + min-width: 0; + } + + &__title { + font-weight: var(--font-weight-medium); + } +} diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.tsx b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.tsx new file mode 100644 index 000000000000..ac407f290cf6 --- /dev/null +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/ProviderCard.tsx @@ -0,0 +1,50 @@ +import { FC, ReactNode } from 'react' +import { colorIconSecondary } from 'common/theme/tokens' +import BareButton from 'components/base/forms/BareButton' +import Chip from 'components/base/Chip' +import Icon from 'components/icons/Icon' +import './ProviderCard.scss' + +export type ProviderCardProps = { + icon: ReactNode + title: string + description: string + badge?: string + onClick: () => void +} + +const ProviderCard: FC = ({ + badge, + description, + icon, + onClick, + title, +}) => ( + + + {icon} + + + + {title} + {!!badge && ( + + {badge} + + )} + + {description} + + + + + +) + +export default ProviderCard diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/index.ts b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/index.ts new file mode 100644 index 000000000000..2723dde7790c --- /dev/null +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/ProviderCard/index.ts @@ -0,0 +1,2 @@ +export { default } from './ProviderCard' +export type { ProviderCardProps } from './ProviderCard' diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/github.ts b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/github.ts index e5d1e20d1647..277b0d6a0f7e 100644 --- a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/github.ts +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/github.ts @@ -1,6 +1,7 @@ import { TrustRelationship } from 'common/types/responses' export const GITHUB_ISSUER = 'https://token.actions.githubusercontent.com' +export const GITHUB_LABEL = 'GitHub Actions' // Claims the GitHub form can round-trip; anything else edits as freeform. const GITHUB_FORM_CLAIMS = ['repository', 'repository_id', 'environment'] diff --git a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/providers.tsx b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/providers.tsx index 5fe06015f93f..95a6ec192303 100644 --- a/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/providers.tsx +++ b/frontend/web/components/pages/organisation-settings/tabs/trust-relationships/providers.tsx @@ -1,20 +1,44 @@ import React, { ReactNode } from 'react' +import { colorIconDefault } from 'common/theme/tokens' import { GithubIcon } from 'components/icons/GithubIcon' -import { GITHUB_ISSUER } from './github' +import Icon from 'components/icons/Icon' +import { GITHUB_ISSUER, GITHUB_LABEL } from './github' -// Known OIDC providers, keyed by issuer. Add an entry here to give a future -// preset (GitLab CI, Kubernetes, ...) its own badge in the list view. +export type TrustRelationshipProviderKey = 'github' | 'other' + +// The providers the create flow offers, and the badges the list view shows. +// Add an entry here to introduce a preset (GitLab CI, Kubernetes, ...). export type TrustRelationshipProvider = { - issuer: string + key: TrustRelationshipProviderKey label: string - icon: ReactNode + description: string + // The list view badges an issuer it recognises; a freeform provider has none. + issuer?: string + badge?: string + // Sized by the caller: the list badge is small, the create flow's card is not. + icon: (size: number) => ReactNode } export const TRUST_RELATIONSHIP_PROVIDERS: TrustRelationshipProvider[] = [ { - icon: , + badge: 'Recommended', + description: + 'Let workflows in a GitHub repository authenticate with their OIDC job token. Recommended if your CI runs on GitHub Actions.', + icon: (size) => ( + + ), issuer: GITHUB_ISSUER, - label: 'GitHub Actions', + key: 'github', + label: GITHUB_LABEL, + }, + { + description: + 'Configure a custom issuer, audience and claim matching rules for any OIDC identity provider, such as GitLab CI or Kubernetes.', + icon: (size) => ( + + ), + key: 'other', + label: 'Other OIDC provider', }, ]