Skip to content

Commit a49677e

Browse files
committed
refactor(settings): one prop-driven Sources list, SSO sub-views and URL tabs, shared parts on SCIM, admin, Mothership, Search MCP and Slack
1 parent a9937b0 commit a49677e

39 files changed

Lines changed: 1757 additions & 1831 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Button } from '@sim/emcn/button'
2+
import Link from 'next/link'
3+
4+
interface ExternalLinkProps {
5+
href: string
6+
/** The link's whole name, rendered once: Docs, Managed keys, Open in Slack. */
7+
label: string
8+
}
9+
10+
/**
11+
* The one way an organization Settings page links out: a quiet compact button that opens `href` in a
12+
* new tab, its label rendered once and the external-link glyph trailing. A page-level link (a guide)
13+
* sits in the control bar under the page header; a record's link sits with that record.
14+
*/
15+
export function ExternalLink({ href, label }: ExternalLinkProps) {
16+
return (
17+
<Button
18+
variant='quiet'
19+
size='compact'
20+
trailingIcon='I037'
21+
render={<Link href={href} target='_blank' rel='noopener noreferrer' />}
22+
>
23+
{label}
24+
</Button>
25+
)
26+
}

‎apps/sim/app/(product)/o/[organizationId]/settings/_components/sections/admin/admin-directory.tsx‎

Lines changed: 112 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22

