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
package/docs/charts.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# charts — dependency-free SVG
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
SeriesChart, MetricsChart, MiniChart, MetricsMiniWidget,
|
|
6
|
+
KPITile, KPIStrip, CircularProgress, PieChart,
|
|
7
|
+
showSeriesStats, showSeriesData, exportChartPng,
|
|
8
|
+
computeSeriesStats, computeTrend, deltaBadge, // pure math
|
|
9
|
+
} from 'portal-mojo/charts';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
No chart library — web-mojo's SVG charts ported whole; they match the
|
|
13
|
+
design tokens in both themes. Demos: Develop → Components → Display.
|
|
14
|
+
|
|
15
|
+
## The metrics wire (all metrics-driven components speak it)
|
|
16
|
+
|
|
17
|
+
`GET /api/metrics/fetch` params:
|
|
18
|
+
|
|
19
|
+
| param | meaning |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `slugs` | comma list, **REQUIRED** (the server 400s without; singular `slug` exists but never send it — prod once rejected it) |
|
|
22
|
+
| `granularity` | `minutes/hours/days/weeks/months` (server default `hours`) |
|
|
23
|
+
| `account` | `'public'` (server default) / `'global'` / `'group-<id>'` / `'user-<id>'` — components default to `'global'` (web-mojo parity) |
|
|
24
|
+
| `dt_start` / `dt_end` | window bounds, **epoch SECONDS** (rule 6) |
|
|
25
|
+
| `category` | optional category passthrough via `apiParams` |
|
|
26
|
+
| `child_kind`, `breakdown` | typed `MetricsChart` fan-out props (`childKind`, `breakdown`) |
|
|
27
|
+
|
|
28
|
+
Response is a slug-keyed map `{data: {slug: number[]}, labels: string[]}` —
|
|
29
|
+
normalized ONLY by `mojoMetrics` in the client into `{labels, datasets}`.
|
|
30
|
+
Malformed feeds degrade to the empty state WITH a console.warn — never a
|
|
31
|
+
crash.
|
|
32
|
+
|
|
33
|
+
**Two windowing params ride together, deliberately:** the real backend uses
|
|
34
|
+
`dt_*` and ignores `range`; components send both for quick ranges and only
|
|
35
|
+
`dt_*` for custom ranges. The mock now gives `dt_start`/`dt_end` precedence,
|
|
36
|
+
accepts either bound alone, and mirrors the backend's inclusive buckets.
|
|
37
|
+
Reverse ranges return an empty series; malformed non-epoch bounds reject.
|
|
38
|
+
Month/year buckets advance by calendar boundaries, not 30/365-day arithmetic.
|
|
39
|
+
The mock caps a pathological request at 400 buckets to keep the showcase
|
|
40
|
+
responsive. It also salts values by `account` so entity-scope demos visibly
|
|
41
|
+
change; that salt is a mock-only presentation affordance, not a server promise.
|
|
42
|
+
|
|
43
|
+
**Trap (fixed here, do not regress):** `dr_start/dr_end` is the model-LIST
|
|
44
|
+
daterange triple — `/api/metrics/fetch` ignores it silently. web-mojo sent
|
|
45
|
+
`dr_*` to metrics for years; its custom ranges never reached the backend.
|
|
46
|
+
|
|
47
|
+
## SeriesChart (presentational)
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<SeriesChart
|
|
51
|
+
labels={['Mon', 'Tue']}
|
|
52
|
+
datasets={[{ label: 'API Calls', data: [420, 380] }]}
|
|
53
|
+
chartType="line" // 'line' | 'bar' | 'area'
|
|
54
|
+
stacked="auto" // bars stack by default
|
|
55
|
+
height={240}
|
|
56
|
+
showLegend showGrid showXLabels showYLabels
|
|
57
|
+
valueFormatter={(n) => ...}
|
|
58
|
+
/>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Legend entries toggle series; hover shows the crosshair index tooltip;
|
|
62
|
+
ticks are "nice"-rounded; container width is measured (no viewBox
|
|
63
|
+
smearing).
|
|
64
|
+
|
|
65
|
+
## MetricsChart (the django-mojo metrics widget)
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
<MetricsChart
|
|
69
|
+
title="Platform activity"
|
|
70
|
+
slugs={['api_calls', 'user_activity_day', 'api_errors']}
|
|
71
|
+
seriesLabels={{ api_calls: 'API Calls' }} // wire carries raw slugs
|
|
72
|
+
account="global"
|
|
73
|
+
defaultRange="24h" // 1H / 24H / 7D / 30D
|
|
74
|
+
defaultGranularity="hours" // gated per range (MIN HR DAY WK MO)
|
|
75
|
+
defaultType="line"
|
|
76
|
+
height={280}
|
|
77
|
+
valueFormatter={(n) => ...} // tooltips, ticks, dialogs
|
|
78
|
+
childKind="store" breakdown={false} // typed fan-out; overwrites apiParams
|
|
79
|
+
loadSeries={exactLoader} // optional; default is mojoMetrics
|
|
80
|
+
seriesCacheKey="caller-42:exact-history" // required stable namespace for custom loaders
|
|
81
|
+
preserveSeriesLabels // keep full-slug/child identity verbatim
|
|
82
|
+
allowedGranularities={['minutes','hours','days']} // optional backend subset; unsupported controls omitted
|
|
83
|
+
refreshSignal={refreshVersion} // changed by a parent refresh owner; exactly one fetch
|
|
84
|
+
// toggles, all default true:
|
|
85
|
+
showGranularity showDateRange showTypeSwitch showRefresh showStats showDataTable
|
|
86
|
+
/>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **Stats (ⓘ)** — per-series Latest/Min/Max/Avg/Median/Sum dialog.
|
|
90
|
+
**View data (table)** — the series as a table + Download CSV. Both are
|
|
91
|
+
native `<dialog>`s via the modal manager; both render the DISPLAY names
|
|
92
|
+
(`seriesLabels`-mapped).
|
|
93
|
+
- **Custom range** — the calendar button in the range segment opens a
|
|
94
|
+
DateRangePicker (presets rail) dialog with an explicit Apply. An applied
|
|
95
|
+
range shows as a chip beside the title, sends `dt_start/dt_end` (local
|
|
96
|
+
day bounds 00:00:00–23:59:59 → epoch seconds), and re-points granularity
|
|
97
|
+
at a bucket that fits the span (`granularitiesForSpanMs`).
|
|
98
|
+
- Quick ranges anchor their window at pick time; **Refresh re-anchors at
|
|
99
|
+
now** (web-mojo kept the stale window — deliberate fix). A quick-window
|
|
100
|
+
refresh changes the query key and does not also call `refetch`, preventing
|
|
101
|
+
the former duplicate request. `refreshSignal` follows the same one-request
|
|
102
|
+
path for a parent-owned refresh control. Range switches
|
|
103
|
+
re-point granularity at the nearest sensible bucket.
|
|
104
|
+
- `allowedGranularities` additively narrows the control vocabulary for a
|
|
105
|
+
backend such as CloudWatch. Values outside the supported subset are not
|
|
106
|
+
rendered; normal metrics callers retain all five granularities.
|
|
107
|
+
- `loadSeries(params)` receives typed `slugs`, `account`, `granularity`,
|
|
108
|
+
`dt_start`, `dt_end`, and optional fan-out fields. The loader namespace is
|
|
109
|
+
part of the TanStack key, preventing a custom exact-identity loader from
|
|
110
|
+
reusing a cached default response with the same wire params.
|
|
111
|
+
- The default loader remaps live tail-only response keys back to configured
|
|
112
|
+
full slugs when that mapping is unique. Duplicate tails remain an explicit
|
|
113
|
+
error and require an exact loader that can split/reassemble requests.
|
|
114
|
+
- CSV data export neutralizes formula-looking **string** cells before RFC
|
|
115
|
+
quoting. Negative numeric metrics remain numeric.
|
|
116
|
+
- Deliberately NOT carried: `compactHeader` (MetricsMiniWidget IS the
|
|
117
|
+
compact card), `.export()` (removed in web-mojo too — use
|
|
118
|
+
`exportChartPng`), `withDelta` endpoint switching (KPIStrip covers the
|
|
119
|
+
KPI use-case).
|
|
120
|
+
|
|
121
|
+
## MiniChart (sparkline primitive)
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<MiniChart data={[4, 9, 6]} labels={['Mon','Tue','Wed']} chartType="line"
|
|
125
|
+
height={48} color="var(--accent)" fill smoothing={0.3}
|
|
126
|
+
showTooltip showCrosshair showXAxis={false}
|
|
127
|
+
minValue={} maxValue={} softMin={} softMax={} />
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Single series, line or bar. Hard crops (`minValue/maxValue`) win over bar
|
|
131
|
+
soft bounds (`softMin/softMax` — normalize-to targets that expand if data
|
|
132
|
+
exceeds). Bars always include zero; the x-axis sits at zero when data
|
|
133
|
+
crosses it. All-zero bar data renders the dashed "alive, just zero"
|
|
134
|
+
baseline instead of blank. Draw-in animation is pathLength-normalized CSS
|
|
135
|
+
(no measuring); `prefers-reduced-motion` kills it. Colors accept `var()`
|
|
136
|
+
tokens — the default fill is the stroke at 0.12 opacity, so one token
|
|
137
|
+
drives both.
|
|
138
|
+
|
|
139
|
+
## MetricsMiniWidget (compact card + entity search)
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<MetricsMiniWidget
|
|
143
|
+
title="Logins" icon="bi bi-box-arrow-in-right"
|
|
144
|
+
slugs={['user_activity_day']} // FIRST slug drives the card
|
|
145
|
+
granularity="hours" defaultRange="24h" chartType="line"
|
|
146
|
+
subtitle={(ctx) => <><b>{ctx.total}</b> in 24h · {ctx.nowLabel}: {ctx.nowValue}</>}
|
|
147
|
+
showTrending trendRange={6}
|
|
148
|
+
tone="accent" // token-tinted card variant
|
|
149
|
+
showSettings showDateRange settingsKey="dash-activity"
|
|
150
|
+
search={{ // the C2 addition
|
|
151
|
+
model: GroupModel, // or endpoint: '/api/group'
|
|
152
|
+
toAccount: (id) => `group-${id}`, // picked row → account param
|
|
153
|
+
placeholder: 'All groups (global)',
|
|
154
|
+
}}
|
|
155
|
+
onScopeChange={(account, row) => ...}
|
|
156
|
+
/>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- **Entity search**: a CollectionSelect bound to any model list — shared
|
|
160
|
+
cache keys, NEVER a parallel fetch path. Picking a record re-fetches
|
|
161
|
+
with `account=toAccount(id, row)`; clearing returns to the base
|
|
162
|
+
`account` prop.
|
|
163
|
+
- **subtitle** is a ReactNode or `(ctx) => ReactNode`; `ctx` is the typed
|
|
164
|
+
twin of web-mojo's Mustache tokens: `{total, nowValue, lastValue,
|
|
165
|
+
prevValue, trendingPercent, nowLabel, totalLabel, granularity}`
|
|
166
|
+
(`nowValue` is ALWAYS the latest bucket; `lastValue` is the
|
|
167
|
+
offset-shifted trend-window sum — source distinction preserved).
|
|
168
|
+
- **Trending** is the source's windowed math (`computeTrend`):
|
|
169
|
+
`trendRange` (k = floor(range/2) buckets per window), `trendOffset`
|
|
170
|
+
(skip trailing partial buckets), `prevTrendOffset` (compare N earlier).
|
|
171
|
+
Up renders green / down red (source parity — no tone flip here; KPITile
|
|
172
|
+
has `tone`).
|
|
173
|
+
- **Settings** (gear, off by default): granularity / chart type / date
|
|
174
|
+
range dialog; persisted under localStorage `metrics-chart-<settingsKey>`
|
|
175
|
+
(source key format). A granularity change with a custom window in play
|
|
176
|
+
auto-adjusts the window (hours→24h … years→5y, source table); it never
|
|
177
|
+
sets dates for the first time.
|
|
178
|
+
- `defaultRange={null}` omits window params entirely (backend default
|
|
179
|
+
window — the source MiniChart's behavior when no range was given).
|
|
180
|
+
- Card is single-series by design (source parity); extra slugs still feed
|
|
181
|
+
the stats/view-data dialogs. A response without the first slug warns
|
|
182
|
+
once and shows the empty state.
|
|
183
|
+
|
|
184
|
+
## KPITile + KPIStrip
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
<KPIStrip
|
|
188
|
+
tiles={[
|
|
189
|
+
{ slug: 'api_calls', label: 'API Calls', tone: 'good' },
|
|
190
|
+
{ slug: 'api_errors', label: 'API errors', tone: 'bad', severity: 'warn' },
|
|
191
|
+
{ key: 'open', label: 'Open Tickets', value: openCount, sparklineSlug: 'api_calls' },
|
|
192
|
+
]}
|
|
193
|
+
granularity="days" range="7d" account="global"
|
|
194
|
+
onTileClick={({ slug, key }) => ...}
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
ONE batched `/api/metrics/fetch` covers values, deltas and sparklines:
|
|
199
|
+
value = latest bucket, delta = latest − previous, `deltaPct` omitted when
|
|
200
|
+
the previous bucket was 0 — the same convention
|
|
201
|
+
`/api/metrics/series?with_delta=true` uses, so a server-delta variant is a
|
|
202
|
+
drop-in later. A spec with `value` set is a caller-value tile (the
|
|
203
|
+
source's REST-count tiles — pages already have `size:0` count queries);
|
|
204
|
+
`sparklineSlug` lets it borrow a metric trail + delta. Missing slugs warn
|
|
205
|
+
once and render '—'.
|
|
206
|
+
|
|
207
|
+
`KPITile` alone is pure presentation: `label`, `value`, `delta`,
|
|
208
|
+
`deltaPct`, `tone` ('bad' = rising is bad), `severity`
|
|
209
|
+
(critical/high/warn/info/good left stripe; high shares the warn token —
|
|
210
|
+
no orange in the palette), `sparkline`, `onClick`, `loading`. Badge rules
|
|
211
|
+
(source verbatim, via `deltaBadge`): finite pct → `+12%` (one decimal
|
|
212
|
+
under 10), else absolute delta → `+4`, else no badge; ±0 renders flat.
|
|
213
|
+
**Never Infinity%.**
|
|
214
|
+
|
|
215
|
+
## CircularProgress
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
<CircularProgress value={64} size="md" /> // xs sm md lg xl | px
|
|
219
|
+
<CircularProgress value={64} gap={90} rotation={135} /> // gauge
|
|
220
|
+
<CircularProgress value={7} max={10} valueFormat="fraction" /> // 7/10
|
|
221
|
+
<CircularProgress segments={[{ value: 38, color: 'var(--accent)' }, ...]} max={100} />
|
|
222
|
+
<CircularProgress gradientColors={['var(--accent)', 'var(--ok)']} />
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Ring progress with size presets (stroke auto-scales), variants mapped to
|
|
226
|
+
tokens (`success/danger/warning/info` → ok/bad/warn/info), partial-arc
|
|
227
|
+
gauges, rounded caps, center value (`percentage | fraction | value` or a
|
|
228
|
+
`valueFormatter(value, min, max)`), `label`, `icon`, and multi-segment
|
|
229
|
+
mode with `segmentGap` degrees. Value changes glide via the CSS
|
|
230
|
+
dash-offset transition (the imperative `animateTo/pulse` API from the
|
|
231
|
+
source is just… setting the prop). Dropped from source: the Bootstrap
|
|
232
|
+
popover tooltip (pass `title` or wrap it) and the hardcoded 'dark'/'light'
|
|
233
|
+
theme presets (tokens make them meaningless).
|
|
234
|
+
|
|
235
|
+
## PieChart
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
<PieChart data={[{ label: 'Chrome', value: 61, color: 'var(--info)' }]} /> // shape 1
|
|
239
|
+
<PieChart data={{ labels: ['a'], datasets: [{ data: [1] }] }} /> // shape 2 (Chart.js)
|
|
240
|
+
<PieChart data={{ US: 42, DE: 21 }} /> // shape 3 (map)
|
|
241
|
+
<PieChart endpoint="/api/some/summary" params={{ days: 7 }} /> // fetched
|
|
242
|
+
<PieChart data={d} cutout={0.62}
|
|
243
|
+
centerLabel={(ctx) => ctx.total.toLocaleString()} centerSubLabel="users" />
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Native SVG pie/doughnut. All three web-mojo input shapes; unknown shapes
|
|
247
|
+
warn + empty state. Colors: per-item `color` → `colors[i]` (default
|
|
248
|
+
CHART_PALETTE) → `colorGenerator(i)` (default golden angle — distinct hues
|
|
249
|
+
forever). `legendPosition` right/bottom/none; `showLabels` puts labels at
|
|
250
|
+
the slice edges; tooltip clamps inside the svg area; `onSliceClick` gets
|
|
251
|
+
`{label, value, pct, index}`. Data changes tween label-keyed (existing
|
|
252
|
+
slices slide, new ones grow from a 0-arc; rAF, cancelled on unmount).
|
|
253
|
+
Center labels are strings (SVG `<text>` — not a ReactNode slot). The
|
|
254
|
+
single-full-circle case renders a circle element (SVG arcs can't draw
|
|
255
|
+
360°). `endpoint` fetches through `mojoCall` (the one boundary) with the
|
|
256
|
+
source's `data.data ?? data` nesting heuristic; fetch errors warn + empty.
|
|
257
|
+
Pure helpers (`parsePieInput`, `buildPieGeometry`, `goldenAngleColor`,
|
|
258
|
+
`interpolateSegments`, `arcPath`) are exported from `pie-math`.
|
|
259
|
+
|
|
260
|
+
## Stats + view-data dialogs (shared)
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
showSeriesStats({ title, labels, datasets, granularity, formatter });
|
|
264
|
+
showSeriesData({ title, labels, datasets, granularity, formatter }); // + Download CSV
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Awaitable native-`<dialog>`s (modal manager). Stats: per-series
|
|
268
|
+
Latest/Min/Max/Avg/Median/Sum with the "Hourly · N points" header. Data:
|
|
269
|
+
one row per label, one column per series; CSV downloads as
|
|
270
|
+
`<title-slug>-<date>.csv` (RFC-safe quoting via `buildCsv`). Empty data
|
|
271
|
+
renders "No data to display." — never a blank dialog.
|
|
272
|
+
|
|
273
|
+
## exportChartPng
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
exportChartPng(refOrElement, { filename: 'chart.png', background: null, scale: 2 });
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Framework-free SVG→PNG: finds the `<svg>` in the target (element or ref),
|
|
280
|
+
clones it, **inlines computed styles** (token-driven fills/strokes/fonts
|
|
281
|
+
would otherwise vanish — theme classes don't serialize), rasterizes at
|
|
282
|
+
`scale` (default 2) over the nearest ancestor's real background (dark
|
|
283
|
+
exports stay dark; pass a color or `null` for transparent), and downloads.
|
|
284
|
+
⚠️ Tainted-canvas caveat (from the source, still true): an SVG referencing
|
|
285
|
+
cross-origin resources (external `<image href>`, remote fonts) taints the
|
|
286
|
+
canvas and `toDataURL` throws a SecurityError. These charts reference
|
|
287
|
+
none; inline such resources before exporting anything that does.
|
|
288
|
+
|
|
289
|
+
## Pure math (headless-tested)
|
|
290
|
+
|
|
291
|
+
`computeSeriesStats`, `defaultStatFormat`, `computeTrend`, `deltaBadge`,
|
|
292
|
+
`buildCsv`/`csvEscape`/`csvFilename`, `quickRangeWindow`,
|
|
293
|
+
`granularitiesForSpanMs`, `ymdRangeToEpochSeconds`, `toNumber` — all in
|
|
294
|
+
`charts/stats.ts`, no DOM/React, shared by every surface above (web-mojo
|
|
295
|
+
carried two diverging copies of the stats math; here there is one).
|
|
296
|
+
|
|
297
|
+
## WorldMap
|
|
298
|
+
|
|
299
|
+
`portal-mojo/charts` also exports `WorldMap` — the dependency-free geo map
|
|
300
|
+
(markers, routes, drill-down, the country centroid table) plus its projection
|
|
301
|
+
math. It has its own page: **[worldmap.md](worldmap.md)**. Note that
|
|
302
|
+
`charts/pie-math` exports `arcPath` and the map's arc helper is therefore
|
|
303
|
+
`geoArcPath`.
|
package/docs/client.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# client — the typed django-mojo protocol layer
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import {
|
|
5
|
+
mojoList, mojoGet, mojoSave, mojoDelete, mojoDownload, mojoMetrics,
|
|
6
|
+
mojoCall, mojoQueryDefaults, usingMockTransport, apiOrigin,
|
|
7
|
+
MojoError, AuthRequiredError,
|
|
8
|
+
} from 'portal-mojo/client';
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Transport
|
|
12
|
+
|
|
13
|
+
- `VITE_MOJO_API` unset → the in-memory mock answers every call
|
|
14
|
+
(`npm run dev`). Set to a django-mojo origin → real backend
|
|
15
|
+
(`npm run dev:live`, configured in `apps/portal/.env.live`).
|
|
16
|
+
- Every request carries `X-Mojo-UID` (device id) and, once `initAuth()` ran
|
|
17
|
+
and a session exists, `Authorization: Bearer <jwt>`.
|
|
18
|
+
- CORS note (dev, cross-origin): every request shows an `OPTIONS` preflight
|
|
19
|
+
twin in devtools. Not a duplicate fetch.
|
|
20
|
+
|
|
21
|
+
## Envelope — the ONE unwrap boundary
|
|
22
|
+
|
|
23
|
+
django-mojo wraps everything: `{status, data, …}`. `status: false` (or a
|
|
24
|
+
non-2xx with an envelope body) becomes a **thrown `MojoError(message,
|
|
25
|
+
status, errorCode, data)`** carrying the server's real message, numeric HTTP
|
|
26
|
+
status, semantic `error_code`, and structured safe failure evidence. Nothing outside `client.ts`
|
|
27
|
+
parses envelopes; `mojoCall(path, {method, params, body})` is the typed
|
|
28
|
+
escape hatch for protocol modules and returns the unwrapped `Envelope`.
|
|
29
|
+
|
|
30
|
+
Live non-2xx responses and mock `status:false` envelopes preserve the same
|
|
31
|
+
shape. Modern envelopes use numeric top-level `code` plus a string semantic
|
|
32
|
+
`error_code`; numeric legacy `error_code` remains a status fallback. Failed
|
|
33
|
+
envelopes always reject. Structured `data` can contain deliberately safe
|
|
34
|
+
recovery evidence, but callers must still avoid logging it indiscriminately.
|
|
35
|
+
|
|
36
|
+
## Functions
|
|
37
|
+
|
|
38
|
+
| Fn | Wire | Returns |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `mojoList<T>(endpoint, params)` | `GET endpoint?start&size&sort&search&<lookups>` | `{rows: T[], count, start, size}` |
|
|
41
|
+
| `mojoGet<T>(endpoint, id)` | `GET endpoint/<id>` | `T` |
|
|
42
|
+
| `mojoSave<T>(endpoint, id\|null, changes)` | `POST endpoint[/<id>]` | `T` (server-authoritative row). REJECTS on failure |
|
|
43
|
+
| `mojoDelete(endpoint, id)` | `DELETE endpoint/<id>` | `void` — server answers `{status:"deleted"}` (string; deliberate) |
|
|
44
|
+
| `mojoDownload(endpoint, params, 'csv'\|'json')` | `GET endpoint?download_format&filename&<filters>` (start/size stripped) | triggers a browser download of the WHOLE filtered set |
|
|
45
|
+
| `mojoMetrics(params)` | `GET /api/metrics/fetch?slugs&range&granularity` | `{labels, datasets:[{label: slug, data}]}` — normalized from the wire's `{data:{slug: number[]}, labels}` |
|
|
46
|
+
|
|
47
|
+
Wire facts (measured against live django-mojo): list sort is `'-field'`
|
|
48
|
+
desc; filters are Django lookups (`field`, `field__in=a,b`,
|
|
49
|
+
`field__icontains`, `field__gte`, `field__isnull=true`); the daterange
|
|
50
|
+
triple is `dr_field/dr_start/dr_end`; datetimes serialize as epoch seconds;
|
|
51
|
+
unknown params are silently ignored by the server.
|
|
52
|
+
|
|
53
|
+
The executable mock coerces comparisons narrowly: both operands must be
|
|
54
|
+
fully numeric for numeric comparison; temporal comparison only recognizes
|
|
55
|
+
canonical `YYYY[-MM[-DD]]` or full ISO datetime shapes; everything else is
|
|
56
|
+
text. This keeps numeric IDs (`10 > 2`) and `User.dob` DateFields correct
|
|
57
|
+
without turning arbitrary date-looking labels into timestamps. The showcase
|
|
58
|
+
Filters demo exercises both `dob__gte=YYYY-MM-DD` and epoch-backed `dr_*`.
|
|
59
|
+
|
|
60
|
+
Mock-only contract controls are exported in development: call
|
|
61
|
+
`armMockReauth(method, path)` to make one authenticated method+path match
|
|
62
|
+
answer 440. Ordinary requests and real transports are unaffected.
|
|
63
|
+
|
|
64
|
+
## Mock admin contracts
|
|
65
|
+
|
|
66
|
+
The mock carries measured GroupView/admin transports, not UI-shaped
|
|
67
|
+
shortcuts:
|
|
68
|
+
|
|
69
|
+
- group API keys use `/api/group/apikey`; ordinary/default rows omit the raw
|
|
70
|
+
token, create returns it, and only explicit `graph=token` reads it later;
|
|
71
|
+
- webhook subscriptions validate HTTPS URLs, reject URL credentials, and
|
|
72
|
+
keep metadata detail-only; `/api/group/webhook_secret` is the deliberate
|
|
73
|
+
read/rotate secret endpoint;
|
|
74
|
+
- member invites stamp the requested group and member edits preserve the
|
|
75
|
+
backend permission-dict behavior; group incident rows filter through
|
|
76
|
+
`/api/incident/event?group_id=`;
|
|
77
|
+
- `/api/auth/config` resolves defaults → deployment → root → child, replaces
|
|
78
|
+
lists, preserves nested dict keys, ignores unknown/inactive UUIDs, and
|
|
79
|
+
returns only the public whitelist;
|
|
80
|
+
- settings keep plaintext for secret rows in a private mock slot, while every
|
|
81
|
+
wire response carries `value:''` + `display_value:'******'`. Payload order
|
|
82
|
+
deliberately mirrors live `set_value`: serialize `is_secret` before
|
|
83
|
+
`value`. Non-null group scopes enforce `(key, group)` uniqueness; repeated
|
|
84
|
+
global `(key, null)` rows remain possible because PostgreSQL NULL uniqueness
|
|
85
|
+
does. REST delete is denied;
|
|
86
|
+
- metrics permissions are top-level `/api/metrics/permissions` responses, not
|
|
87
|
+
model envelopes. Single permission reads may be strings, multiple are
|
|
88
|
+
lists, and cleared values read as null. Clearing/deleting retains the
|
|
89
|
+
account row in the list, matching the separate Redis account index.
|
|
90
|
+
|
|
91
|
+
Stable mock permission identities (password `mojo`) make gates reproducible:
|
|
92
|
+
`ian@mojoverify.com` is only an odd-group admin;
|
|
93
|
+
`security.viewer@nativemojo.com` has global security/geofence view;
|
|
94
|
+
`security.manager@nativemojo.com` has security/geofence/settings/metrics
|
|
95
|
+
management; `groups.manager@nativemojo.com` has global groups/users
|
|
96
|
+
management. `/api/geo/check?__mock_country=CN` is the deterministic public
|
|
97
|
+
deny case; the default US case allows.
|
|
98
|
+
|
|
99
|
+
The record-feed mock follows the shared Django feed shape rather than a
|
|
100
|
+
component-specific adapter. `/api/incident/ticket/note` and
|
|
101
|
+
`/api/incident/incident/history` return the newest 100 rows, support parent
|
|
102
|
+
and group filtering, and explicitly use `graph=default`. Posts reject unknown
|
|
103
|
+
parents, inherit the parent's group, stamp the authenticated user, embed that
|
|
104
|
+
user in the response, and keep `media` null.
|
|
105
|
+
|
|
106
|
+
Bouncer operator data is available to the stable global security identities:
|
|
107
|
+
signals and devices provide seeded list/detail graphs, while bot signatures
|
|
108
|
+
exercise create, update, uniqueness validation, and enable/disable; DELETE
|
|
109
|
+
matches the live endpoint's denial. The security
|
|
110
|
+
viewer is read-only; the security manager can mutate supported resources.
|
|
111
|
+
Signal detail deliberately omits `token_nonce`: it is neither needed by the
|
|
112
|
+
admin portal nor safe to type, store, or cache as operator-facing data. Block
|
|
113
|
+
and monitor decisions are also seeded into the incident-event feed. The
|
|
114
|
+
read-only `/api/incident/incident` mock follows the real reporter contract:
|
|
115
|
+
Bouncer device investigations filter the security category and search the
|
|
116
|
+
incident details for the device MUID, without invented model linkage.
|
|
117
|
+
|
|
118
|
+
The mock-only component showcase signs in as
|
|
119
|
+
`showcase.operator@nativemojo.com`, an explicit broad demo identity. The
|
|
120
|
+
narrow `security.viewer`, `security.manager`, `groups.manager`, and Ian
|
|
121
|
+
fixtures remain unchanged for permission-boundary verification.
|
|
122
|
+
|
|
123
|
+
Communications mock parity includes `/api/aws/email/{domain,mailbox,sent,template,send}`
|
|
124
|
+
and `/api/account/public_message`. Credential plaintext is reduced to masks
|
|
125
|
+
and discarded, managed onboarding writes through the registered DNS adapter,
|
|
126
|
+
audit persists domain status, send may return a 2xx `failed` row, and mailbox
|
|
127
|
+
and domain deletes reproduce local audit cascades. Stable email/support
|
|
128
|
+
operator identities are listed in [admin-messaging.md](admin-messaging.md).
|
|
129
|
+
|
|
130
|
+
DNSMan's mock contract is central-state-backed and capability-first. Stable
|
|
131
|
+
identities `dns.viewer@nativemojo.com`, `dns.manager@nativemojo.com`, the
|
|
132
|
+
group-1-only member `dns.tenant@nativemojo.com`, and the house-only superuser
|
|
133
|
+
`dns.platform@nativemojo.com` (password `mojo`) exercise the exact global
|
|
134
|
+
view/manage, tenant, and platform split. Credential
|
|
135
|
+
assignment searches only `/api/dnsman/credential/group-choice`; an exact
|
|
136
|
+
selection hydrates with `?id=…`, and inactive or over-depth groups never
|
|
137
|
+
appear. First-link verification failure creates no row. Failed rotation marks
|
|
138
|
+
the existing row unverified while retaining its old masks. Successful raw
|
|
139
|
+
keys are used only by the imperative call and are never stored in mock state
|
|
140
|
+
or returned. Registrar confirmation tokens, legal-contact PII, WHOIS PII, and
|
|
141
|
+
certificate material likewise stay outside model/query state.
|
|
142
|
+
|
|
143
|
+
## Imperative file uploads
|
|
144
|
+
|
|
145
|
+
`startFileUpload(file, options?)` implements django-mojo's three stages:
|
|
146
|
+
initiate the File, transfer bytes to the returned capability, then reconcile
|
|
147
|
+
and mark the File completed. It returns a `FileUploadTask` immediately; its
|
|
148
|
+
`result` always resolves to a fixed `completed`, `failed`, or `uncertain`
|
|
149
|
+
outcome.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const task = startFileUpload(file, { fileManagerId: 12, groupId: 4, use: 'attachments' });
|
|
153
|
+
const unsubscribe = task.subscribe(({ phase, loadedBytes, totalBytes }) => {
|
|
154
|
+
console.log(phase, loadedBytes, totalBytes);
|
|
155
|
+
});
|
|
156
|
+
const outcome = await task.result;
|
|
157
|
+
unsubscribe();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Progress is the count reported by the actual byte transport. Multipart POST
|
|
161
|
+
progress therefore includes wire overhead. Provider fields are appended
|
|
162
|
+
before `file`, and the browser owns the multipart boundary. The #1485 response
|
|
163
|
+
keeps `upload_url`, `method`, `fields`, and `headers` flat beside the safe File
|
|
164
|
+
lifecycle fields. PUT and POST are supported only. The server-echoed MIME type
|
|
165
|
+
governs the byte request.
|
|
166
|
+
Root-relative direct-upload paths missing the deployment's `/api` prefix are
|
|
167
|
+
repaired before transfer. API Bearer and DUID headers are never forwarded;
|
|
168
|
+
an unsafe backend-provided Bearer transfer header produces only a fixed safe
|
|
169
|
+
outcome.
|
|
170
|
+
|
|
171
|
+
Snapshots and outcomes never contain capability URLs, provider fields or
|
|
172
|
+
headers, API bearer/DUID values, raw response bodies, or File URLs. A completed
|
|
173
|
+
outcome exposes only an `UploadedFileRef`: authoritative id, name, MIME, size,
|
|
174
|
+
category, manager id, and group id.
|
|
175
|
+
|
|
176
|
+
Every task generates one private strict `idempotency_key`. `cancel()` aborts
|
|
177
|
+
current work. Even an initiation abort is `uncertain` with a
|
|
178
|
+
nullable File id: the server may have committed before the response became
|
|
179
|
+
observable. `recover()` cannot act without an id and returns the same outcome;
|
|
180
|
+
calling `retry()` replays the same key so a committed-but-lost initiation
|
|
181
|
+
returns the same File. Cancellation after a known initiation, dropped provider responses,
|
|
182
|
+
and ambiguous completion responses are likewise `uncertain`, since remote
|
|
183
|
+
bytes or a completed File may already exist. `recover()` reconciles and
|
|
184
|
+
attempts completion without replaying bytes. `retry()` reconciles first and
|
|
185
|
+
replays the retained private capability only while the File is still
|
|
186
|
+
uploading. If reconciliation proves failed/expired, that same Retry rotates to
|
|
187
|
+
a fresh key and attempt; completed replays need no target. Both are single-flight and generation guarded. A successful
|
|
188
|
+
completion POST is never authoritative by itself; a following File GET must
|
|
189
|
+
confirm `completed` before the task returns success. Completed scalar
|
|
190
|
+
`file_manager_id` and `group_id` must match an explicitly selected destination.
|
|
191
|
+
|
|
192
|
+
## `mojoQueryDefaults()`
|
|
193
|
+
|
|
194
|
+
Spread into the app's `QueryClient` defaults. Provides: no retry on 4xx
|
|
195
|
+
`MojoError`s (deterministic failures), `networkMode: 'always'` under the
|
|
196
|
+
mock ('online' live), and `refetchOnWindowFocus: false` (freshness comes
|
|
197
|
+
from explicit refresh + opt-in autoRefresh — focus storms read as phantom
|
|
198
|
+
fetches).
|
|
199
|
+
|
|
200
|
+
## Errors
|
|
201
|
+
|
|
202
|
+
- `MojoError{message, status, errorCode, data}` — server-reported failure;
|
|
203
|
+
`message` is the server's text, `status` is numeric, `errorCode` preserves
|
|
204
|
+
the semantic wire code, and `data` preserves safe structured evidence.
|
|
205
|
+
- `AuthRequiredError` — thrown by the pre-request gate when a request needs
|
|
206
|
+
a session that doesn't exist (synthetic 401; no network was touched).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# CollectionMultiSelect — server-backed multi-pick dropdown
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
import { CollectionMultiSelect } from 'portal-mojo/ui';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
A checkbox list over live model data — server search, `SELECT (n)` /
|
|
8
|
+
`DESELECT (n)` with live counts, shift-click range selection — presented by
|
|
9
|
+
default as a **dropdown**: a summary trigger + `<Popover>` menu sharing
|
|
10
|
+
`MultiSelectDropdown`'s shell. The menu stays open while ticking; **Done**,
|
|
11
|
+
Escape or an outside click closes it. `variant="panel"` keeps the
|
|
12
|
+
always-visible box (web-mojo's shape) for settings-page contexts. The value
|
|
13
|
+
is a **controlled id array** in both directions. Ported from web-mojo
|
|
14
|
+
`CollectionMultiSelect.js`; sibling of `CollectionSelect` in the server-data
|
|
15
|
+
selects family (epic #1275).
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const [ids, setIds] = useState<Array<string | number>>([]);
|
|
19
|
+
|
|
20
|
+
<CollectionMultiSelect<GroupRow>
|
|
21
|
+
model={GroupModel} // or endpoint="/api/group"
|
|
22
|
+
value={ids}
|
|
23
|
+
onChange={setIds}
|
|
24
|
+
label="Groups"
|
|
25
|
+
required
|
|
26
|
+
/>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Props
|
|
30
|
+
|
|
31
|
+
| Prop | Behavior |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `model` / `endpoint` | One is required. `model` is a `defineModel` def (supplies endpoint + row type); `endpoint` is the bare path. |
|
|
34
|
+
| `value` / `onChange` | Controlled id array. Every toggle commits — `onChange` fires with the full next array. Ids keep their caller-side types; all comparisons are normalized (`String()`), so string vs number ids work by construction. |
|
|
35
|
+
| `variant` | `'dropdown'` (default): summary trigger + Popover menu — the form-embedded presentation. `'panel'`: the always-visible box. Internals (search, counts, ranges, states) are identical. |
|
|
36
|
+
| `placeholder` | Trigger text while nothing is selected (dropdown). Default `'Select...'`. |
|
|
37
|
+
| `showSelectedLabels` / `maxLabelsToShow` | Trigger summary: up to `maxLabelsToShow` (default 3) comma-joined labels **when every pick's label is known** (labels accumulate from rows the control has seen — across searches), else `"N selected"`. Hydrated initial ids the control has never seen summarize as `"N selected"` — no per-id fetches, no warn. |
|
|
38
|
+
| `placement` | Menu placement against the trigger. Default `'bottom-start'`. |
|
|
39
|
+
| `labelField` / `valueField` | Row fields for label and id, **dot notation** reaches nested fields (`'user.email'`). Defaults `'name'` / `'id'`. A missing field warns once and falls back (label → the id; missing id → row dropped) — never renders nothing silently. |
|
|
40
|
+
| `size` | VISIBLE rows before the list scrolls (max-height = `size × 42px`). Default 8. **Not the fetch size** — the wire page size defaults to 50; override it via `defaultParams: { size: 100 }`. |
|
|
41
|
+
| `maxHeight` | Explicit px override for the list height. |
|
|
42
|
+
| `enableSearch` | Search input above the list. **Defaults ON in the dropdown variant, OFF in the panel.** 400ms debounce (source parity — deliberately not ModelTable's 300) into the `search` wire param; trimmed; empty term drops the param. |
|
|
43
|
+
| `defaultParams` | Extra wire params — a dict, or a **callback re-evaluated per render** (each fetch uses the freshest result; a changed result is a new query key → refetch). Merge order: `{size: 50} → defaultParams → group → search` (later wins). |
|
|
44
|
+
| `requiresActiveGroup` | Folds the active group id in as `group`. With NO active group the fetch is **held** (empty state shows) — never an unscoped list where a scoped one was demanded. Needs a `<GroupProvider>` above; missing one warns. |
|
|
45
|
+
| `ignoreIds` | Ids hidden client-side — the "already added" pattern. Compared normalized. This is the documented exception to the no-client-filtering rule; it exists so a picker can subtract rows already chosen elsewhere without a server round-trip. |
|
|
46
|
+
| `renderItem` | `(row) => ReactNode` — replaces the label span; the checkbox stays component-owned. This replaces web-mojo's mustache `itemTemplate` (trusted-HTML slots end here). |
|
|
47
|
+
| `isRowDisabled` | `(row) => boolean` — per-row disabled: unclickable, skipped by SELECT and by shift-click ranges. |
|
|
48
|
+
| `showSelectAll` | The SELECT/DESELECT header row. Default true. |
|
|
49
|
+
| `label` / `required` / `help` / `error` | Field wrapper (house `.field-label` / `.field-help` / `.field-error`; `error` replaces `help`). |
|
|
50
|
+
| `disabled` | Whole control: trigger, search, buttons, every row. |
|
|
51
|
+
| `id` | id for the trigger button (a `<label for>` can point at it). |
|
|
52
|
+
|
|
53
|
+
## Selection semantics (the contract)
|
|
54
|
+
|
|
55
|
+
- **Selection is an id set, never row references** — it survives searches,
|
|
56
|
+
refetches and paging of the visible list. Picking under one search term
|
|
57
|
+
and then searching another keeps the first picks in `value`.
|
|
58
|
+
- **SELECT (n)**: n = visible rows that are enabled and not yet selected.
|
|
59
|
+
Clicking it ADDS those ids to the value (**union** — deliberate fix over
|
|
60
|
+
the source, which replaced the value and dropped off-screen selections).
|
|
61
|
+
Disabled rows are skipped. Disabled at n = 0.
|
|
62
|
+
- **DESELECT (n)**: n = `value.length` — the WHOLE selection, including ids
|
|
63
|
+
not on screen under the current search. Clicking it clears the entire
|
|
64
|
+
value (source parity: it is the "empty the value" affordance). Disabled
|
|
65
|
+
at 0.
|
|
66
|
+
- **Shift-click range**: anchor = the last clicked row's index; the clicked
|
|
67
|
+
row's NEW state applies across the span; disabled rows skipped; the
|
|
68
|
+
anchor moves to the clicked row afterwards. The anchor resets whenever
|
|
69
|
+
the row set changes (new data / `ignoreIds` change) — stale indices into
|
|
70
|
+
a replaced list are meaningless (the source kept them and could walk off
|
|
71
|
+
the array).
|
|
72
|
+
- Counts render only when > 0 (`SELECT`, not `SELECT (0)`) — source parity.
|
|
73
|
+
|
|
74
|
+
## Wire contract
|
|
75
|
+
|
|
76
|
+
One GET to the list endpoint through `useModelList`, so the cache key is the
|
|
77
|
+
shared `[endpoint, params]` — this picker, `ModelTable` and model hooks read
|
|
78
|
+
one cache and one invalidation root (`[endpoint]`). There is no parallel
|
|
79
|
+
fetch path; `model.invalidate(qc)` refreshes open pickers too.
|
|
80
|
+
|
|
81
|
+
Params sent: `size: 50` (default page size) merged with `defaultParams`,
|
|
82
|
+
plus `group` (when `requiresActiveGroup`) and `search` (when searching).
|
|
83
|
+
Sorting is the server's default unless `defaultParams` carries `sort`.
|
|
84
|
+
|
|
85
|
+
## Invariants
|
|
86
|
+
|
|
87
|
+
1. Controlled value, one pipeline: the checkboxes can never display a state
|
|
88
|
+
`value` doesn't hold (the visual checkbox is `pointer-events: none`; all
|
|
89
|
+
interaction — including `shiftKey` — lands on the row element).
|
|
90
|
+
2. Every id comparison goes through one `String()` normalization. Never
|
|
91
|
+
compare raw ids anywhere in or around this component.
|
|
92
|
+
3. Rows the server returns are shown as-is except the `ignoreIds` subtraction
|
|
93
|
+
— every other narrowing (kind, group, search…) must be a wire param.
|
|
94
|
+
4. Loading shows skeleton rows (house silhouette) whenever the current key's
|
|
95
|
+
rows aren't on screen yet — cold load AND search/param changes. Empty is
|
|
96
|
+
`"No items available"`; fetch errors show the server's message + Retry.
|
|
97
|
+
|
|
98
|
+
## Pitfalls
|
|
99
|
+
|
|
100
|
+
- `size` is visible rows, not the fetch size. A list of 300 candidates with
|
|
101
|
+
the default wire size shows only the first 50 — raise it via
|
|
102
|
+
`defaultParams: { size: 300 }` (or make the user search; search is server-side).
|
|
103
|
+
- The legacy `excludeIds` option is **not ported**: despite its "server-side
|
|
104
|
+
filtering" comment it was a second client-side filter identical to
|
|
105
|
+
`ignoreIds` (web-mojo filtered both in `buildItems`). Use `ignoreIds`.
|
|
106
|
+
- DESELECT clears ids that are hidden by `ignoreIds` or the current search
|
|
107
|
+
too — it empties the value, not the visible page.
|
|
108
|
+
- An inline `defaultParams` object/callback re-evaluates per render; that is
|
|
109
|
+
fine (TanStack hashes keys structurally — equal content, same key). Only a
|
|
110
|
+
changed RESULT refetches.
|
|
111
|
+
- `requiresActiveGroup` holds the fetch until a group is active; pair the
|
|
112
|
+
control with `RequiresGroup` or group-scoped screens so users aren't shown
|
|
113
|
+
a permanently empty picker in global context.
|
|
114
|
+
- The dropdown's trigger/footer classes (`.multiselect-trigger`,
|
|
115
|
+
`.multiselect-footer`, `.multiselect-done`) live in
|
|
116
|
+
`multiselect-dropdown.css` — consuming apps must import it alongside
|
|
117
|
+
`collection-multiselect.css` (the reference `theme.css` imports both).
|