# Settings-01 - Platform Settings (`settings-01`) - SignalOS page block

> Platform settings page with organization profile, roles, users, and custom models. Fully controlled — all state and handlers passed via props.

- **Version:** 0.5.0
- **Kind:** block (group: settings) · **Category:** pages
- **Install:** `npx shadcn@latest add @signalos/settings-01`
- **Registry dependencies (pulled automatically):** @signalos/tokens, @signalos/utils, @signalos/page-header, @signalos/button, @signalos/card, @signalos/dialog, @signalos/dropdown-menu, @signalos/input, @signalos/label, @signalos/skeleton, @signalos/badge, @signalos/data-table, @signalos/form-dialog, @signalos/text-field, @signalos/select, @signalos/icon-button, @signalos/textarea, @signalos/combobox, @signalos/data-primitives
- **npm dependencies:** lucide-react@^1.7.0, class-variance-authority@^0.7.0, @tanstack/react-table@^8.21.3
- **Files installed:** `src/components/blocks/settings-01/Settings01.tsx`, `src/components/blocks/settings-01/Settings01.constants.ts`, `src/components/blocks/settings-01/Settings01.types.ts`, `src/components/blocks/settings-01/components/RoleDialog.tsx`, `src/components/blocks/settings-01/components/OrganizationDialog.tsx`, `src/components/blocks/settings-01/components/CustomForm.tsx`, `src/components/blocks/settings-01/components/UserDialog.tsx`, `src/components/blocks/settings-01/components/UpdatePasswordDialog.tsx`, `src/components/blocks/settings-01/components/RoleCard.tsx`, `src/components/blocks/settings-01/components/RolesSection.tsx`, `src/components/blocks/settings-01/components/CustomModelDialog.tsx`

## Access

This is a private registry: pulling source requires a `SIGNALOS_REGISTRY_TOKEN`
(GitHub fine-grained PAT with read access to the signal-widgets repo) and an
`@signalos` entry in components.json `"registries"`. Previews and this document are public.

## Usage

```tsx
// after: npx shadcn@latest add @signalos/settings-01
// mount the pulled page in a route, e.g. src/app/settings/page.tsx
import Settings01 from "@/components/blocks/settings-01/Settings01"

export default function Page() {
  return <Settings01 />
}
```

## Props

### `OrgProfile`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | yes | - |  |
| `email` | `string` | yes | - |  |
| `role` | `string` | yes | - |  |

### `OrgProfileFormValues`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | yes | - |  |
| `email` | `string` | yes | - |  |

### `AdminRole`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string \| undefined` | no | - |  |
| `name` | `string` | yes | - |  |
| `description` | `string \| undefined` | no | - |  |

### `PlatformUser`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes | - |  |
| `full_name` | `string` | yes | - |  |
| `email` | `string` | yes | - |  |
| `role` | `{ readonly id: string; readonly name: string; }` | yes | - |  |
| `active` | `boolean` | yes | - |  |

### `SelectOption`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | yes | - |  |
| `value` | `string` | yes | - |  |

### `ManagerOption`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `email` | `string \| undefined` | no | - |  |
| `value` | `string` | yes | - |  |
| `label` | `ReactNode` | yes | - |  |
| `icon` | `ReactNode` | no | - |  |
| `disabled` | `boolean \| undefined` | no | - |  |
| `keywords` | `string[] \| undefined` | no | - |  |

### `UserFormValues`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `fullName` | `string` | yes | - |  |
| `escalationManager` | `string` | yes | - |  |
| `role` | `string` | yes | - |  |
| `email` | `string` | yes | - |  |
| `password` | `string` | yes | - |  |

### `UserDialogMode`

```ts
export type UserDialogMode = "create" | "edit"
```

### `UpdatePasswordFormValues`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `password` | `string` | yes | - |  |

### `CustomModelProviderKind`

```ts
export type CustomModelProviderKind =
  | "openai_compatible"
  | "azure_openai"
  | "bedrock"
  | "vertex"
  | "openai"
  | "anthropic"
  | "google_genai"
  | "groq"
  | "mistral"
  | "deepseek"
```

### `CustomModelStatus`

```ts
export type CustomModelStatus = "active" | "error" | "inactive"
```

