Skip to content

Commit ffabae1

Browse files
emir-karabegwaleedlatif1
authored andcommitted
Connect member accounts inline with canonical provider descriptions
1 parent 1f8c01d commit ffabae1

29 files changed

Lines changed: 788 additions & 402 deletions
Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
.connect {
2-
display: flex;
3-
flex-direction: column;
4-
align-items: center;
5-
justify-content: center;
6-
gap: var(--ui-section-gap);
2+
display: grid;
73
block-size: var(--ui-widget-max-size);
84
min-block-size: 0;
9-
padding: var(--spacing-16);
10-
border: var(--spacing-stroke-default) solid var(--ui-divider);
11-
border-radius: var(--radius-menu-shared);
12-
text-align: center;
13-
}
14-
.connect p {
15-
color: var(--ui-secondary);
16-
}
17-
.providers {
18-
display: flex;
19-
flex-wrap: wrap;
20-
justify-content: center;
21-
gap: var(--ui-row-gap);
225
}

‎apps/sim/platform/app/(shell)/home/_components/connected-widget/connected-widget.tsx‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import { type ReactNode, useState } from 'react'
44
import styles from '@/platform/app/(shell)/home/_components/connected-widget/connected-widget.module.css'
55
import { HomeSection } from '@/platform/app/(shell)/home/_components/home-section/home-section'
6-
import { ActionMenu, Button } from '@/platform/design-system/components'
6+
import { ActionMenu } from '@/platform/design-system/components'
7+
import { ConnectionPrompt } from '@/platform/design-system/components/connection-prompt/connection-prompt'
78
import type { MenuAction } from '@/platform/design-system/components/menu/action-menu'
89
import { Modal } from '@/platform/design-system/components/modal/modal'
910

