portal-mojo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +55 -0
- package/docs/DetailView.md +173 -0
- package/docs/ModelTable.md +128 -0
- package/docs/README.md +98 -0
- package/docs/admin-assistant.md +72 -0
- package/docs/admin-bouncer.md +120 -0
- package/docs/admin-cloudwatch.md +38 -0
- package/docs/admin-credentials.md +116 -0
- package/docs/admin-dashboard.md +15 -0
- package/docs/admin-devices-geoip.md +284 -0
- package/docs/admin-dns-certificates.md +122 -0
- package/docs/admin-dns-records.md +65 -0
- package/docs/admin-dns-registrar.md +33 -0
- package/docs/admin-dns.md +80 -0
- package/docs/admin-identity-users.md +103 -0
- package/docs/admin-incidents.md +71 -0
- package/docs/admin-jobs.md +426 -0
- package/docs/admin-members.md +112 -0
- package/docs/admin-messaging.md +56 -0
- package/docs/admin-metrics-explorer.md +63 -0
- package/docs/admin-monitoring.md +99 -0
- package/docs/admin-network-security.md +393 -0
- package/docs/admin-phonehub.md +27 -0
- package/docs/admin-push.md +36 -0
- package/docs/admin-rules.md +87 -0
- package/docs/admin-security-tickets.md +40 -0
- package/docs/admin-settings.md +78 -0
- package/docs/admin-shortlinks.md +32 -0
- package/docs/admin-storage.md +141 -0
- package/docs/auth-pages.md +212 -0
- package/docs/auth.md +58 -0
- package/docs/calendar.md +122 -0
- package/docs/charts.md +303 -0
- package/docs/client.md +206 -0
- package/docs/collection-multiselect.md +117 -0
- package/docs/collection-select.md +132 -0
- package/docs/combobox.md +105 -0
- package/docs/dataview.md +226 -0
- package/docs/datepicker.md +151 -0
- package/docs/daterange.md +170 -0
- package/docs/datetimepicker.md +249 -0
- package/docs/defineModel.md +72 -0
- package/docs/detail-primitives.md +174 -0
- package/docs/feedback.md +85 -0
- package/docs/file-upload-ux.md +146 -0
- package/docs/forms.md +435 -0
- package/docs/grouping-and-fmt.md +98 -0
- package/docs/idioms.md +112 -0
- package/docs/image-editor.md +101 -0
- package/docs/loading.md +242 -0
- package/docs/location-address.md +76 -0
- package/docs/markdown.md +203 -0
- package/docs/menus-and-access.md +89 -0
- package/docs/multiselect-dropdown.md +153 -0
- package/docs/params.md +73 -0
- package/docs/popover.md +130 -0
- package/docs/realtime.md +61 -0
- package/docs/record-feed.md +238 -0
- package/docs/right-panel.md +42 -0
- package/docs/tabs-and-form-wizard.md +60 -0
- package/docs/taginput.md +120 -0
- package/docs/timepicker.md +173 -0
- package/docs/timezone-select.md +147 -0
- package/docs/worldmap.md +302 -0
- package/package.json +55 -0
- package/src/admin/assistant/AssistantFeed.tsx +29 -0
- package/src/admin/assistant/AssistantPanel.tsx +65 -0
- package/src/admin/assistant/api.ts +17 -0
- package/src/admin/assistant/data.ts +63 -0
- package/src/admin/assistant/index.ts +21 -0
- package/src/admin/assistant/launchers.tsx +33 -0
- package/src/admin/assistant/pages.tsx +78 -0
- package/src/admin/assistant/streaming.ts +238 -0
- package/src/admin/assistant/types.ts +30 -0
- package/src/admin/bouncer/devices.tsx +230 -0
- package/src/admin/bouncer/index.ts +34 -0
- package/src/admin/bouncer/models.ts +199 -0
- package/src/admin/bouncer/signals.tsx +182 -0
- package/src/admin/bouncer/signatures.tsx +220 -0
- package/src/admin/cloudwatch/CloudWatchChart.tsx +61 -0
- package/src/admin/cloudwatch/CloudWatchDashboardPage.tsx +52 -0
- package/src/admin/cloudwatch/CloudWatchResourceDetail.tsx +33 -0
- package/src/admin/cloudwatch/data.ts +163 -0
- package/src/admin/cloudwatch/index.ts +16 -0
- package/src/admin/credentials/group-api-keys.tsx +433 -0
- package/src/admin/credentials/index.ts +4 -0
- package/src/admin/credentials/models.ts +244 -0
- package/src/admin/credentials/secret-dialog.tsx +77 -0
- package/src/admin/credentials/webhook-subscriptions.tsx +423 -0
- package/src/admin/dashboard/AdminDashboardPage.tsx +101 -0
- package/src/admin/dashboard/data.ts +60 -0
- package/src/admin/dashboard/index.ts +2 -0
- package/src/admin/dns/CertificateDetail.tsx +115 -0
- package/src/admin/dns/CertificateLifecyclePoller.tsx +141 -0
- package/src/admin/dns/CertificateRequestDialog.tsx +103 -0
- package/src/admin/dns/CertificatesPage.tsx +85 -0
- package/src/admin/dns/DnsRecordEditor.tsx +110 -0
- package/src/admin/dns/DnsRecordsPage.tsx +36 -0
- package/src/admin/dns/DnsRecordsPanel.tsx +71 -0
- package/src/admin/dns/DomainCertificatesSection.tsx +40 -0
- package/src/admin/dns/DomainDetail.tsx +41 -0
- package/src/admin/dns/DomainOnboardingDialog.tsx +27 -0
- package/src/admin/dns/DomainPurchaseDetail.tsx +11 -0
- package/src/admin/dns/DomainPurchaseWizard.tsx +112 -0
- package/src/admin/dns/DomainPurchasesPage.tsx +16 -0
- package/src/admin/dns/DomainsPage.tsx +27 -0
- package/src/admin/dns/ProviderCredentialsPage.tsx +304 -0
- package/src/admin/dns/RegistrantContactPage.tsx +42 -0
- package/src/admin/dns/api.ts +515 -0
- package/src/admin/dns/certificate-data.ts +221 -0
- package/src/admin/dns/data.ts +32 -0
- package/src/admin/dns/dns-data.ts +266 -0
- package/src/admin/dns/dns-integration.ts +24 -0
- package/src/admin/dns/domain-links.ts +37 -0
- package/src/admin/dns/index.ts +86 -0
- package/src/admin/dns/models.ts +418 -0
- package/src/admin/dns/purchase-data.ts +67 -0
- package/src/admin/dns/registrant-data.ts +52 -0
- package/src/admin/identity/index.ts +5 -0
- package/src/admin/identity/members/GroupMembersPanel.tsx +146 -0
- package/src/admin/identity/members/MemberDetail.tsx +226 -0
- package/src/admin/identity/members/MembersPage.tsx +144 -0
- package/src/admin/identity/members/index.ts +24 -0
- package/src/admin/identity/members/member-flows.tsx +132 -0
- package/src/admin/identity/members/models.ts +204 -0
- package/src/admin/identity/users/UserDetail.tsx +257 -0
- package/src/admin/identity/users/UsersPage.tsx +163 -0
- package/src/admin/identity/users/index.ts +27 -0
- package/src/admin/identity/users/models.ts +232 -0
- package/src/admin/identity/users/sections/ApiKeysSection.tsx +119 -0
- package/src/admin/identity/users/sections/AuditSection.tsx +190 -0
- package/src/admin/identity/users/sections/DevicesSection.tsx +140 -0
- package/src/admin/identity/users/sections/GroupsSection.tsx +64 -0
- package/src/admin/identity/users/sections/LoginsSection.tsx +106 -0
- package/src/admin/identity/users/sections/MetadataPanel.tsx +36 -0
- package/src/admin/identity/users/sections/NotificationsSection.tsx +117 -0
- package/src/admin/identity/users/sections/OAuthSection.tsx +72 -0
- package/src/admin/identity/users/sections/OverviewSection.tsx +109 -0
- package/src/admin/identity/users/sections/PermissionsSection.tsx +35 -0
- package/src/admin/identity/users/sections/PersonalSection.tsx +73 -0
- package/src/admin/identity/users/sections/ProfileSection.tsx +175 -0
- package/src/admin/identity/users/sections/SecuritySection.tsx +123 -0
- package/src/admin/identity/users/sections/actions.tsx +752 -0
- package/src/admin/identity/users/sections/permission-catalog.ts +138 -0
- package/src/admin/identity/users/sections/queries.ts +74 -0
- package/src/admin/identity/users/sections/shared.tsx +299 -0
- package/src/admin/incidents/EventDetail.tsx +43 -0
- package/src/admin/incidents/EventsPage.tsx +52 -0
- package/src/admin/incidents/IncidentDetail.tsx +68 -0
- package/src/admin/incidents/IncidentsPage.tsx +102 -0
- package/src/admin/incidents/forensics.tsx +73 -0
- package/src/admin/incidents/index.ts +16 -0
- package/src/admin/incidents/models.ts +113 -0
- package/src/admin/incidents/sanitize.ts +71 -0
- package/src/admin/index.ts +383 -0
- package/src/admin/jobs/JobDashboardPage.tsx +111 -0
- package/src/admin/jobs/JobDetail.tsx +721 -0
- package/src/admin/jobs/JobRunnersPage.tsx +216 -0
- package/src/admin/jobs/JobsTablePage.tsx +122 -0
- package/src/admin/jobs/RunnerDetail.tsx +576 -0
- package/src/admin/jobs/ScheduledTaskDetail.tsx +421 -0
- package/src/admin/jobs/ScheduledTasksPage.tsx +167 -0
- package/src/admin/jobs/columns.tsx +297 -0
- package/src/admin/jobs/control.ts +488 -0
- package/src/admin/jobs/index.ts +85 -0
- package/src/admin/jobs/models.ts +599 -0
- package/src/admin/jobs/queries.ts +272 -0
- package/src/admin/jobs/scheduled-task-form.tsx +493 -0
- package/src/admin/jobs/sections/JobChannelsPanel.tsx +142 -0
- package/src/admin/jobs/sections/JobOperationsSection.tsx +423 -0
- package/src/admin/jobs/sections/JobRunnersStrip.tsx +82 -0
- package/src/admin/jobs/sections/JobThroughputSection.tsx +118 -0
- package/src/admin/messaging/EmailDomainWizard.tsx +24 -0
- package/src/admin/messaging/EmailDomainsPage.tsx +17 -0
- package/src/admin/messaging/EmailTemplatesPage.tsx +13 -0
- package/src/admin/messaging/MailboxesPage.tsx +15 -0
- package/src/admin/messaging/PublicMessagesPage.tsx +13 -0
- package/src/admin/messaging/SandboxedEmailPreview.tsx +23 -0
- package/src/admin/messaging/SentMessagesPage.tsx +11 -0
- package/src/admin/messaging/api.ts +17 -0
- package/src/admin/messaging/data.ts +10 -0
- package/src/admin/messaging/index.ts +23 -0
- package/src/admin/messaging/models.ts +76 -0
- package/src/admin/messaging/push/PushPage.tsx +44 -0
- package/src/admin/messaging/push/api.ts +17 -0
- package/src/admin/messaging/push/index.ts +9 -0
- package/src/admin/messaging/push/models.ts +43 -0
- package/src/admin/messaging/sanitize.ts +44 -0
- package/src/admin/monitoring/LogInspector.tsx +160 -0
- package/src/admin/monitoring/LogsPage.tsx +135 -0
- package/src/admin/monitoring/MetricsExplorerPage.tsx +360 -0
- package/src/admin/monitoring/MetricsPermissionsPage.tsx +166 -0
- package/src/admin/monitoring/MetricsSourcePicker.tsx +136 -0
- package/src/admin/monitoring/index.ts +55 -0
- package/src/admin/monitoring/metrics-explorer-client.ts +229 -0
- package/src/admin/monitoring/metrics-explorer-data.ts +158 -0
- package/src/admin/monitoring/models.ts +155 -0
- package/src/admin/network/BlockedIPsPage.tsx +194 -0
- package/src/admin/network/BlockedIpDetail.tsx +262 -0
- package/src/admin/network/FirewallLogPage.tsx +115 -0
- package/src/admin/network/IPSetDetail.tsx +280 -0
- package/src/admin/network/IPSetEditor.tsx +191 -0
- package/src/admin/network/IPSetsPage.tsx +177 -0
- package/src/admin/network/geofence/BlocksTab.tsx +289 -0
- package/src/admin/network/geofence/ExemptionsTab.tsx +349 -0
- package/src/admin/network/geofence/GeofencingPage.tsx +119 -0
- package/src/admin/network/geofence/PostureHeader.tsx +184 -0
- package/src/admin/network/geofence/RuleEditor.tsx +176 -0
- package/src/admin/network/geofence/RulesTab.tsx +254 -0
- package/src/admin/network/geofence/SimulatorTab.tsx +281 -0
- package/src/admin/network/geofence/geofence-data.ts +674 -0
- package/src/admin/network/index.ts +68 -0
- package/src/admin/network/models.ts +607 -0
- package/src/admin/phonehub/PhoneHubPage.tsx +57 -0
- package/src/admin/phonehub/api.ts +47 -0
- package/src/admin/phonehub/data.ts +6 -0
- package/src/admin/phonehub/index.ts +10 -0
- package/src/admin/phonehub/models.ts +68 -0
- package/src/admin/rules/HandlerChainBuilder.tsx +51 -0
- package/src/admin/rules/RuleSetDetailPage.tsx +33 -0
- package/src/admin/rules/RuleSetsPage.tsx +33 -0
- package/src/admin/rules/editors.tsx +54 -0
- package/src/admin/rules/handler-dsl.ts +213 -0
- package/src/admin/rules/index.ts +13 -0
- package/src/admin/rules/models.ts +112 -0
- package/src/admin/security/devices/LoginEventDetail.tsx +123 -0
- package/src/admin/security/devices/LoginLocationMap.tsx +321 -0
- package/src/admin/security/devices/LoginLocationsPage.tsx +162 -0
- package/src/admin/security/devices/UserDeviceDetail.tsx +546 -0
- package/src/admin/security/devices/UserDeviceLocationDetail.tsx +272 -0
- package/src/admin/security/devices/UserDevicesPage.tsx +84 -0
- package/src/admin/security/devices/index.ts +40 -0
- package/src/admin/security/devices/models.ts +470 -0
- package/src/admin/security/geoip/GeoIpCachePage.tsx +135 -0
- package/src/admin/security/geoip/GeoIpDossier.tsx +804 -0
- package/src/admin/security/geoip/geoip-forms.ts +102 -0
- package/src/admin/security/geoip/index.ts +27 -0
- package/src/admin/security/geoip/models.ts +397 -0
- package/src/admin/security/index.ts +31 -0
- package/src/admin/security/models.ts +137 -0
- package/src/admin/security/tickets.tsx +562 -0
- package/src/admin/security-permissions.ts +5 -0
- package/src/admin/settings/SettingDetail.tsx +57 -0
- package/src/admin/settings/SettingEditor.tsx +157 -0
- package/src/admin/settings/SettingsPage.tsx +72 -0
- package/src/admin/settings/index.ts +19 -0
- package/src/admin/settings/model.ts +143 -0
- package/src/admin/shortlinks/ShortlinkDetail.tsx +55 -0
- package/src/admin/shortlinks/ShortlinkHistoryPage.tsx +18 -0
- package/src/admin/shortlinks/ShortlinksPage.tsx +59 -0
- package/src/admin/shortlinks/index.ts +18 -0
- package/src/admin/shortlinks/models.ts +246 -0
- package/src/admin/storage/BackendsPage.tsx +43 -0
- package/src/admin/storage/BucketDetail.tsx +77 -0
- package/src/admin/storage/BucketsPage.tsx +64 -0
- package/src/admin/storage/FileManagerDetail.tsx +36 -0
- package/src/admin/storage/FilePreview.tsx +54 -0
- package/src/admin/storage/FileUploadSurface.tsx +250 -0
- package/src/admin/storage/FileView.tsx +109 -0
- package/src/admin/storage/FilesPage.tsx +53 -0
- package/src/admin/storage/file-renditions.ts +99 -0
- package/src/admin/storage/index.ts +31 -0
- package/src/admin/storage/models.ts +585 -0
- package/src/admin/storage/storage-dialogs.tsx +80 -0
- package/src/charts/CircularProgress.tsx +281 -0
- package/src/charts/KPIStrip.tsx +162 -0
- package/src/charts/KPITile.tsx +125 -0
- package/src/charts/MetricsChart.tsx +459 -0
- package/src/charts/MetricsMiniWidget.tsx +588 -0
- package/src/charts/MiniChart.tsx +283 -0
- package/src/charts/PieChart.tsx +354 -0
- package/src/charts/SeriesChart.tsx +378 -0
- package/src/charts/chart-dialogs.tsx +149 -0
- package/src/charts/exportChart.ts +144 -0
- package/src/charts/index.ts +28 -0
- package/src/charts/pie-math.ts +127 -0
- package/src/charts/stats.ts +265 -0
- package/src/charts/worldmap/WorldMap.tsx +649 -0
- package/src/charts/worldmap/countryCentroids.ts +304 -0
- package/src/charts/worldmap/geo.ts +293 -0
- package/src/charts/worldmap/use-world-land.ts +47 -0
- package/src/charts/worldmap/world-land.ts +18 -0
- package/src/charts/worldmap/worldmap-data.ts +285 -0
- package/src/client/auth.ts +737 -0
- package/src/client/client.ts +268 -0
- package/src/client/duid.ts +31 -0
- package/src/client/errors.ts +33 -0
- package/src/client/group-context.ts +28 -0
- package/src/client/group.tsx +126 -0
- package/src/client/hooks.ts +50 -0
- package/src/client/index.ts +37 -0
- package/src/client/jwt.ts +75 -0
- package/src/client/location.ts +285 -0
- package/src/client/lookups.ts +41 -0
- package/src/client/markdown-fallback.tsx +115 -0
- package/src/client/markdown-parse.ts +386 -0
- package/src/client/markdown-sanitize.ts +217 -0
- package/src/client/markdown.ts +122 -0
- package/src/client/me.ts +188 -0
- package/src/client/mock.ts +9984 -0
- package/src/client/model.ts +227 -0
- package/src/client/params.ts +292 -0
- package/src/client/realtime-mock.ts +221 -0
- package/src/client/realtime.ts +561 -0
- package/src/client/record-feed.ts +329 -0
- package/src/client/safe-export.ts +71 -0
- package/src/client/types.ts +278 -0
- package/src/client/upload.ts +421 -0
- package/src/ui/AddressField.tsx +273 -0
- package/src/ui/AttachmentQueue.tsx +92 -0
- package/src/ui/CollectionMultiSelect.tsx +622 -0
- package/src/ui/CollectionSelect.tsx +517 -0
- package/src/ui/ComboBox.tsx +336 -0
- package/src/ui/DetailView.tsx +438 -0
- package/src/ui/FileDrop.tsx +194 -0
- package/src/ui/FileField.tsx +369 -0
- package/src/ui/FilterBar.tsx +269 -0
- package/src/ui/FormFields.tsx +77 -0
- package/src/ui/FormView.tsx +360 -0
- package/src/ui/FormWizard.tsx +336 -0
- package/src/ui/GroupSwitcher.tsx +202 -0
- package/src/ui/Guarded.tsx +16 -0
- package/src/ui/MarkdownView.tsx +89 -0
- package/src/ui/ModelTable.tsx +848 -0
- package/src/ui/MultiSelectDropdown.tsx +390 -0
- package/src/ui/Popover.tsx +204 -0
- package/src/ui/RecordFeed.tsx +627 -0
- package/src/ui/RequiresGroup.tsx +32 -0
- package/src/ui/RightPanel.tsx +107 -0
- package/src/ui/SidebarNav.tsx +330 -0
- package/src/ui/Tabs.tsx +161 -0
- package/src/ui/TagInput.tsx +381 -0
- package/src/ui/ThemeProvider.tsx +52 -0
- package/src/ui/UploadQueue.tsx +603 -0
- package/src/ui/armed-button.tsx +91 -0
- package/src/ui/date/Calendar.tsx +498 -0
- package/src/ui/date/DatePicker.tsx +289 -0
- package/src/ui/date/DateRangePicker.tsx +280 -0
- package/src/ui/date/DateTimePicker.tsx +587 -0
- package/src/ui/date/PresetRail.tsx +165 -0
- package/src/ui/date/TimePicker.tsx +624 -0
- package/src/ui/date/TimezoneSelect.tsx +267 -0
- package/src/ui/date/fns.ts +679 -0
- package/src/ui/detail/DataView.tsx +692 -0
- package/src/ui/detail/FlowStrip.tsx +80 -0
- package/src/ui/detail/KnownFieldsCard.tsx +121 -0
- package/src/ui/detail/MetadataSection.tsx +219 -0
- package/src/ui/detail/StackTraceView.tsx +150 -0
- package/src/ui/detail/StatusPanel.tsx +73 -0
- package/src/ui/detail/Timeline.tsx +52 -0
- package/src/ui/detail/index.ts +12 -0
- package/src/ui/field-registry.tsx +428 -0
- package/src/ui/field-wire.ts +322 -0
- package/src/ui/form-autosave.ts +583 -0
- package/src/ui/format.ts +444 -0
- package/src/ui/grouping.ts +150 -0
- package/src/ui/image-editor/ImageEditor.tsx +484 -0
- package/src/ui/image-editor/canvas.ts +254 -0
- package/src/ui/image-editor/index.ts +3 -0
- package/src/ui/image-editor/math.ts +416 -0
- package/src/ui/index.ts +58 -0
- package/src/ui/loading.tsx +604 -0
- package/src/ui/menu-registry.ts +213 -0
- package/src/ui/modal.tsx +297 -0
- package/src/ui/password.tsx +302 -0
- package/src/ui/schema-form-core.tsx +315 -0
- package/src/ui/toast.tsx +219 -0
- package/src/ui/ui.tsx +45 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The system permission catalog — web-mojo's User.CATEGORY_PERMISSIONS +
|
|
2
|
+
// GRANULAR_PERMISSION_TABS carried VERBATIM (src/core/models/User.js:76-149,
|
|
3
|
+
// read in full 2026-08-05), expressed as FormView tabsets instead of the
|
|
4
|
+
// live-mutated arrays the do-not-recreate list retires.
|
|
5
|
+
//
|
|
6
|
+
// Sys Perms section == User.SYSTEM_PERMISSION_FIELDS: ONE tabset — the
|
|
7
|
+
// System category tab + each granular domain tab. App Perms section ==
|
|
8
|
+
// User.APP_PERMISSION_FIELDS: whatever the app registered under the
|
|
9
|
+
// USER_APP_PERMS_TABSET registry name (registerFormTabs is the portal's
|
|
10
|
+
// User.registerPermissions equivalent) — the section renders only when that
|
|
11
|
+
// registry is non-empty, mirroring `User.APP_PERMISSION_FIELDS.length > 0`.
|
|
12
|
+
//
|
|
13
|
+
// Every switch autosaves a dotted `permissions.<name>` key — FormView
|
|
14
|
+
// expands it to a partial dict and django-mojo MERGES it into the
|
|
15
|
+
// permissions JSONField (rest.py on_rest_update_jsonfield), so absent keys
|
|
16
|
+
// keep their grants.
|
|
17
|
+
import type { Field } from '../../../../client';
|
|
18
|
+
import type { FormTab } from '../../../../ui';
|
|
19
|
+
|
|
20
|
+
interface CatalogPerm {
|
|
21
|
+
name: string;
|
|
22
|
+
label: string;
|
|
23
|
+
tooltip?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Category permissions (broad domain-level access) — source verbatim. */
|
|
27
|
+
export const CATEGORY_PERMISSIONS: CatalogPerm[] = [
|
|
28
|
+
{ name: 'admin', label: 'System Admin', tooltip: 'Full access to everything — permission equivalent of superuser' },
|
|
29
|
+
{ name: 'view_admin', label: 'Admin Panel', tooltip: 'Access the admin panel, Mojo, and system tools' },
|
|
30
|
+
{ name: 'security', label: 'Security', tooltip: 'Incidents, events, rules, tickets, firewall, bouncer, GeoIP, system logs' },
|
|
31
|
+
{ name: 'users', label: 'Users', tooltip: 'User records, passkeys, TOTP, API keys, OAuth, devices, locations' },
|
|
32
|
+
{ name: 'groups', label: 'Groups', tooltip: 'Groups, members, group API keys, settings' },
|
|
33
|
+
{ name: 'comms', label: 'Communications', tooltip: 'Email, phone, SMS, push notifications, chat, notifications' },
|
|
34
|
+
{ name: 'jobs', label: 'Jobs', tooltip: 'Jobs, job events, job logs, runners, queue control, system stats' },
|
|
35
|
+
{ name: 'metrics', label: 'Metrics', tooltip: 'Metrics recording, fetching, categories, values, permissions' },
|
|
36
|
+
{ name: 'files', label: 'Files', tooltip: 'File managers, files, renditions, vault files, vault data, S3 buckets' },
|
|
37
|
+
{ name: 'assistant', label: 'Mojo', tooltip: 'Access to Mojo' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
/** Granular view/manage pairs by domain tab — source verbatim. */
|
|
41
|
+
export const GRANULAR_PERMISSION_TABS: { label: string; permissions: CatalogPerm[] }[] = [
|
|
42
|
+
{
|
|
43
|
+
label: 'Account',
|
|
44
|
+
permissions: [
|
|
45
|
+
{ name: 'view_users', label: 'View Users' },
|
|
46
|
+
{ name: 'manage_users', label: 'Manage Users' },
|
|
47
|
+
{ name: 'view_groups', label: 'View Groups' },
|
|
48
|
+
{ name: 'manage_groups', label: 'Manage Groups' },
|
|
49
|
+
{ name: 'manage_group', label: 'Manage Own Group' },
|
|
50
|
+
{ name: 'view_members', label: 'View Members' },
|
|
51
|
+
{ name: 'manage_settings', label: 'Manage Settings' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: 'Communication',
|
|
56
|
+
permissions: [
|
|
57
|
+
{ name: 'view_support', label: 'View Support Messages' },
|
|
58
|
+
{ name: 'manage_support', label: 'Manage Support Messages' },
|
|
59
|
+
{ name: 'manage_chat', label: 'Manage Chat' },
|
|
60
|
+
{ name: 'manage_aws', label: 'Manage Email (AWS)' },
|
|
61
|
+
{ name: 'view_notifications', label: 'View Notifications' },
|
|
62
|
+
{ name: 'manage_notifications', label: 'Manage Notifications' },
|
|
63
|
+
{ name: 'send_notifications', label: 'Send Notifications' },
|
|
64
|
+
{ name: 'view_devices', label: 'View Push Devices' },
|
|
65
|
+
{ name: 'manage_devices', label: 'Manage Push Devices' },
|
|
66
|
+
{ name: 'manage_push_config', label: 'Push Config' },
|
|
67
|
+
{ name: 'view_phone_numbers', label: 'View Phone Numbers' },
|
|
68
|
+
{ name: 'manage_phone_numbers', label: 'Manage Phone Numbers' },
|
|
69
|
+
{ name: 'manage_phone_config', label: 'Phone Config' },
|
|
70
|
+
{ name: 'view_sms', label: 'View SMS' },
|
|
71
|
+
{ name: 'manage_sms', label: 'Manage SMS' },
|
|
72
|
+
{ name: 'send_sms', label: 'Send SMS' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Platform',
|
|
77
|
+
permissions: [
|
|
78
|
+
{ name: 'view_security', label: 'View Security' },
|
|
79
|
+
{ name: 'manage_security', label: 'Manage Security' },
|
|
80
|
+
{ name: 'view_logs', label: 'View Logs' },
|
|
81
|
+
{ name: 'manage_logs', label: 'Manage Logs' },
|
|
82
|
+
{ name: 'view_jobs', label: 'View Jobs' },
|
|
83
|
+
{ name: 'manage_jobs', label: 'Manage Jobs' },
|
|
84
|
+
{ name: 'view_metrics', label: 'View Metrics' },
|
|
85
|
+
{ name: 'manage_metrics', label: 'Manage Metrics' },
|
|
86
|
+
{ name: 'write_metrics', label: 'Write Metrics' },
|
|
87
|
+
{ name: 'view_fileman', label: 'View File Managers' },
|
|
88
|
+
{ name: 'manage_files', label: 'Manage Files' },
|
|
89
|
+
{ name: 'view_vault', label: 'View Vault' },
|
|
90
|
+
{ name: 'manage_vault', label: 'Manage Vault' },
|
|
91
|
+
{ name: 'manage_docit', label: 'Manage Docs' },
|
|
92
|
+
{ name: 'manage_shortlinks', label: 'Manage Shortlinks' },
|
|
93
|
+
{ name: 'view_dns', label: 'View DNS' },
|
|
94
|
+
{
|
|
95
|
+
name: 'manage_dns', label: 'Manage DNS',
|
|
96
|
+
tooltip: 'Manage domains, DNS records, provider credentials and certificates. Adopting a hosted zone and reading certificate key material stay restricted to platform superusers.',
|
|
97
|
+
},
|
|
98
|
+
{ name: 'view_geofence', label: 'View Geofence Config' },
|
|
99
|
+
{ name: 'manage_geofence', label: 'Manage Geofence Config' },
|
|
100
|
+
{
|
|
101
|
+
name: 'bypass_geofence', label: 'Bypass Geofence (Whitelist)',
|
|
102
|
+
tooltip: 'Exempts this user from ALL geofence rules on authenticated requests. Login-time geofencing still applies until backend post-login support lands.',
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/** User._permSwitch port: catalog entry → dotted autosave switch field. */
|
|
109
|
+
function permSwitch(p: CatalogPerm): Field {
|
|
110
|
+
return {
|
|
111
|
+
name: `permissions.${p.name}`,
|
|
112
|
+
type: 'switch',
|
|
113
|
+
label: p.label,
|
|
114
|
+
columns: 6,
|
|
115
|
+
...(p.tooltip ? { help: p.tooltip } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The "Sys Perms" tabset — System category tab + the granular domain tabs,
|
|
121
|
+
* one row (User.SYSTEM_PERMISSION_FIELDS shape).
|
|
122
|
+
*/
|
|
123
|
+
export const SYSTEM_PERMISSION_TABS: FormTab[] = [
|
|
124
|
+
{ key: 'system', label: 'System', fields: CATEGORY_PERMISSIONS.map(permSwitch) },
|
|
125
|
+
...GRANULAR_PERMISSION_TABS.map((tab) => ({
|
|
126
|
+
key: tab.label.toLowerCase(),
|
|
127
|
+
label: tab.label,
|
|
128
|
+
fields: tab.permissions.map(permSwitch),
|
|
129
|
+
})),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Registry name for app-registered permission tabs — the portal equivalent
|
|
134
|
+
* of User.registerPermissions(...). An app calls
|
|
135
|
+
* `registerFormTabs(USER_APP_PERMS_TABSET, [{key, label, fields}])` and the
|
|
136
|
+
* "App Perms" section appears with those tabs; empty registry = no section.
|
|
137
|
+
*/
|
|
138
|
+
export const USER_APP_PERMS_TABSET = 'user-app-perms';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The shared query set UserDetail owns — the port of UserView's shared
|
|
2
|
+
// Collections (devicesCollection, pushDevicesCollection, membersCollection,
|
|
3
|
+
// loginsCollection, eventsCollection, activityCollection,
|
|
4
|
+
// objectLogsCollection + the fire-and-forget throttle GET). Overview KPIs,
|
|
5
|
+
// the rail count badges, and the header chips all read from here; section
|
|
6
|
+
// lists with their own paging/search run their own param'd queries and
|
|
7
|
+
// dedupe through the TanStack cache.
|
|
8
|
+
import { useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { mojoCall } from '../../../../client';
|
|
10
|
+
import { LogModel } from '../../../monitoring';
|
|
11
|
+
import { EventModel } from '../../../incidents';
|
|
12
|
+
import { MemberModel } from '../../members';
|
|
13
|
+
import {
|
|
14
|
+
DeviceModel, LoginEventModel, PushDeviceModel,
|
|
15
|
+
ApiKeyModel,
|
|
16
|
+
} from '../models';
|
|
17
|
+
|
|
18
|
+
export interface ThrottleState {
|
|
19
|
+
count: number;
|
|
20
|
+
limit: number;
|
|
21
|
+
window: number;
|
|
22
|
+
retry_after_seconds: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SharedUserQueryAccess {
|
|
26
|
+
devices: boolean;
|
|
27
|
+
pushDevices: boolean;
|
|
28
|
+
members: boolean;
|
|
29
|
+
logins: boolean;
|
|
30
|
+
events: boolean;
|
|
31
|
+
logs: boolean;
|
|
32
|
+
apiKeys: boolean;
|
|
33
|
+
throttle: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useSharedUserQueries(userId: number, access: SharedUserQueryAccess) {
|
|
37
|
+
// Sizes mirror the source's shared collections (25/25/10/10/25/25/25).
|
|
38
|
+
const devices = DeviceModel.useList({ user: userId, size: 25 }, { enabled: access.devices });
|
|
39
|
+
const pushDevices = PushDeviceModel.useList({ user: userId, size: 25 }, { enabled: access.pushDevices });
|
|
40
|
+
const members = MemberModel.useList({ user: userId, size: 10 }, { enabled: access.members });
|
|
41
|
+
const logins = LoginEventModel.useList({ user: userId, size: 10, sort: '-created' }, { enabled: access.logins });
|
|
42
|
+
const events = EventModel.useList(
|
|
43
|
+
{ size: 25, model_name: 'account.User', model_id: userId, sort: '-created' },
|
|
44
|
+
{ enabled: access.events },
|
|
45
|
+
);
|
|
46
|
+
const activity = LogModel.useList({ size: 25, uid: userId, sort: '-created' }, { enabled: access.logs });
|
|
47
|
+
const objectLogs = LogModel.useList(
|
|
48
|
+
{ size: 25, model_name: 'account.User', model_id: userId, sort: '-created' },
|
|
49
|
+
{ enabled: access.logs },
|
|
50
|
+
);
|
|
51
|
+
const apiKeys = ApiKeyModel.useList({ user: userId, size: 25, sort: '-id' }, { enabled: access.apiKeys });
|
|
52
|
+
|
|
53
|
+
// Login-throttle state for the header "Login locked Xs" badge.
|
|
54
|
+
// Admin-tier endpoint; failure (or a non-admin viewer) is non-fatal —
|
|
55
|
+
// the badge just stays hidden (source _refreshThrottle).
|
|
56
|
+
const throttle = useQuery<ThrottleState | null>({
|
|
57
|
+
queryKey: ['/api/auth/manage/throttle', userId],
|
|
58
|
+
queryFn: async () => {
|
|
59
|
+
try {
|
|
60
|
+
const body = await mojoCall('/api/auth/manage/throttle', { params: { user_id: userId, key: 'login' } });
|
|
61
|
+
return (body.data ?? null) as ThrottleState | null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
enabled: access.throttle,
|
|
67
|
+
staleTime: 30_000,
|
|
68
|
+
retry: false,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return { devices, pushDevices, members, logins, events, activity, objectLogs, apiKeys, throttle };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type SharedUserQueries = ReturnType<typeof useSharedUserQueries>;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// user-sections/shared — the helpers every UserDetail section leans on:
|
|
2
|
+
// the disable-lifecycle maps, tone maps, presence math, the admin-caller
|
|
3
|
+
// gate, day-grouped list rendering, and the compact pager/search idiom the
|
|
4
|
+
// section lists share (web-mojo ListView's searchable + paginationMode
|
|
5
|
+
// 'pages' pageSize 5, expressed as server params).
|
|
6
|
+
//
|
|
7
|
+
// Ported from admin/account/users/UserView.js helpers (isOnline,
|
|
8
|
+
// DISABLE_REASON_BADGES, _statusBadge, _inactivityWarning, LOG_LEVEL_*,
|
|
9
|
+
// LOGIN_TONE, PROVIDER_ICONS) — read in full 2026-08-05.
|
|
10
|
+
import { useState, type ReactNode } from 'react';
|
|
11
|
+
import { dateFns, fmt, type Tone } from '../../../../ui';
|
|
12
|
+
import { useCan, type Params } from '../../../../client';
|
|
13
|
+
import { USER_MANAGE_PERMISSIONS, type UserRow } from '../models';
|
|
14
|
+
|
|
15
|
+
// ── Disable lifecycle (metadata.protected.disable.*) ──────────────────
|
|
16
|
+
// Truth field is `is_active`; the block carries reason/by/note/warning/
|
|
17
|
+
// history (services/disable.py schema — `at` values are ISO strings, not
|
|
18
|
+
// epochs). Reason → badge map per the admin-users spec alignment.
|
|
19
|
+
|
|
20
|
+
export interface DisableHistoryEntry {
|
|
21
|
+
at?: string | null;
|
|
22
|
+
reason?: string | null;
|
|
23
|
+
by_user_id?: number | null;
|
|
24
|
+
by_username?: string | null;
|
|
25
|
+
note?: string | null;
|
|
26
|
+
reactivated_at?: string | null;
|
|
27
|
+
reactivated_by_user_id?: number | null;
|
|
28
|
+
reactivated_by_username?: string | null;
|
|
29
|
+
reactivated_note?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DisableBlock {
|
|
33
|
+
reason?: string | null;
|
|
34
|
+
at?: string | null;
|
|
35
|
+
by_user_id?: number | null;
|
|
36
|
+
by_username?: string | null;
|
|
37
|
+
note?: string | null;
|
|
38
|
+
exempt_from_auto_disable?: boolean;
|
|
39
|
+
warning?: { sent_at?: string | null; days_until_disable_at_send?: number | null };
|
|
40
|
+
history?: DisableHistoryEntry[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const DISABLE_REASON_BADGES: Record<string, { label: string; tone: Tone }> = {
|
|
44
|
+
admin: { label: 'Blocked', tone: 'danger' },
|
|
45
|
+
abuse: { label: 'Banned', tone: 'danger' },
|
|
46
|
+
inactive: { label: 'Auto-disabled', tone: 'warning' },
|
|
47
|
+
anonymized: { label: 'Anonymized', tone: 'muted' },
|
|
48
|
+
self: { label: 'Self-deactivated', tone: 'muted' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function disableBlock(user: UserRow): DisableBlock | null {
|
|
52
|
+
const meta = user.metadata as Record<string, unknown> | null | undefined;
|
|
53
|
+
const protectedNs = meta?.protected as Record<string, unknown> | undefined;
|
|
54
|
+
const block = protectedNs?.disable;
|
|
55
|
+
return block && typeof block === 'object' ? (block as DisableBlock) : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function disableReason(user: UserRow): string | null {
|
|
59
|
+
return disableBlock(user)?.reason ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Anonymization is irreversible per backend — the active toggle hides. */
|
|
63
|
+
export function isAnonymized(user: UserRow): boolean {
|
|
64
|
+
return disableReason(user) === 'anonymized';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reason-keyed status badge; null while active (the toggle already says so). */
|
|
68
|
+
export function statusBadge(user: UserRow): { label: string; tone: Tone } | null {
|
|
69
|
+
if (user.is_active) return null;
|
|
70
|
+
const reason = disableReason(user);
|
|
71
|
+
return (reason && DISABLE_REASON_BADGES[reason]) || { label: 'Inactive', tone: 'muted' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** In-flight inactivity warning — only meaningful while still active. */
|
|
75
|
+
export function inactivityWarning(user: UserRow): { sent_at: string; days: number | null } | null {
|
|
76
|
+
if (!user.is_active) return null;
|
|
77
|
+
const w = disableBlock(user)?.warning;
|
|
78
|
+
if (!w?.sent_at) return null;
|
|
79
|
+
return { sent_at: w.sent_at, days: w.days_until_disable_at_send ?? null };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Presence ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** Online if `last_activity` is within 5 minutes (UserView.isOnline). */
|
|
85
|
+
export function isOnline(user: UserRow): boolean {
|
|
86
|
+
const ms = toMs(user.last_activity);
|
|
87
|
+
if (ms == null) return false;
|
|
88
|
+
return Date.now() - ms < 5 * 60 * 1000;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Superuser / Staff / User — `is_staff` is in no live graph, so "Staff" is
|
|
92
|
+
* derived from held permissions (any grant ⇒ staff-equivalent tier). */
|
|
93
|
+
export function accountType(user: UserRow): string {
|
|
94
|
+
if (user.is_superuser) return 'Superuser';
|
|
95
|
+
const grants = Object.values(user.permissions ?? {}).filter((v) => v === true || v === 1);
|
|
96
|
+
return grants.length > 0 ? 'Staff' : 'User';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Tone maps ─────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export const LOG_LEVEL_TONE: Record<string, Tone> = {
|
|
102
|
+
error: 'danger',
|
|
103
|
+
critical: 'danger',
|
|
104
|
+
warning: 'warning',
|
|
105
|
+
warn: 'warning',
|
|
106
|
+
info: 'info',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const LOG_LEVEL_ICON: Record<string, string> = {
|
|
110
|
+
error: 'bi-shield-x',
|
|
111
|
+
critical: 'bi-shield-x',
|
|
112
|
+
warning: 'bi-exclamation-triangle',
|
|
113
|
+
warn: 'bi-exclamation-triangle',
|
|
114
|
+
info: 'bi-pencil-square',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// `LOGIN_TONE` / `loginTone(event_type)` were DELETED in #1291. They keyed on
|
|
118
|
+
// a field `UserLoginEvent` has never had, so every dot fell through to muted;
|
|
119
|
+
// their one consumer (LoginsSection) now uses `loginRiskTone(row)` from
|
|
120
|
+
// admin/security/devices/models, which reads `is_new_country` /
|
|
121
|
+
// `is_new_region` — fields the wire actually sends.
|
|
122
|
+
|
|
123
|
+
export const PROVIDER_ICONS: Record<string, string> = {
|
|
124
|
+
google: 'bi-google',
|
|
125
|
+
github: 'bi-github',
|
|
126
|
+
microsoft: 'bi-microsoft',
|
|
127
|
+
apple: 'bi-apple',
|
|
128
|
+
facebook: 'bi-facebook',
|
|
129
|
+
twitter: 'bi-twitter-x',
|
|
130
|
+
linkedin: 'bi-linkedin',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function providerIcon(provider: string | null | undefined): string {
|
|
134
|
+
return PROVIDER_ICONS[String(provider ?? '').toLowerCase()] ?? 'bi-link-45deg';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Admin tier ────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* System-pinned User mutation tier. Active-member grants cannot satisfy it;
|
|
141
|
+
* superusers retain their normal override. Gates credential writes,
|
|
142
|
+
* force-verify, the active toggle, and the destructive kebab items.
|
|
143
|
+
*/
|
|
144
|
+
export function useAdminCaller(): boolean {
|
|
145
|
+
return useCan(USER_MANAGE_PERMISSIONS).can;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Cross-record nav ──────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** Invoke the consuming app's optional Group-detail seam. */
|
|
151
|
+
export function openGroupDetail(id: number, onOpenGroup?: (groupId: number) => void) {
|
|
152
|
+
onOpenGroup?.(id);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Time helpers ──────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/** Any stored temporal shape → ms for merge-feed sorting (UserView._toMs).
|
|
158
|
+
* Delegates to the toolkit's sniffer so epoch seconds, epoch millis,
|
|
159
|
+
* numeric strings and ISO all sort against each other correctly. */
|
|
160
|
+
export function toMs(value: number | string | null | undefined): number | null {
|
|
161
|
+
return dateFns.detectTemporal(value)?.ms ?? null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Day-relative header label: Today / Yesterday / Mon, Jun 3. */
|
|
165
|
+
export function dayLabel(value: number | string | null | undefined): string {
|
|
166
|
+
const ms = toMs(value);
|
|
167
|
+
if (ms == null) return 'Unknown date';
|
|
168
|
+
const d = new Date(ms);
|
|
169
|
+
const today = new Date();
|
|
170
|
+
const startOf = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
171
|
+
const diffDays = Math.round((startOf(today) - startOf(d)) / 864e5);
|
|
172
|
+
if (diffDays === 0) return 'Today';
|
|
173
|
+
if (diffDays === 1) return 'Yesterday';
|
|
174
|
+
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* groupByDay over already-paged rows (web-mojo groupByDay('created') on the
|
|
179
|
+
* section ListViews): consecutive rows sharing a calendar day render under
|
|
180
|
+
* one header. Grouping is display-only — order stays the server's.
|
|
181
|
+
*/
|
|
182
|
+
export function groupRowsByDay<T>(rows: T[], created: (row: T) => number | string | null | undefined): { label: string; rows: T[] }[] {
|
|
183
|
+
const out: { label: string; rows: T[] }[] = [];
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
const label = dayLabel(created(row));
|
|
186
|
+
const last = out[out.length - 1];
|
|
187
|
+
if (last && last.label === label) last.rows.push(row);
|
|
188
|
+
else out.push({ label, rows: [row] });
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Section list state (server search + pages) ────────────────────────
|
|
194
|
+
|
|
195
|
+
export interface SectionListState {
|
|
196
|
+
/** Wire params: search + start/size (server-driven, rule 1). */
|
|
197
|
+
params: Params;
|
|
198
|
+
search: string;
|
|
199
|
+
setSearch: (next: string) => void;
|
|
200
|
+
page: number;
|
|
201
|
+
setPage: (next: number) => void;
|
|
202
|
+
pageSize: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* ListView-parity list state: pageSize 5, numbered paging via start/size,
|
|
207
|
+
* search resets to page 1. All of it lands on the wire — nothing filters
|
|
208
|
+
* client-side.
|
|
209
|
+
*/
|
|
210
|
+
export function useSectionList(pageSize = 5, extra?: Params): SectionListState {
|
|
211
|
+
const [search, setSearchRaw] = useState('');
|
|
212
|
+
const [page, setPage] = useState(1);
|
|
213
|
+
const params: Params = {
|
|
214
|
+
...extra,
|
|
215
|
+
start: (page - 1) * pageSize,
|
|
216
|
+
size: pageSize,
|
|
217
|
+
};
|
|
218
|
+
if (search) params.search = search;
|
|
219
|
+
return {
|
|
220
|
+
params,
|
|
221
|
+
search,
|
|
222
|
+
setSearch: (next) => { setSearchRaw(next); setPage(1); },
|
|
223
|
+
page,
|
|
224
|
+
setPage,
|
|
225
|
+
pageSize,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The search box the section lists share. */
|
|
230
|
+
export function SectionSearch({ state, placeholder }: { state: SectionListState; placeholder: string }) {
|
|
231
|
+
return (
|
|
232
|
+
<input
|
|
233
|
+
className="input us-search"
|
|
234
|
+
type="search"
|
|
235
|
+
placeholder={placeholder}
|
|
236
|
+
value={state.search}
|
|
237
|
+
onChange={(e) => state.setSearch(e.target.value)}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Compact pager: ‹ page/pages › + "n of count" readout. */
|
|
243
|
+
export function Pager({ state, count }: { state: SectionListState; count: number }) {
|
|
244
|
+
const pages = Math.max(1, Math.ceil(count / state.pageSize));
|
|
245
|
+
if (pages <= 1) return null;
|
|
246
|
+
return (
|
|
247
|
+
<div className="us-pager">
|
|
248
|
+
<button
|
|
249
|
+
className="btn btn-compact"
|
|
250
|
+
disabled={state.page <= 1}
|
|
251
|
+
onClick={() => state.setPage(state.page - 1)}
|
|
252
|
+
aria-label="Previous page"
|
|
253
|
+
>
|
|
254
|
+
<i className="bi bi-chevron-left" />
|
|
255
|
+
</button>
|
|
256
|
+
<span className="dim">{state.page} / {pages}</span>
|
|
257
|
+
<button
|
|
258
|
+
className="btn btn-compact"
|
|
259
|
+
disabled={state.page >= pages}
|
|
260
|
+
onClick={() => state.setPage(state.page + 1)}
|
|
261
|
+
aria-label="Next page"
|
|
262
|
+
>
|
|
263
|
+
<i className="bi bi-chevron-right" />
|
|
264
|
+
</button>
|
|
265
|
+
<span className="dim us-pager-count">{count} total</span>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Section tabs (TabView port for in-section tab rows) ───────────────
|
|
271
|
+
|
|
272
|
+
export function SectionTabs({ tabs, active, onSelect }: {
|
|
273
|
+
tabs: { key: string; label: string; badge?: ReactNode }[];
|
|
274
|
+
active: string;
|
|
275
|
+
onSelect: (key: string) => void;
|
|
276
|
+
}) {
|
|
277
|
+
return (
|
|
278
|
+
<div className="us-tabs" role="tablist">
|
|
279
|
+
{tabs.map((t) => (
|
|
280
|
+
<button
|
|
281
|
+
key={t.key}
|
|
282
|
+
type="button"
|
|
283
|
+
role="tab"
|
|
284
|
+
aria-selected={t.key === active}
|
|
285
|
+
className={`us-tab${t.key === active ? ' us-tab-active' : ''}`}
|
|
286
|
+
onClick={() => onSelect(t.key)}
|
|
287
|
+
>
|
|
288
|
+
{t.label}
|
|
289
|
+
{t.badge != null && t.badge !== 0 && <span className="us-tab-badge">{t.badge}</span>}
|
|
290
|
+
</button>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Relative "last seen" that reads day-relative for device rows. */
|
|
297
|
+
export function lastSeenLabel(value: number | null | undefined): string {
|
|
298
|
+
return fmt.relative(value ?? null, 'never');
|
|
299
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DetailView, Eyebrow, FlatRow, KnownFieldsCard, StatusPanel, fmt } from '../../ui';
|
|
2
|
+
import { EvidenceCard, INCIDENT_METADATA_FIELDS, RequestResponseForensics, TraceForensics, first, metadataOf } from './forensics';
|
|
3
|
+
import { showIncidentDetail, useEventDetail } from './models';
|
|
4
|
+
|
|
5
|
+
function incidentId(value: unknown): number | null {
|
|
6
|
+
if (typeof value === 'number') return value;
|
|
7
|
+
if (value && typeof value === 'object' && typeof (value as { id?: unknown }).id === 'number') return (value as { id: number }).id;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
export function EventDetail({ id, onClose }: { id: number; onClose: () => void }) {
|
|
11
|
+
const query = useEventDetail(id);
|
|
12
|
+
if (query.isPending) return <div className="modal-pad dim">Loading event…</div>;
|
|
13
|
+
if (!query.data || query.error) return <div className="modal-pad text-bad">{query.error?.message ?? 'Event not found'}</div>;
|
|
14
|
+
const event = query.data;
|
|
15
|
+
const metadata = metadataOf(event);
|
|
16
|
+
const linkedIncident = incidentId(event.incident);
|
|
17
|
+
const trace = first(metadata, 'stack_trace', 'traceback');
|
|
18
|
+
const hasOssec = event.category.startsWith('ossec') || metadata.alert_id != null || metadata.logfile != null;
|
|
19
|
+
const hasBouncer = metadata.decision != null || metadata.muid != null || metadata.triggered_signals != null;
|
|
20
|
+
const hasPermissions = metadata.permission_keys != null || metadata.perms != null || metadata.auth_result != null;
|
|
21
|
+
return <DetailView
|
|
22
|
+
icon="bi-activity"
|
|
23
|
+
title={event.title || `Event #${event.id}`}
|
|
24
|
+
subtitle={`Event #${event.id} · ${fmt.datetime(event.created)}`}
|
|
25
|
+
chips={[{ text: `L${event.level}`, tone: event.level >= 8 ? 'danger' : event.level >= 4 ? 'warning' : 'muted' }, { text: event.scope, tone: 'muted' }, { text: event.category, tone: 'muted' }]}
|
|
26
|
+
sections={[
|
|
27
|
+
{ key: 'overview', label: 'Overview', icon: 'bi-grid-1x2', render: () => <>
|
|
28
|
+
<StatusPanel tone={event.level >= 8 ? 'danger' : event.level >= 4 ? 'warning' : 'info'} state={`LEVEL ${event.level}`} headline={event.details || event.title || 'No event details'} meta={`${event.category} · ${event.scope}`} actions={linkedIncident ? <button className="btn btn-compact" onClick={() => showIncidentDetail(linkedIncident)}>Open incident</button> : null} />
|
|
29
|
+
<FlatRow label="Model">{event.model_name ? `${event.model_name} #${event.model_id ?? '—'}` : '—'}</FlatRow>
|
|
30
|
+
<FlatRow label="Group ID">{event.group_id ?? 'Global'}</FlatRow>
|
|
31
|
+
</> },
|
|
32
|
+
{ key: 'source', label: 'Source', icon: 'bi-globe2', render: () => <><Eyebrow>Source and network</Eyebrow><FlatRow label="Source IP"><code>{event.source_ip || '—'}</code></FlatRow><FlatRow label="Hostname"><code>{event.hostname || '—'}</code></FlatRow><FlatRow label="Country">{String(event.country_code || metadata.country_code || '—')}</FlatRow><FlatRow label="Server">{String(metadata.server ?? '—')}</FlatRow></> },
|
|
33
|
+
{ key: 'request', label: 'Request', icon: 'bi-funnel', render: () => <RequestResponseForensics metadata={metadata} /> },
|
|
34
|
+
...(trace ? [{ key: 'trace', label: 'Stack trace', icon: 'bi-code-square', render: () => <TraceForensics metadata={metadata} /> }] : []),
|
|
35
|
+
...(hasOssec ? [{ key: 'ossec', label: 'OSSEC', icon: 'bi-shield-exclamation', render: () => <EvidenceCard metadata={metadata} /> }] : []),
|
|
36
|
+
...(hasBouncer ? [{ key: 'bouncer', label: 'Bouncer', icon: 'bi-shield-shaded', render: () => <KnownFieldsCard data={metadata} showRaw={false} known={[{ key: 'decision', hideEmpty: true }, { key: 'risk_score', hideEmpty: true }, { key: 'page', hideEmpty: true }, { key: 'page_type', hideEmpty: true }, { key: 'muid', hideEmpty: true }, { key: 'duid', hideEmpty: true }, { key: 'triggered_signals', hideEmpty: true }]} /> }] : []),
|
|
37
|
+
...(hasPermissions ? [{ key: 'permissions', label: 'Auth evidence', icon: 'bi-key', render: () => <KnownFieldsCard data={metadata} showRaw={false} known={[{ key: 'permission_keys', hideEmpty: true }, { key: 'perms', hideEmpty: true }, { key: 'auth_result', hideEmpty: true }, { key: 'user_email', hideEmpty: true }]} /> }] : []),
|
|
38
|
+
{ key: 'metadata', label: 'Known fields', icon: 'bi-braces', render: () => <KnownFieldsCard data={metadata} known={[...INCIDENT_METADATA_FIELDS, { key: 'alert_id', hideEmpty: true }, { key: 'logfile', hideEmpty: true }, { key: 'error_code', hideEmpty: true }, { key: 'error_message', hideEmpty: true }]} showRaw={false} /> },
|
|
39
|
+
]}
|
|
40
|
+
initialSection="overview"
|
|
41
|
+
onClose={onClose}
|
|
42
|
+
/>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createSafeExporter } from '../../client';
|
|
2
|
+
import { Badge, ModelTable, fmt, groupByDay, type Column, type FilterDef, type Tone } from '../../ui';
|
|
3
|
+
import { EventModel, showEventDetail, type EventRow } from './models';
|
|
4
|
+
import { sanitizeEventRow } from './sanitize';
|
|
5
|
+
|
|
6
|
+
function levelTone(level: number): Tone { return level >= 8 ? 'danger' : level >= 4 ? 'warning' : 'muted'; }
|
|
7
|
+
const EVENT_EXPORTER = createSafeExporter<EventRow>({
|
|
8
|
+
endpoint: EventModel.endpoint, filename: 'security-events', sanitizeRow: sanitizeEventRow,
|
|
9
|
+
fields: [
|
|
10
|
+
{ key: 'id' }, { key: 'created' }, { key: 'level' }, { key: 'scope' }, { key: 'category' },
|
|
11
|
+
{ key: 'title' }, { key: 'details' }, { key: 'source_ip' }, { key: 'hostname' },
|
|
12
|
+
{ key: 'country_code' }, { key: 'model_name' }, { key: 'model_id' }, { key: 'group_id' },
|
|
13
|
+
{ key: 'server', value: (row) => row.metadata.server },
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
const FILTERS: FilterDef[] = [
|
|
17
|
+
{ key: 'created', label: 'Created', type: 'daterange' },
|
|
18
|
+
{ key: 'level', label: 'Minimum level', type: 'number', lookup: 'gte' },
|
|
19
|
+
{ key: 'level', label: 'Maximum level', type: 'number', lookup: 'lte' },
|
|
20
|
+
{ key: 'scope', label: 'Scope', type: 'text' }, { key: 'category', label: 'Category', type: 'text' },
|
|
21
|
+
{ key: 'category', label: 'Not category', type: 'text', lookup: 'not' },
|
|
22
|
+
{ key: 'source_ip', label: 'Source IP', type: 'text' }, { key: 'model_name', label: 'Model name', type: 'text' },
|
|
23
|
+
{ key: 'model_id', label: 'Model ID', type: 'number', lookup: 'exact' },
|
|
24
|
+
{ key: 'metadata__server', label: 'Server', type: 'text' },
|
|
25
|
+
{ key: 'metadata__http_url', label: 'URL contains', type: 'text' },
|
|
26
|
+
{ key: 'metadata__http_path', label: 'Path contains', type: 'text' },
|
|
27
|
+
{ key: 'metadata__http_query_string', label: 'Query contains', type: 'text' },
|
|
28
|
+
{ key: 'metadata__http_status', label: 'HTTP status', type: 'number', lookup: 'exact' },
|
|
29
|
+
{ key: 'metadata__http_user_agent', label: 'User agent contains', type: 'text' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function EventsPage() {
|
|
33
|
+
const grouping = groupByDay<EventRow>('created');
|
|
34
|
+
const columns: Column<EventRow>[] = [
|
|
35
|
+
{ key: 'created', label: 'Created', sortable: true, render: (row) => fmt.datetime(row.created) },
|
|
36
|
+
{ key: 'level', label: 'Level', sortable: false, align: 'center', render: (row) => <Badge tone={levelTone(row.level)}>L{row.level}</Badge> },
|
|
37
|
+
{ key: 'scope', label: 'Scope', sortable: false }, { key: 'category', label: 'Category', sortable: false, render: (row) => <code>{row.category}</code> },
|
|
38
|
+
{ key: 'title', label: 'Details', sortable: false, hideable: false, render: (row) => <span title={row.details ?? undefined}>{fmt.truncate(row.title || row.details || 'No details', 80)}</span> },
|
|
39
|
+
{ key: 'source_ip', label: 'Source IP', sortable: false, render: (row) => <code>{row.source_ip || '—'}</code> },
|
|
40
|
+
{ key: 'metadata__server', label: 'Server', sortable: false, render: (row) => String(row.metadata.server ?? row.hostname ?? '—') },
|
|
41
|
+
];
|
|
42
|
+
return <ModelTable
|
|
43
|
+
model={EventModel} title="Events" eyebrow="Security operations"
|
|
44
|
+
columns={columns} filters={FILTERS} searchable searchPlaceholder="Search event details"
|
|
45
|
+
defaultParams={{ sort: '-created', category__not: 'ossec' }}
|
|
46
|
+
rowTone={(row) => row.level >= 8 ? 'danger' : row.level >= 4 ? 'warning' : null}
|
|
47
|
+
{...grouping} groupHeaderStyle="band"
|
|
48
|
+
exporter={EVENT_EXPORTER} exportFormats={['csv', 'json']}
|
|
49
|
+
columnChooser persistState persistKey="admin-security-events" autoRefresh={30}
|
|
50
|
+
onRowClick={(row) => showEventDetail(row.id)}
|
|
51
|
+
/>;
|
|
52
|
+
}
|