33
import { useState } from 'react'
44
import { Button } from '@sim/emcn/button'
5-
import { EmptyState } from '@sim/emcn/empty-state'
65
import { TextField } from '@sim/emcn/field'
76
import { ActionMenu } from '@sim/emcn/menu/action-menu'
87
import { Modal } from '@sim/emcn/modal'
98
import { Pagination } from '@sim/emcn/pagination'
109
import { SearchField } from '@sim/emcn/search-field'
11-
import { SettingsRow, SettingsSection } from '@sim/emcn/settings-section'
12-
import { InlineNotice, StatusTag } from '@sim/emcn/status'
10+
import { SettingsRow } from '@sim/emcn/settings-section'
11+
import { FailureNotice, InlineNotice, StatusTag } from '@sim/emcn/status'
12+
import { SettingsList } from '@/app/(product)/_components/settings-parts/settings-list'
1313
import {
1414
ActionRow,
15-
FieldStack,
16-
Muted,
15+
SectionStack,
1716
SectionToolbar,
1817
} from '@/app/(product)/_components/settings-parts/settings-parts'
1918
import { AddUserDialog } from '@/app/(product)/o/[organizationId]/settings/_components/sections/admin/add-user-dialog'
@@ -47,107 +46,112 @@ export function AdminDirectory({ directory, onImpersonate }: AdminDirectoryProps
4746
return directory.setRole(pendingUser, pending.action === 'Promote' ? 'admin' : 'user')
4847
}
4948
const action = pending?.action
49+
const userRows = rows.map((user) => {
50+
const busy = directory.busy(user.id)
51+
return (
52+
<SettingsRow key={user.id} label={user.name || '—'} description={user.email}>
53+
<ActionRow>
54+
<StatusTag
55+
label={user.role === 'admin' ? 'Admin' : 'User'}
56+
intent={user.role === 'admin' ? 'info' : 'neutral'}
57+
/>
58+
{user.banned && <StatusTag label='Banned' intent='danger' />}
59+
{user.id !== directory.operatorId && (
60+
<>
61+
<Button
62+
size='compact'
63+
disabled={directory.impersonating || busy}
64+
onClick={() => onImpersonate(user)}
65+
>
66+
Impersonate
67+
</Button>
68+
<ActionMenu
69+
label={`${user.email} actions`}
70+
icon='I108'
71+
align='end'
72+
actions={[
73+
{
74+
id: 'reset',
75+
label: 'Reset password',
76+
disabled: busy,
77+
onSelect: () => directory.resetPassword(user),
78+
},
79+
{
80+
id: 'role',
81+
label: user.role === 'admin' ? 'Demote' : 'Promote',
82+
destructive: user.role === 'admin',
83+
disabled: busy,
84+
onSelect: () =>
85+
setPending({
86+
id: user.id,
87+
action: user.role === 'admin' ? 'Demote' : 'Promote',
88+
}),
89+
},
90+
user.banned
91+
? {
92+
id: 'unban',
93+
label: 'Unban',
94+
disabled: busy,
95+
onSelect: () => directory.unban(user),
96+
}
97+
: {
98+
id: 'ban',
99+
label: 'Ban',
100+
separatorBefore: true,
101+
destructive: true,
102+
disabled: busy,
103+
onSelect: () => {
104+
setReason('')
105+
setPending({ id: user.id, action: 'Ban' })
106+
},
107+
},
108+
]}
109+
/>
110+
</>
111+
)}
112+
</ActionRow>
113+
</SettingsRow>
114+
)
115+
})
50116
return (
51-
<SettingsSection title='User management'>
52-
<FieldStack>
53-
<DirectorySearch
54-
key={q}
55-
initial={q}
56-
searching={directory.searching}
57-
onSearch={(term) => void setSearch({ q: term || null, offset: null })}
58-
onAdd={() => setAdding(true)}
117+
<SectionStack>
118+
<DirectorySearch
119+
key={q}
120+
initial={q}
121+
searching={directory.searching}
122+
onSearch={(term) => void setSearch({ q: term || null, offset: null })}
123+
onAdd={() => setAdding(true)}
124+
/>
125+
{directory.warning && <InlineNotice intent='warning' message={directory.warning} />}
126+
{q ? (
127+
directory.error ? (
128+
<FailureNotice
129+
message={directory.error}
130+
retrying={directory.searching}
131+
onRetry={directory.retry}
132+
/>
133+
) : (
134+
results && (
135+
<SettingsList
136+
title='Users'
137+
empty={rows.length === 0 ? { title: 'No matching users' } : undefined}
138+
>
139+
{userRows}
140+
</SettingsList>
141+
)
142+
)
143+
) : (
144+
rows.length > 0 && <SettingsList title='Recently impersonated'>{userRows}</SettingsList>
145+
)}
146+
{q && pages > 1 && (
147+
<Pagination
148+
label='User search pages'
149+
page={page}
150+
pageCount={pages}
151+
disabled={directory.searching}
152+
onPageChange={(next) => void setSearch({ offset: (next - 1) * ADMIN_PAGE_SIZE })}
59153
/>
60-
{directory.error && <InlineNotice intent='danger' message={directory.error} />}
61-
{directory.warning && <InlineNotice intent='warning' message={directory.warning} />}
62-
{q && results && rows.length === 0 && <EmptyState title='No matching users' />}
63-
{rows.length > 0 && (
64-
<div>
65-
{!q && <Muted>Recently impersonated</Muted>}
66-
{rows.map((user) => {
67-
const busy = directory.busy(user.id)
68-
return (
69-
<SettingsRow key={user.id} field label={user.name || '—'} description={user.email}>
70-
<ActionRow>
71-
<StatusTag
72-
label={user.role}
73-
intent={user.role === 'admin' ? 'info' : 'neutral'}
74-
/>
75-
<StatusTag
76-
label={user.banned ? 'Banned' : 'Active'}
77-
intent={user.banned ? 'danger' : 'success'}
78-
/>
79-
{user.id !== directory.operatorId && (
80-
<>
81-
<Button
82-
size='compact'
83-
disabled={directory.impersonating || busy}
84-
onClick={() => onImpersonate(user)}
85-
>
86-
Impersonate
87-
</Button>
88-
<ActionMenu
89-
label={`${user.email} actions`}
90-
icon='I108'
91-
align='end'
92-
actions={[
93-
{
94-
id: 'reset',
95-
label: 'Reset password',
96-
disabled: busy,
97-
onSelect: () => directory.resetPassword(user),
98-
},
99-
{
100-
id: 'role',
101-
label: user.role === 'admin' ? 'Demote' : 'Promote',
102-
destructive: user.role === 'admin',
103-
disabled: busy,
104-
onSelect: () =>
105-
setPending({
106-
id: user.id,
107-
action: user.role === 'admin' ? 'Demote' : 'Promote',
108-
}),
109-
},
110-
user.banned
111-
? {
112-
id: 'unban',
113-
label: 'Unban',
114-
disabled: busy,
115-
onSelect: () => directory.unban(user),
116-
}
117-
: {
118-
id: 'ban',
119-
label: 'Ban',
120-
separatorBefore: true,
121-
destructive: true,
122-
disabled: busy,
123-
onSelect: () => {
124-
setReason('')
125-
setPending({ id: user.id, action: 'Ban' })
126-
},
127-
},
128-
]}
129-
/>
130-
</>
131-
)}
132-
</ActionRow>
133-
</SettingsRow>
134-
)
135-
})}
136-
</div>
137-
)}
138-
{q && pages > 1 && (
139-
<>
140-
<Muted>{`Page ${page} of ${pages} (${results?.total ?? 0} users)`}</Muted>
141-
<Pagination
142-
label='User search pages'
143-
page={page}
144-
pageCount={pages}
145-
disabled={directory.searching}
146-
onPageChange={(next) => void setSearch({ offset: (next - 1) * ADMIN_PAGE_SIZE })}
147-
/>
148-
</>
149-
)}
150-
</FieldStack>
154+
)}
151155
{adding && (
152156
<AddUserDialog
153157
onClose={() => setAdding(false)}
@@ -164,13 +168,13 @@ export function AdminDirectory({ directory, onImpersonate }: AdminDirectoryProps
164168
onOpenChange={(open) => {
165169
if (!open) setPending(undefined)
166170
}}
167-
title={`${action} user`}
171+
title={`${action} ${pendingUser.name || pendingUser.email}`}
168172
description={
169173
action === 'Ban'
170-
? `Banning ${pendingUser.email} signs them out everywhere and blocks them from signing back in. You can unban them later.`
174+
? `Banning ${pendingUser.email} signs them out everywhere and blocks them from signing back in. You can unban them later`
171175
: action === 'Demote'
172176
? `Demoting ${pendingUser.email} removes their platform admin access.`
173-
: `Promoting ${pendingUser.email} grants full platform admin access, including impersonating any user.`
177+
: `Promoting ${pendingUser.email} grants full platform admin access, including impersonating any user`
174178
}
175179
confirmLabel={pending.action}
176180
destructive={action !== 'Promote'}
@@ -187,7 +191,7 @@ export function AdminDirectory({ directory, onImpersonate }: AdminDirectoryProps
187191
)}
188192
</Modal>
189193
)}
190-
</SettingsSection>
194+
</SectionStack>
191195
)
192196
}
193197