@@ -53,20 +54,15 @@ export function ConnectedWidget({
5354
{connected ? (
5455
children
5556
) : (
56-
<div className={styles.connect} data-material='shell'>
57-
<p data-type='T060'>Connect your {title.toLowerCase()}</p>
58-
<div className={styles.providers}>
59-
{integrations[title].map(({ name, icon }) => (
60-
<Button
61-
key={name}
62-
disabled={!onConnectionChange}
63-
icon={icon}
64-
onClick={() => onConnectionChange?.(true)}
65-
>
66-
{name}
67-
</Button>
68-
))}
69-
</div>
57+
<div className={styles.connect}>
58+
<ConnectionPrompt
59+
title={`Connect your ${title.toLowerCase()}`}
60+
options={integrations[title].map(({ name, icon }) => ({
61+
label: name,
62+
icon,
63+
onConnect: onConnectionChange ? () => onConnectionChange(true) : undefined,
64+
}))}
65+
/>
7066
</div>
7167
)}
7268
<Modal

‎apps/sim/platform/app/(shell)/integrations/_components/account-authorization/account-authorization.tsx‎

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.scopes,
2+
.scope {
3+
display: grid;
4+
min-inline-size: 0;
5+
}
6+
.scopes {
7+
gap: var(--ui-section-gap);
8+
}
9+
.scope {
10+
gap: var(--ui-stack-gap);
11+
overflow-wrap: anywhere;
12+
}
13+
.scope > span + span {
14+
color: var(--ui-secondary);
15+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import styles from '@/platform/app/(shell)/integrations/_components/connection-access/connection-access.module.css'
2+
import { Disclosure } from '@/platform/design-system/components'
3+
import type { OrganizationIntegration } from '@/platform/lib/integrations/model'
4+
5+
interface ConnectionAccessProps {
6+
integration: OrganizationIntegration
7+
}
8+
9+
export function ConnectionAccess({ integration }: ConnectionAccessProps) {
10+
const scopes = integration.requestedScopes
11+
if (!scopes?.length) return null
12+
return (
13+
<Disclosure label='Requested permissions' variant='activity'>
14+
<ul className={styles.scopes} aria-label='Requested provider permissions'>
15+
{scopes.map(({ id, label }) => (
16+
<li key={id} className={styles.scope}>
17+
<span data-type='T059'>{label}</span>
18+
{label !== id && <span data-type='T050'>{id}</span>}
19+
</li>
20+
))}
21+
</ul>
22+
</Disclosure>
23+
)
24+
}

‎apps/sim/platform/app/(shell)/integrations/_components/integration-details/integration-details.module.css‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,13 @@
1010
.account {
1111
gap: var(--ui-section-gap);
1212
}
13-
.header,
14-
.actions {
13+
.header {
1514
display: flex;
1615
flex-wrap: wrap;
1716
align-items: center;
18-
}
19-
.header {
2017
justify-content: space-between;
2118
gap: var(--ui-row-gap);
2219
}
23-
.actions {
24-
gap: var(--ui-action-gap);
25-
}
2620
.list {
2721
gap: var(--ui-list-gap);
2822
margin-inline: calc(-1 * var(--ui-row-inset));

‎apps/sim/platform/app/(shell)/integrations/_components/integration-details/integration-details.tsx‎

Lines changed: 50 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22

33
import { useState } from 'react'
44
import { useRouter, useSearchParams } from 'next/navigation'
5-
import { AccountAuthorization } from '@/platform/app/(shell)/integrations/_components/account-authorization/account-authorization'
5+
import { ConnectionAccess } from '@/platform/app/(shell)/integrations/_components/connection-access/connection-access'
66
import styles from '@/platform/app/(shell)/integrations/_components/integration-details/integration-details.module.css'
77
import { ToolPermissions } from '@/platform/app/(shell)/integrations/_components/tool-permissions/tool-permissions'
88
import {
99
ActionMenu,
1010
ActivityRow,
11-
Button,
1211
Chip,
1312
EmptyState,
1413
Modal,
1514
} from '@/platform/design-system/components'
15+
import { ConnectionPrompt } from '@/platform/design-system/components/connection-prompt/connection-prompt'
1616
import {
1717
SettingsRow,
1818
SettingsSurface,
@@ -24,26 +24,23 @@ interface IntegrationDetailsProps {
2424
onChange: (next: OrganizationIntegration) => void
2525
}
2626

27-
type AccountAction =
28-
| { kind: 'connect' }
29-
| { kind: 'reconnect' | 'disconnect'; account: IntegrationAccount }
30-
3127
function accountStatus(account: IntegrationAccount) {
3228
return account.status === 'active' ? 'Connected' : 'Reconnect needed'
3329
}
3430

3531
export function IntegrationDetails({ integration, onChange }: IntegrationDetailsProps) {
3632
const router = useRouter()
3733
const search = useSearchParams()
38-
const [action, setAction] = useState<AccountAction | null>(null)
34+
const [removing, setRemoving] = useState<IntegrationAccount | null>(null)
3935
const { accounts, providerId, connectableMethod } = integration
4036
const requested = search.get('account')
4137
const selected = requested
4238
? accounts.find((entry) => entry.id === requested)
4339
: accounts.length === 1
4440
? accounts[0]
4541
: undefined
46-
const canConnect = integration.availability === 'ready' && connectableMethod !== null
42+
const canConnect =
43+
!accounts.length && integration.availability === 'ready' && connectableMethod !== null
4744
const candidate = integration.connectionCandidate
4845
const canAttach =
4946
integration.fixtureAuthorization &&
@@ -59,6 +56,26 @@ export function IntegrationDetails({ integration, onChange }: IntegrationDetails
5956
return `/integrations/${encodeURIComponent(providerId)}${next.size ? `?${next}` : ''}`
6057
}
6158

59+
function connect() {
60+
if (!canAttach || !candidate) return
61+
onChange({
62+
...integration,
63+
accounts: [candidate],
64+
connectableMethod: null,
65+
connectionCandidate: undefined,
66+
})
67+
}
68+
69+
function reconnect(account: IntegrationAccount) {
70+
if (!integration.fixtureAuthorization || !account.canReconnect) return
71+
onChange({
72+
...integration,
73+
accounts: accounts.map((item) =>
74+
item.id === account.id ? { ...item, status: 'active' } : item
75+
),
76+
})
77+
}
78+
6279
function disconnect(account: IntegrationAccount) {
6380
const restore = integration.availability === 'ready' && account.canReconnect
6481
onChange({
@@ -95,27 +112,16 @@ export function IntegrationDetails({ integration, onChange }: IntegrationDetails
95112
)
96113
}
97114
if (integration.availability === 'managed' && !accounts.length) {
98-
return (
99-
<EmptyState
100-
title='Organization managed'
101-
description='Your access follows your GitLab project permissions.'
102-
/>
103-
)
115+
return <EmptyState title='Organization managed' description={integration.accessDescription} />
104116
}
105117

118+
const accountHeading = !selected && accounts.length > 1 ? 'Accounts' : 'Account'
106119
return (
107120
<div className={styles.sections}>
108-
<section className={styles.account} aria-label={selected ? 'Account' : 'Accounts'}>
121+
<section className={styles.account} aria-label={accountHeading}>
109122
<header className={styles.header}>
110-
<h2 data-type='T093'>{selected ? 'Account' : 'Accounts'}</h2>
111-
<div className={styles.actions}>
112-
{selected && accounts.length > 1 && <Chip href={accountHref()}>All accounts</Chip>}
113-
{canConnect && accounts.length > 0 && (
114-
<Button size='compact' onClick={() => setAction({ kind: 'connect' })}>
115-
Add account
116-
</Button>
117-
)}
118-
</div>
123+
<h2 data-type='T093'>{accountHeading}</h2>
124+
{selected && accounts.length > 1 && <Chip href={accountHref()}>All accounts</Chip>}
119125
</header>
120126
{selected ? (
121127
<SettingsSurface>
@@ -131,7 +137,8 @@ export function IntegrationDetails({ integration, onChange }: IntegrationDetails
131137
{
132138
id: 'reconnect',
133139
label: 'Reconnect account',
134-
onSelect: () => setAction({ kind: 'reconnect', account: selected }),
140+
disabled: !integration.fixtureAuthorization,
141+
onSelect: () => reconnect(selected),
135142
},
136143
]
137144
: []),
@@ -140,7 +147,7 @@ export function IntegrationDetails({ integration, onChange }: IntegrationDetails
140147
label: 'Disconnect account',
141148
destructive: true,
142149
separatorBefore: selected.canReconnect,
143-
onSelect: () => setAction({ kind: 'disconnect', account: selected }),
150+
onSelect: () => setRemoving(selected),
144151
},
145152
]}
146153
/>
@@ -157,18 +164,21 @@ export function IntegrationDetails({ integration, onChange }: IntegrationDetails
157164
/>
158165
))}
159166
</ul>
160-
) : (
161-
<EmptyState
162-
title='No accounts connected'
163-
action={
164-
canConnect ? (
165-
<Button size='compact' onClick={() => setAction({ kind: 'connect' })}>
166-
Connect account
167-
</Button>
168-
) : undefined
169-
}
167+
) : canConnect ? (
168+
<ConnectionPrompt
169+
title={`Connect your ${integration.name} account`}
170+
options={[
171+
{
172+
label: integration.name,
173+
icon: { providerId: integration.providerId },
174+
onConnect: canAttach ? connect : undefined,
175+
},
176+
]}
170177
/>
178+
) : (
179+
<EmptyState title='No accounts connected' description={integration.accessDescription} />
171180
)}
181+
<ConnectionAccess integration={integration} />
172182
</section>
173183
{selected?.permissions && (
174184
<ToolPermissions
@@ -185,56 +195,16 @@ export function IntegrationDetails({ integration, onChange }: IntegrationDetails
185195
}
186196
/>
187197
)}
188-
{action?.kind === 'disconnect' && (
198+
{removing && (
189199
<Modal
190200
open
191-
onOpenChange={() => setAction(null)}
201+
onOpenChange={() => setRemoving(null)}
192202
title='Disconnect account?'
193203
variant='confirm'
194204
destructive
195-
description={`Sim will lose access to ${action.account.label}.`}
205+
description={`Sim will lose access to ${removing.label}.`}
196206
confirmLabel='Disconnect'
197-
onConfirm={() => disconnect(action.account)}
198-
/>
199-
)}
200-
{action?.kind === 'connect' && canConnect && connectableMethod && (
201-
<AccountAuthorization
202-
provider={integration.name}
203-
method={connectableMethod}
204-
candidateLabel={candidate?.label}
205-
onClose={() => setAction(null)}
206-
onAuthorize={
207-
canAttach && candidate
208-
? () => {
209-
onChange({
210-
...integration,
211-
accounts: [...accounts, candidate],
212-
connectableMethod: null,
213-
connectionCandidate: undefined,
214-
})
215-
router.replace(accountHref(candidate.id), { scroll: false })
216-
}
217-
: undefined
218-
}
219-
/>
220-
)}
221-
{action?.kind === 'reconnect' && action.account.canReconnect && (
222-
<AccountAuthorization
223-
provider={integration.name}
224-
method={action.account.method}
225-
account={action.account}
226-
onClose={() => setAction(null)}
227-
onAuthorize={
228-
integration.fixtureAuthorization
229-
? () =>
230-
onChange({
231-
...integration,
232-
accounts: accounts.map((entry) =>
233-
entry.id === action.account.id ? { ...entry, status: 'active' } : entry
234-
),
235-
})
236-
: undefined
237-
}
207+
onConfirm={() => disconnect(removing)}
238208
/>
239209
)}
240210
</div>

‎apps/sim/platform/app/(shell)/integrations/_components/integration-list/integration-list.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export function IntegrationList({ integrations, secrets }: IntegrationListProps)
3333
name: item.name,
3434
detail: memberStatus(item),
3535
section: sectionFor(item),
36-
searchable: [item.name, ...item.accounts.map(({ label }) => label)].join(' ').toLowerCase(),
36+
searchable: [item.name, item.description, ...item.accounts.map(({ label }) => label)]
37+
.join(' ')
38+
.toLowerCase(),
3739
}))
3840
if (secrets) {
3941
const managed = secrets.mode === 'organization'

0 commit comments

Comments
 (0)