### `CustomModel`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes | - |  |
| `name` | `string` | yes | - |  |
| `provider_kind` | `CustomModelProviderKind` | yes | - |  |
| `model_id` | `string \| null \| undefined` | no | - |  |
| `endpoint_url` | `string \| null \| undefined` | no | - |  |
| `api_version` | `string \| null \| undefined` | no | - |  |
| `config` | `Record<string, unknown> \| null \| undefined` | no | - |  |
| `has_api_key` | `boolean` | yes | - |  |
| `status` | `CustomModelStatus` | yes | - |  |
| `error_message` | `string \| null \| undefined` | no | - |  |
| `created_at` | `string` | yes | - |  |
| `last_checked_at` | `string \| null \| undefined` | no | - |  |

### `Settings01Props`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `title` | `string \| undefined` | no | `"Platform Settings"` |  |
| `description` | `string \| undefined` | no | `"Configure global operational parameters, manage organizational identity, and orchestrate external data flow within the SignalOS ecosystem."` |  |
| `headerMeta` | `ReactNode` | no | - | Optional content rendered above the title (e.g. an environment or version banner). Omit to render nothing - the block ships no default value here since there is no environment/version string that is correct for every consumer. |
| `usersTitle` | `string \| undefined` | no | `"Users List"` |  |
| `usersDescription` | `string \| undefined` | no | `"Manage users and their access permissions"` |  |
| `customModelsTitle` | `string \| undefined` | no | `"Custom Models"` |  |
| `customModelsDescription` | `string \| undefined` | no | `"Bring your own LLM providers and keys for use across pipelines."` |  |
| `profile` | `OrgProfile` | yes | - |  |
| `dialogOpen` | `boolean` | yes | - |  |
| `onDialogOpenChange` | `(open: boolean) => void` | yes | - |  |
| `formValues` | `OrgProfileFormValues` | yes | - |  |
| `onFormChange` | `(field: keyof OrgProfileFormValues, value: string) => void` | yes | - |  |
| `onSave` | `() => void` | yes | - |  |
| `isSaving` | `boolean \| undefined` | no | - | Shared saving flag for the organization and role dialogs. |
| `orgFormErrors` | `Partial<Record<keyof OrgProfileFormValues, string>> \| undefined` | no | - |  |
| `roles` | `AdminRole[] \| undefined` | no | - |  |
| `isLoadingRoles` | `boolean \| undefined` | no | - |  |
| `deletingRoleId` | `string \| null \| undefined` | no | - | Id of the role currently being deleted (shows the row spinner). |
| `deleteLoading` | `boolean \| undefined` | no | - |  |
| `onAddRole` | `(() => void) \| undefined` | no | - |  |
| `onEditRole` | `((role: AdminRole) => void) \| undefined` | no | - |  |
| `onDeleteRole` | `((id: string) => void) \| undefined` | no | - |  |
| `roleDialogOpen` | `boolean` | yes | - |  |
| `roleDialogMode` | `"edit" \| "create" \| undefined` | no | `"create"` | Whether the role dialog is creating a new role or editing `onEditRole`'s role. |
| `onRoleDialogOpenChange` | `(open: boolean) => void` | yes | - |  |
| `roleForm` | `{ name: string; description: string; }` | yes | - |  |
| `onRoleFormChange` | `(field: "name" \| "description", value: string) => void` | yes | - |  |
| `onRoleSave` | `() => void` | yes | - |  |
| `roleFormErrors` | `Partial<Record<"name" \| "description", string>> \| undefined` | no | - |  |
| `isRolesSaving` | `boolean \| undefined` | no | - | Saving flag for the role dialog only. Falls back to `isSaving` when omitted, so callers that don't distinguish the two keep today's behavior. |
| `users` | `PlatformUser[] \| undefined` | no | - |  |
| `isLoadingUsers` | `boolean \| undefined` | no | - |  |
| `userSearch` | `string \| undefined` | no | - |  |
| `onUserSearchChange` | `((value: string) => void) \| undefined` | no | - |  |
| `selectedUser` | `PlatformUser \| null \| undefined` | no | - |  |
| `onSelectUser` | `((user: PlatformUser \| null) => void) \| undefined` | no | - |  |
| `onAddUser` | `(() => void) \| undefined` | no | - |  |
| `userDialogOpen` | `boolean` | yes | - |  |
| `onUserDialogOpenChange` | `(open: boolean) => void` | yes | - |  |
| `userDialogMode` | `UserDialogMode` | yes | - |  |
| `userForm` | `UserFormValues` | yes | - |  |
| `managerOptions` | `ManagerOption[]` | yes | - |  |
| `onManagerSearchChange` | `((value: string) => void) \| undefined` | no | - | Called as the user types in the escalation-manager combobox search box. |
| `isLoadingManagerOptions` | `boolean \| undefined` | no | `false` | Shows a loading state in the manager combobox while options are fetched. |
| `roleOptions` | `SelectOption[]` | yes | - |  |
| `onUserFormChange` | `(field: keyof UserFormValues, value: string) => void` | yes | - |  |
| `onUserSave` | `() => void` | yes | - |  |
| `isSavingUser` | `boolean \| undefined` | no | - |  |
| `userFormErrors` | `Partial<Record<"role" \| "email" \| "password" \| "fullName", string>> \| undefined` | no | - |  |
| `updatePasswordDialogOpen` | `boolean` | yes | - |  |
| `onUpdatePasswordDialogOpenChange` | `(open: boolean) => void` | yes | - |  |
| `updatePasswordForm` | `UpdatePasswordFormValues` | yes | - |  |
| `onUpdatePasswordChange` | `(value: string) => void` | yes | - |  |
| `onUpdatePasswordSave` | `() => void` | yes | - |  |
| `isSavingPassword` | `boolean \| undefined` | no | - |  |
| `updatePasswordErrors` | `{ password?: string \| undefined; } \| undefined` | no | - |  |
| `userColumns` | `ColumnDef<PlatformUser>[]` | yes | - | Column definitions for the users table - the caller owns row actions. |
| `userPagination` | `{ page: number; pageSize: number; total: number; totalPages: number; pageSizeOptions?: number[] \| undefined; onPageChange: (page: number) => void; onRowsPerPageChange: (pageSize: number) => void; } \| undefined` | no | - |  |
| `customModels` | `CustomModel[] \| undefined` | no | - |  |
| `isLoadingModels` | `boolean \| undefined` | no | - |  |
| `selectedModel` | `CustomModel \| null \| undefined` | no | - |  |
| `onSelectModel` | `((model: CustomModel \| null) => void) \| undefined` | no | - |  |
| `onAddModel` | `(() => void) \| undefined` | no | - |  |
| `modelColumns` | `ColumnDef<CustomModel>[]` | yes | - | Column definitions for the models table - the caller owns row actions. |
| `customModelDialogOpen` | `boolean \| undefined` | no | - |  |
| `onCustomModelDialogOpenChange` | `((open: boolean) => void) \| undefined` | no | - |  |
| `customModelDialogContentClassName` | `string \| undefined` | no | - | Overrides the custom-model dialog's content classes (e.g. to widen it for long provider forms). |
| `providers` | `CustomModelProvider[] \| undefined` | no | - | Providers available in the model dialog's provider select. |
| `isLoadingProviders` | `boolean \| undefined` | no | - |  |
| `editingModel` | `boolean \| undefined` | no | - | True when the dialog is editing an existing model (vs creating one). |
| `editingModelHasStoredSecrets` | `boolean \| undefined` | no | - | True when the model being edited has a stored API key (has_api_key). |
| `modelName` | `string` | yes | - |  |
| `providerKind` | `"" \| CustomModelProviderKind` | yes | - |  |
| `fields` | `CustomModelFields` | yes | - | Dynamic field values keyed by `ProviderField.key`. |
| `selectedProvider` | `CustomModelProvider \| undefined` | no | - |  |
| `isProviderMissing` | `boolean` | yes | - | True when the chosen provider's package is not installed server-side. |
| `isTesting` | `boolean` | yes | - |  |
| `isTestSuccess` | `boolean \| undefined` | no | - |  |
| `testFeedback` | `{ ok: boolean; message: string; } \| null \| undefined` | no | - |  |
| `canTest` | `boolean` | yes | - | True once the form is complete enough to attempt a connection test. |
| `onNameChange` | `(value: string) => void` | yes | - |  |
| `onProviderChange` | `(value: CustomModelProviderKind) => void` | yes | - |  |
| `onFieldChange` | `(key: string, value: string) => void` | yes | - |  |
| `onTest` | `() => void` | yes | - |  |
| `onCustomModelSave` | `(() => void) \| undefined` | no | - |  |
| `isSavingModel` | `boolean` | yes | - |  |

