Skip to content

Commit aedcac7

Browse files
feat(OIDC): adjusts for the trust relationships UI (#8313)
1 parent cd829b2 commit aedcac7

14 files changed

Lines changed: 272 additions & 148 deletions

File tree

frontend/web/components/Switch.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import RCSwitch, { Props as RCSwitchProps } from 'rc-switch'
33
import Icon from './icons/Icon'
44

55
export type SwitchProps = RCSwitchProps & {
6+
id?: string
67
checked?: boolean
78
darkMode?: boolean
89
offMarkup?: React.ReactNode

frontend/web/components/base/forms/FieldLabel.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Tooltip, { TooltipProps } from 'components/Tooltip'
66
interface FieldLabelProps {
77
// Associates the label with its control; required for accessibility.
88
htmlFor?: string
9+
id?: string
910
children: ReactNode
1011
// Shows a danger asterisk after the label.
1112
required?: boolean
@@ -22,11 +23,12 @@ const FieldLabel: FC<FieldLabelProps> = ({
2223
children,
2324
className,
2425
htmlFor,
26+
id,
2527
required,
2628
tooltip,
2729
tooltipPlace = 'top',
2830
}) => (
29-
<label htmlFor={htmlFor} className={cn('control-label', className)}>
31+
<label id={id} htmlFor={htmlFor} className={cn('control-label', className)}>
3032
{children}
3133
{required && (
3234
<span className='text-danger ml-1' aria-hidden>

frontend/web/components/icons/GithubIcon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface GithubIconProps {
99

1010
export const GithubIcon: React.FC<GithubIconProps> = ({
1111
className = '',
12-
fill = '#000000',
12+
fill = 'currentColor',
1313
height = 14,
1414
width = 14,
1515
}) => {

frontend/web/components/pages/organisation-settings/tabs/trust-relationships/GithubTrustRelationshipForm/GithubTrustRelationshipForm.tsx

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React, { FC, useEffect, useMemo, useState } from 'react'
22
import Button from 'components/base/forms/Button'
33
import ErrorMessage from 'components/ErrorMessage'
4-
import Input from 'components/base/forms/Input'
54
import InputGroup from 'components/base/forms/InputGroup'
5+
import Link from 'components/base/link'
66
import Utils from 'common/utils/utils'
77
import {
88
Repository,
@@ -211,14 +211,12 @@ const GithubTrustRelationshipForm: FC<GithubTrustRelationshipFormProps> = ({
211211
/>
212212
<div className='text-muted mb-3'>
213213
Install the{' '}
214-
<Button
215-
theme='text'
216-
className='fw-normal'
214+
<Link
217215
href={`/organisation/${organisationId}/integrations`}
218216
target='_blank'
219217
>
220218
Flagsmith GitHub integration
221-
</Button>{' '}
219+
</Link>{' '}
222220
to pick repositories from a list.
223221
</div>
224222
</>
@@ -227,13 +225,8 @@ const GithubTrustRelationshipForm: FC<GithubTrustRelationshipFormProps> = ({
227225
repositoryFields = (
228226
<InputGroup
229227
title='Repository'
230-
component={
231-
<Input
232-
className='full-width'
233-
value={`Pinned by repository ID ${pinnedRepoId}`}
234-
readOnly
235-
/>
236-
}
228+
value={`Pinned by repository ID ${pinnedRepoId}`}
229+
inputProps={{ className: 'full-width', readOnly: true }}
237230
/>
238231
)
239232
} else if (installationId) {
@@ -281,9 +274,8 @@ const GithubTrustRelationshipForm: FC<GithubTrustRelationshipFormProps> = ({
281274
<>
282275
<InputGroup
283276
title='Audience'
284-
component={
285-
<Input className='full-width' value={audience} readOnly />
286-
}
277+
value={audience}
278+
inputProps={{ className: 'full-width', readOnly: true }}
287279
/>
288280
<WorkflowSetupSnippet
289281
audience={audience}
@@ -308,9 +300,8 @@ const GithubTrustRelationshipForm: FC<GithubTrustRelationshipFormProps> = ({
308300
<div className='text-right mt-4'>
309301
<Button
310302
onClick={save}
311-
disabled={
312-
(!repoFullName && !isUnresolvedPin) || isCreating || isUpdating
313-
}
303+
disabled={!repoFullName && !isUnresolvedPin}
304+
isLoading={isCreating || isUpdating}
314305
>
315306
{isEdit ? 'Save trust relationship' : 'Create trust relationship'}
316307
</Button>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.new-trust-relationship__provider {
2+
border: 1px solid var(--color-border-default);
3+
}

frontend/web/components/pages/organisation-settings/tabs/trust-relationships/NewTrustRelationshipModal/NewTrustRelationshipModal.tsx

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import React, { FC, useState } from 'react'
1+
import { FC, useEffect, useRef, useState } from 'react'
2+
import Button from 'components/base/forms/Button'
3+
import ProviderCard from 'components/pages/organisation-settings/tabs/trust-relationships/ProviderCard'
4+
import {
5+
TRUST_RELATIONSHIP_PROVIDERS,
6+
TrustRelationshipProvider,
7+
} from 'components/pages/organisation-settings/tabs/trust-relationships/providers'
28
import GithubTrustRelationshipForm from 'components/pages/organisation-settings/tabs/trust-relationships/GithubTrustRelationshipForm'
39
import TrustRelationshipModal from 'components/pages/organisation-settings/tabs/trust-relationships/TrustRelationshipModal'
10+
import './NewTrustRelationshipModal.scss'
411

5-
type Provider = 'github' | 'other'
12+
const ICON_SIZE = 40
613

714
type NewTrustRelationshipModalProps = {
815
organisationId: number
@@ -13,55 +20,70 @@ const NewTrustRelationshipModal: FC<NewTrustRelationshipModalProps> = ({
1320
existingAudiences,
1421
organisationId,
1522
}) => {
16-
const [provider, setProvider] = useState<Provider | null>(null)
23+
const [provider, setProvider] = useState<TrustRelationshipProvider | null>(
24+
null,
25+
)
26+
const formRef = useRef<HTMLDivElement>(null)
27+
28+
useEffect(() => {
29+
// Land on the first field, which depends on the provider and, for GitHub, on
30+
// whether the integration is installed.
31+
formRef.current
32+
?.querySelector<HTMLElement>(
33+
'input:not([readonly]):not([type=hidden]), textarea',
34+
)
35+
?.focus()
36+
}, [provider])
1737

18-
if (provider === 'github') {
38+
if (!provider) {
1939
return (
20-
<GithubTrustRelationshipForm
21-
organisationId={organisationId}
22-
existingAudiences={existingAudiences}
23-
/>
40+
<div className='p-4'>
41+
<p className='text-secondary mb-3'>
42+
Choose how your CI will authenticate.
43+
</p>
44+
<div className='d-flex flex-column gap-3'>
45+
{TRUST_RELATIONSHIP_PROVIDERS.map((option) => (
46+
<ProviderCard
47+
key={option.key}
48+
onClick={() => setProvider(option)}
49+
icon={option.icon(ICON_SIZE)}
50+
title={option.label}
51+
description={option.description}
52+
badge={option.badge}
53+
/>
54+
))}
55+
</div>
56+
</div>
2457
)
2558
}
26-
if (provider === 'other') {
27-
return <TrustRelationshipModal organisationId={organisationId} />
28-
}
2959

3060
return (
31-
<div className='p-4'>
32-
<div
33-
className='panel--grey p-3 mb-3 clickable'
34-
data-test='provider-github'
35-
role='button'
36-
tabIndex={0}
37-
onClick={() => setProvider('github')}
38-
onKeyDown={(e) => {
39-
if (e.key === 'Enter' || e.key === ' ') setProvider('github')
40-
}}
41-
>
42-
<h6 className='mb-1'>GitHub Actions</h6>
43-
<div className='text-muted fs-small'>
44-
Let workflows in a GitHub repository authenticate with their OIDC job
45-
token. Recommended if your CI runs on GitHub Actions.
61+
<>
62+
<div className='px-4 pt-4'>
63+
<div className='fs-small text-secondary text-uppercase mb-2'>
64+
Provider
4665
</div>
47-
</div>
48-
<div
49-
className='panel--grey p-3 clickable'
50-
data-test='provider-other'
51-
role='button'
52-
tabIndex={0}
53-
onClick={() => setProvider('other')}
54-
onKeyDown={(e) => {
55-
if (e.key === 'Enter' || e.key === ' ') setProvider('other')
56-
}}
57-
>
58-
<h6 className='mb-1'>Other OIDC provider</h6>
59-
<div className='text-muted fs-small'>
60-
Configure a custom issuer, audience and claim matching rules for any
61-
OIDC identity provider, such as GitLab CI or Kubernetes.
66+
<div className='new-trust-relationship__provider d-flex align-items-center gap-3 p-3 rounded-lg bg-surface-subtle'>
67+
<span className='d-flex' aria-hidden>
68+
{provider.icon(ICON_SIZE - 12)}
69+
</span>
70+
<div className='flex-fill fw-semibold'>{provider.label}</div>
71+
<Button theme='text' onClick={() => setProvider(null)}>
72+
Change
73+
</Button>
6274
</div>
6375
</div>
64-
</div>
76+
<div ref={formRef}>
77+
{provider.key === 'github' ? (
78+
<GithubTrustRelationshipForm
79+
organisationId={organisationId}
80+
existingAudiences={existingAudiences}
81+
/>
82+
) : (
83+
<TrustRelationshipModal organisationId={organisationId} />
84+
)}
85+
</div>
86+
</>
6587
)
6688
}
6789

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Layout, spacing and radius come from utilities; this is what they cannot
2+
// express. `border-1` is not an option here: it uses a black alpha, so it all
3+
// but disappears in dark mode.
4+
.provider-card {
5+
border: 1px solid var(--color-border-default);
6+
7+
&:hover {
8+
border-color: var(--color-border-strong);
9+
background: var(--color-surface-subtle);
10+
}
11+
12+
&__body {
13+
min-width: 0;
14+
}
15+
16+
&__title {
17+
font-weight: var(--font-weight-medium);
18+
}
19+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { FC, ReactNode } from 'react'
2+
import { colorIconSecondary } from 'common/theme/tokens'
3+
import BareButton from 'components/base/forms/BareButton'
4+
import Chip from 'components/base/Chip'
5+
import Icon from 'components/icons/Icon'
6+
import './ProviderCard.scss'
7+
8+
export type ProviderCardProps = {
9+
icon: ReactNode
10+
title: string
11+
description: string
12+
badge?: string
13+
onClick: () => void
14+
}
15+
16+
const ProviderCard: FC<ProviderCardProps> = ({
17+
badge,
18+
description,
19+
icon,
20+
onClick,
21+
title,
22+
}) => (
23+
<BareButton
24+
className='provider-card d-flex align-items-center gap-3 w-100 p-3 text-start rounded-xl transition-fast'
25+
onClick={onClick}
26+
>
27+
<span
28+
className='d-flex align-items-center justify-content-center flex-shrink-0 p-2 rounded-lg bg-surface-muted'
29+
aria-hidden
30+
>
31+
{icon}
32+
</span>
33+
<span className='provider-card__body d-flex flex-column gap-1 flex-fill'>
34+
<span className='d-flex align-items-center gap-2 flex-wrap'>
35+
<span className='provider-card__title'>{title}</span>
36+
{!!badge && (
37+
<Chip size='xs' variant='accent'>
38+
{badge}
39+
</Chip>
40+
)}
41+
</span>
42+
<span className='fs-small text-secondary'>{description}</span>
43+
</span>
44+
<span className='d-flex align-items-center flex-shrink-0' aria-hidden>
45+
<Icon name='chevron-right' width={20} fill={colorIconSecondary} />
46+
</span>
47+
</BareButton>
48+
)
49+
50+
export default ProviderCard
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './ProviderCard'
2+
export type { ProviderCardProps } from './ProviderCard'

0 commit comments

Comments
 (0)