‎apps/sim/app/(product)/o/[organizationId]/settings/_components/sections/admin/admin-panel.tsx‎

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { Button } from '@sim/emcn/button'
55
import { Select } from '@sim/emcn/picker/select'
66
import { SettingsRow, SettingsSection } from '@sim/emcn/settings-section'
77
import { FailureNotice } from '@sim/emcn/status'
8-
import { Switch } from '@sim/emcn/switch'
98
import Link from 'next/link'
109
import { SectionStack } from '@/app/(product)/_components/settings-parts/settings-parts'
10+
import { SwitchRow } from '@/app/(product)/_components/settings-parts/switch-row'
1111
import { AdminDirectory } from '@/app/(product)/o/[organizationId]/settings/_components/sections/admin/admin-directory'
1212
import { WorkflowImport } from '@/app/(product)/o/[organizationId]/settings/_components/sections/admin/workflow-import'
1313
import type { AdminConsole, ImportInputs } from '@/platform/lib/data/admin-model'
@@ -35,27 +35,20 @@ export function AdminPanel({ console: admin }: { console: AdminConsole }) {
3535
<SectionStack>
3636
<SettingsSection title='Administration'>
3737
{settings === null ? (
38-
<FailureNotice message='Could not load admin settings.' onRetry={admin.retrySettings} />
38+
<FailureNotice message='Could not load admin settings' onRetry={admin.retrySettings} />
3939
) : (
4040
settings && (
4141
<>
42-
<SettingsRow
42+
<SwitchRow
4343
label='Super admin mode'
4444
description='Act with platform administrator access'
45-
>
46-
<Switch
47-
label='Super admin mode'
48-
checked={settings.superUserModeEnabled}
49-
disabled={admin.savingSetting}
50-
onCheckedChange={admin.setSuperUserMode}
51-
/>
52-
</SettingsRow>
45+
checked={settings.superUserModeEnabled}
46+
disabled={admin.savingSetting}
47+
onCheckedChange={admin.setSuperUserMode}
48+
/>
5349
{settings.superUserModeEnabled && admin.benchmarkAvailable && (
5450
<SettingsRow label='Benchmarks' description='Open the agent benchmark runs'>
55-
<Button
56-
size='compact'
57-
render={<Link href={BENCHMARK_HREF}>Open benchmarks</Link>}
58-
>
51+
<Button size='compact' render={<Link href={BENCHMARK_HREF} />}>
5952
Open benchmarks
6053
</Button>
6154
</SettingsRow>

‎apps/sim/app/(product)/o/[organizationId]/settings/_components/sections/admin/fixture-admin-panel.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export function FixtureAdminPanel({ example }: { example: AdminExample }) {
8484
? { users: matches.slice(offset, offset + ADMIN_PAGE_SIZE), total: matches.length }
8585
: undefined,
8686
searching: false,
87+
retry: () => {},
8788
recent: draft.recent.flatMap((id) => draft.users.filter((user) => user.id === id)),
8889
impersonating: false,
8990
busy: () => false,
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* A field with a manual form: the switch sits under the control, its ink on the field's text edge. */
1+
/* A field with a manual form: the switch sits under the control, at its end. */
22
.paired {
33
display: grid;
44
grid-template-columns: minmax(0, 1fr);
@@ -7,5 +7,4 @@
77
.switch {
88
display: flex;
99
justify-content: flex-end;
10-
margin-inline-end: calc(-1 * var(--ui-action-overhang));
1110
}

0 commit comments

Comments
 (0)