### `ProviderFieldType`

```ts
export type ProviderFieldType = "text" | "url" | "textarea" | "keyvalue"
```

### `ProviderField`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key` | `string` | yes | - |  |
| `label` | `string` | yes | - |  |
| `required` | `boolean` | yes | - |  |
| `secret` | `boolean` | yes | - |  |
| `type` | `ProviderFieldType` | yes | - |  |
| `placeholder` | `string \| undefined` | no | - |  |

### `CustomModelProvider`

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `provider_kind` | `CustomModelProviderKind` | yes | - |  |
| `label` | `string` | yes | - |  |
| `package` | `string` | yes | - |  |
| `installed` | `boolean` | yes | - |  |
| `temperature_max` | `number` | yes | - |  |
| `fields` | `ProviderField[]` | yes | - |  |

### `CustomModelFields`

```ts
export type CustomModelFields = Record<string, string>
```

## Changelog

# settings-01

## 0.5.0

- Add `title`/`description` overrides for the page header, `usersTitle`/
  `usersDescription` for the Users List section, and `customModelsTitle`/
  `customModelsDescription` for the Custom Models section - all default to
  today's copy.
- **Breaking:** remove the hardcoded `"Console v2.4 / Environment:
  Production"` banner. It was fake data baked into every install with no way
  to override or remove it. Replaced with an optional `headerMeta?: ReactNode`
  slot that renders nothing when omitted - pass your own environment/version
  content if you need one.
