diff --git a/apps/web/src/design-preview/access-matrix.tsx b/apps/web/src/design-preview/access-matrix.tsx new file mode 100644 index 0000000..649aba7 --- /dev/null +++ b/apps/web/src/design-preview/access-matrix.tsx @@ -0,0 +1,82 @@ +import { useMemo } from 'react' +import { LockIcon } from 'lucide-react' +import { APPS, type AdminUser, type AppId } from '@authportal/shared' +import { Switch } from '@authportal/ui/components/switch' +import { PendingChip, SignalAlert } from './bits' +import { APP_SHORT } from './mock' +import type { RowMark } from './preview-store' +import { PreviewSectionGrid, TwoLineCell, type GridColumn } from './section-grid' + +/** + * Матрица доступа (solution-users-3): пользователи в строках, приложения в колонках, + * переключатели в ячейках. Администраторам доступ выдан ко всем приложениям — их ячейки заблокированы. + */ +export function AccessMatrix({ + users, + marks, + onToggle, +}: { + users: AdminUser[] + marks: Record + onToggle: (user: AdminUser, appId: AppId, on: boolean) => void +}) { + const columns = useMemo[]>(() => { + const granted = (id: AppId) => users.filter((u) => u.is_admin || u.apps.includes(id)).length + return [ + { + id: 'user', + title: 'Пользователь', + size: 260, + sortValue: (u) => u.email, + cell: (u) => ( +
+ + {marks[u.id] ? : null} +
+ ), + }, + ...APPS.map>((a) => ({ + id: a.id, + title: `${APP_SHORT[a.id]} · ${granted(a.id)}`, + size: 128, + cell: (u) => + u.is_admin ? ( + + + + + ) : ( + onToggle(u, a.id, on)} + aria-label={`${u.email}: ${a.title}`} + /> + ), + })), + ] + }, [users, marks, onToggle]) + + return ( +
+ + Их ячейки заблокированы. Тонкую настройку разделов откройте в правах пользователя. + + u.id} + rowStatus={(u) => marks[u.id]} + searchText={(u) => u.email} + searchPlaceholder="Email" + views={[ + { label: 'Все', match: () => true }, + { label: 'Пользователи', match: (u) => !u.is_admin }, + { label: 'Админы', match: (u) => u.is_admin }, + ]} + /> +
+ ) +} diff --git a/apps/web/src/design-preview/pages/user-profile-page.tsx b/apps/web/src/design-preview/pages/user-profile-page.tsx index c1ad43e..5b4cc38 100644 --- a/apps/web/src/design-preview/pages/user-profile-page.tsx +++ b/apps/web/src/design-preview/pages/user-profile-page.tsx @@ -120,8 +120,8 @@ export function UserProfilePage() { - - + + Права доступа diff --git a/apps/web/src/design-preview/pages/users-page.tsx b/apps/web/src/design-preview/pages/users-page.tsx index b4be228..0d8edd3 100644 --- a/apps/web/src/design-preview/pages/users-page.tsx +++ b/apps/web/src/design-preview/pages/users-page.tsx @@ -12,9 +12,8 @@ import { AccessSheet } from '../access-sheet' import { diffAccess, type AccessValue } from '../access-model' import { CodeBlock, KvList, PageHead, PreviewSheet, TypedConfirm } from '../bits' import { kpi } from '../kpi' -import { ALL_APP_IDS, APP_SHORT, USER_JOURNAL, type Tone } from '../mock' -import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid' -import { StatusBadge } from '../bits' +import { ALL_APP_IDS, APP_SHORT, USER_JOURNAL } from '../mock' +import { AccessMatrix } from '../access-matrix' import { UsersGrid, type UsersGridHandlers } from '../users-grid' import { usePreviewStore } from '../preview-store' import { usePatchUsers } from '../user-actions' @@ -96,13 +95,6 @@ export function UsersPage() { ]), } - const matrix: MatrixRow[] = ALL_APP_IDS.map((id) => { - const mine = users.filter((u) => u.apps.includes(id)) - const level = (action: string) => mine.filter((u) => u.is_admin || u.permissions.some((k) => k.startsWith(`${id}:`) && k.endsWith(`:${action}`))).length - const admins = mine.filter((u) => u.is_admin || u.permissions.some((k) => k.startsWith(`${id}:`) && k.endsWith(':admin'))).length - return { id, name: APP_SHORT[id], total: mine.length, read: level('read'), write: level('write'), admin: admins, tone: admins < 2 ? 'wn' : 'ok' } - }) - const admins = users.filter((u) => u.is_admin).length const noPasskey = users.filter((u) => !u.disabled && u.passkey_count === 0).length const disabled = users.filter((u) => u.disabled).length @@ -137,12 +129,17 @@ export function UsersPage() { - r.id} + + patch( + [u.id], + (x) => ({ ...x, apps: on ? [...x.apps, appId] : x.apps.filter((id) => id !== appId) }), + `${on ? 'Доступ выдан' : 'Доступ снят'}: ${u.email}`, + () => [`${on ? '+' : '−'} приложение ${APP_SHORT[appId]}`], + ) + } /> @@ -251,14 +248,3 @@ export function UsersPage() { ) } - -type MatrixRow = { id: string; name: string; total: number; read: number; write: number; admin: number; tone: Tone } - -const matrixColumns: GridColumn[] = [ - { id: 'app', title: 'Приложение', size: 240, sortValue: (r) => r.name, cell: (r) => }, - { id: 'state', title: 'Статус', size: 160, cell: (r) => {r.tone === 'wn' ? 'Мало админов' : 'Выдан'} }, - { id: 'total', title: 'Пользователей', size: 140, sortValue: (r) => r.total, cell: (r) => {r.total} }, - { id: 'read', title: 'Чтение', size: 100, cell: (r) => {r.read} }, - { id: 'write', title: 'Запись', size: 100, cell: (r) => {r.write} }, - { id: 'admin', title: 'Админ', size: 100, cell: (r) => {r.admin} }, -]