- Add `roleDialogMode?: "create" | "edit"` (default `"create"`, matching
  today's behavior) so `RoleDialog` can show "Edit Role" copy when editing an
  existing role via `onEditRole`. Add `isRolesSaving` to decouple the role
  dialog's saving state from the organization dialog's `isSaving`.
- `managerOptions` is now typed `ManagerOption[]` (a `ComboboxOption` with an
  optional `email`) instead of the plain `SelectOption[]` - existing
  `SelectOption[]` values still satisfy the new type. The escalation-manager
  combobox now renders an initials avatar + name + email per option instead
  of a plain label. Add `onManagerSearchChange` and `isLoadingManagerOptions`,
  wired into the combobox's search and loading state.
- Add `customModelDialogContentClassName` to override the custom-model
  dialog's sizing (defaults to today's size).
- Add stable `data-testid`s: `users-section`, `org-profile-email-value`,
  `org-profile-role-value`, and the `role-modal-*`/`user-modal-*` family on
  the role and user dialogs' fields, submit, and cancel actions.
- Wrap the organization-profile card and roles section in `self-start`
  containers so the sticky profile card no longer stretches to the roles
  column's height.

## 0.4.0

- Decompose the block: role cards move to `components/RoleCard.tsx` +
  `components/RolesSection.tsx` and the custom-model dialog to
  `components/CustomModelDialog.tsx`; the misspelled `componets/` folder is
  renamed to `components/` (consumer paths change accordingly).
- Role card section labels now use the shared `SectionLabel` from
  `@signalos/data-primitives`; the inline SVG header icon is replaced with a
  lucide icon.
- Add the missing `"use client"` directive and remove non-token `bg-gray-300`
  skeleton overrides.
- Page titles now come from `page-header` defaults (local overrides removed).
- API cleanup (breaking): remove unused props `onCancel`, `onEditUser`,
  `customModelForm`, `onCustomModelNameChange`, `onCustomModelProviderChange`,
  `onCustomModelFieldChange`, `onTestConnection`, `isTestingModel`, and the
  backend payload types that belonged in the app's API layer. All props are
  documented and `readonly` now; `RoleDialog`/`OrganizationDialog` switch to
  named exports. Fix the clear-search button's accessible label.

## 0.3.13

- Add validation for all form fields.

## 0.3.11

- Add `Textarea` primitive (`@signalos/textarea`) and wire it into `RoleDialog` description field.

## 0.3.3

- Version alignment release (no content change recorded).

## 0.3.0

- Replace Users and Custom Models list views with the `DataTable` widget.
- Add `@signalos/data-table` registry dependency and `@tanstack/react-table` npm dependency.

## 0.2.0

- Add Users section with search field, Add User button, user rows, and selected-user detail panel.
- Add Custom Models section with Add Model button, model cards, and selected-model detail panel.
- Add `PlatformUser` and `CustomModel` types; extend `Settings01Props` with users/models props.
- All new sections fully controlled via props — no internal state.

## 0.1.2

- Fix: add missing imports (lucide-react icons, DropdownMenu sub-components).
- Fix: remove all internal state from component — roles, loading, and delete state now fully controlled via props.
- Add `AdminRole` type and roles-related props to `Settings01Props`.
- Update preview to wire up roles state and handlers.

## 0.1.1

- Content sync fix to bring source in line with published registry payload.

## 0.1.0

- Initial release: organization profile card, roles grid, and settings page
  scaffolding composed from shared widgets.
