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,28 @@
|
|
|
1
|
+
// portal-mojo/charts — dependency-free SVG charts in the mission-control
|
|
2
|
+
// design language. SeriesChart (line/bar/area, stacked bars, legend toggle,
|
|
3
|
+
// crosshair tooltip) + MetricsChart (the /api/metrics/fetch control-bar
|
|
4
|
+
// wrapper: granularity/range/type switching, custom-range dialog, stats +
|
|
5
|
+
// view-data dialogs); MiniChart sparklines, the searchable MetricsMiniWidget
|
|
6
|
+
// card, KPITile/KPIStrip, CircularProgress, PieChart, the chart stats math,
|
|
7
|
+
// and the exportChartPng SVG→PNG serializer.
|
|
8
|
+
export * from './SeriesChart';
|
|
9
|
+
export * from './MetricsChart';
|
|
10
|
+
export * from './MiniChart';
|
|
11
|
+
export * from './MetricsMiniWidget';
|
|
12
|
+
export * from './KPITile';
|
|
13
|
+
export * from './KPIStrip';
|
|
14
|
+
export * from './CircularProgress';
|
|
15
|
+
export * from './PieChart';
|
|
16
|
+
export * from './pie-math';
|
|
17
|
+
export * from './chart-dialogs';
|
|
18
|
+
export * from './exportChart';
|
|
19
|
+
export * from './stats';
|
|
20
|
+
export * from './worldmap/WorldMap';
|
|
21
|
+
export * from './worldmap/countryCentroids';
|
|
22
|
+
export * from './worldmap/geo';
|
|
23
|
+
export * from './worldmap/worldmap-data';
|
|
24
|
+
// Basemap geometry is reached through `useWorldLand()` ONLY. `world-land.ts` is
|
|
25
|
+
// deliberately not re-exported here: a static re-export would pull ~143KB of
|
|
26
|
+
// coordinates into every bundle that touches this barrel, which is exactly what
|
|
27
|
+
// the lazy hook exists to avoid.
|
|
28
|
+
export * from './worldmap/use-world-land';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Pure pie geometry + parsing — no DOM, no React (headless-testable).
|
|
2
|
+
// Split out of the PieChart port (web-mojo src/extensions/charts/PieChart.js
|
|
3
|
+
// _parseData/_buildSegmentGeometry/_interpolateSegments + the golden-angle
|
|
4
|
+
// generator, verbatim).
|
|
5
|
+
import { toNumber } from './stats';
|
|
6
|
+
|
|
7
|
+
export const GOLDEN_ANGLE = 137.508;
|
|
8
|
+
|
|
9
|
+
/** Distinct hues forever: i × golden angle around the wheel (source verbatim). */
|
|
10
|
+
export function goldenAngleColor(i: number): string {
|
|
11
|
+
return `hsl(${((i * GOLDEN_ANGLE) % 360).toFixed(0)}, 65%, 52%)`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PieDatum {
|
|
15
|
+
label: string;
|
|
16
|
+
value: number;
|
|
17
|
+
color?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Chart.js-style input (shape 2). Only the first dataset is read. */
|
|
21
|
+
export interface PieDatasetsInput {
|
|
22
|
+
labels: string[];
|
|
23
|
+
datasets: { data: number[]; backgroundColor?: string[] }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The three accepted shapes: array / Chart.js / object map. */
|
|
27
|
+
export type PieInput = PieDatum[] | PieDatasetsInput | Record<string, number>;
|
|
28
|
+
|
|
29
|
+
export interface ParsedSlice {
|
|
30
|
+
label: string;
|
|
31
|
+
value: number;
|
|
32
|
+
color: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Normalize any accepted input shape to [{label, value, color|null}].
|
|
37
|
+
* Unknown shapes warn and yield [] (never crash, never render nothing
|
|
38
|
+
* silently — the empty state says so).
|
|
39
|
+
*/
|
|
40
|
+
export function parsePieInput(input: unknown): ParsedSlice[] {
|
|
41
|
+
if (input == null) return [];
|
|
42
|
+
if (Array.isArray(input)) {
|
|
43
|
+
const arr: readonly unknown[] = input;
|
|
44
|
+
return arr.map((d) => {
|
|
45
|
+
const item = d as Partial<PieDatum> | null;
|
|
46
|
+
return {
|
|
47
|
+
label: String(item?.label ?? ''),
|
|
48
|
+
value: toNumber(item?.value),
|
|
49
|
+
color: item?.color ?? null,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (typeof input === 'object') {
|
|
54
|
+
const asDs = input as Partial<PieDatasetsInput>;
|
|
55
|
+
if (Array.isArray(asDs.labels) && Array.isArray(asDs.datasets)) {
|
|
56
|
+
const first = asDs.datasets[0];
|
|
57
|
+
const bg = Array.isArray(first?.backgroundColor) ? first.backgroundColor : null;
|
|
58
|
+
return asDs.labels.map((label, i) => ({
|
|
59
|
+
label: String(label),
|
|
60
|
+
value: toNumber(first?.data?.[i]),
|
|
61
|
+
color: bg?.[i] ?? null,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
const entries = Object.entries(input as Record<string, unknown>);
|
|
65
|
+
if (entries.every(([, v]) => typeof v !== 'object' || v == null)) {
|
|
66
|
+
return entries.map(([label, v]) => ({ label, value: toNumber(v), color: null }));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.warn('PieChart: unrecognized data shape — rendering empty state', input);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PieSegment {
|
|
74
|
+
label: string;
|
|
75
|
+
value: number;
|
|
76
|
+
/** 0–100 share of the total (0 when the total is 0). */
|
|
77
|
+
pct: number;
|
|
78
|
+
color: string;
|
|
79
|
+
startAngle: number;
|
|
80
|
+
endAngle: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Radians, 12 o'clock start, clockwise — the source's geometry, verbatim. */
|
|
84
|
+
export function buildPieGeometry(slices: readonly { label: string; value: number; pct: number; color: string }[]): PieSegment[] {
|
|
85
|
+
let angle = -Math.PI / 2;
|
|
86
|
+
return slices.map((s) => {
|
|
87
|
+
const arc = (s.pct / 100) * Math.PI * 2;
|
|
88
|
+
const seg: PieSegment = { ...s, startAngle: angle, endAngle: angle + arc };
|
|
89
|
+
angle += arc;
|
|
90
|
+
return seg;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Label-keyed interpolation: existing slices slide, new ones grow from a 0-arc. */
|
|
95
|
+
export function interpolateSegments(prev: readonly PieSegment[], target: readonly PieSegment[], k: number): PieSegment[] {
|
|
96
|
+
const lerp = (a: number, b: number) => a + (b - a) * k;
|
|
97
|
+
const prevByLabel = new Map(prev.map((p) => [p.label, p]));
|
|
98
|
+
return target.map((t) => {
|
|
99
|
+
const p = prevByLabel.get(t.label);
|
|
100
|
+
if (!p) return { ...t, endAngle: lerp(t.startAngle, t.endAngle) };
|
|
101
|
+
return { ...t, startAngle: lerp(p.startAngle, t.startAngle), endAngle: lerp(p.endAngle, t.endAngle) };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** SVG path for one segment (doughnut when innerR > 0). */
|
|
106
|
+
export function arcPath(cx: number, cy: number, outerR: number, innerR: number, seg: PieSegment): string {
|
|
107
|
+
const { startAngle, endAngle } = seg;
|
|
108
|
+
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
|
|
109
|
+
const x1o = cx + outerR * Math.cos(startAngle);
|
|
110
|
+
const y1o = cy + outerR * Math.sin(startAngle);
|
|
111
|
+
const x2o = cx + outerR * Math.cos(endAngle);
|
|
112
|
+
const y2o = cy + outerR * Math.sin(endAngle);
|
|
113
|
+
if (innerR > 0) {
|
|
114
|
+
const x1i = cx + innerR * Math.cos(endAngle);
|
|
115
|
+
const y1i = cy + innerR * Math.sin(endAngle);
|
|
116
|
+
const x2i = cx + innerR * Math.cos(startAngle);
|
|
117
|
+
const y2i = cy + innerR * Math.sin(startAngle);
|
|
118
|
+
return [
|
|
119
|
+
`M ${x1o} ${y1o}`,
|
|
120
|
+
`A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2o} ${y2o}`,
|
|
121
|
+
`L ${x1i} ${y1i}`,
|
|
122
|
+
`A ${innerR} ${innerR} 0 ${largeArc} 0 ${x2i} ${y2i}`,
|
|
123
|
+
'Z',
|
|
124
|
+
].join(' ');
|
|
125
|
+
}
|
|
126
|
+
return [`M ${cx} ${cy}`, `L ${x1o} ${y1o}`, `A ${outerR} ${outerR} 0 ${largeArc} 1 ${x2o} ${y2o}`, 'Z'].join(' ');
|
|
127
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Pure chart math — no DOM, no React. Ported from the private helpers of
|
|
2
|
+
// web-mojo's MetricsChart (_computeStats/_formatStatNumber/_downloadCsv/
|
|
3
|
+
// setQuickRange) and MetricsMiniChartWidget (windowed trending), promoted to
|
|
4
|
+
// typed exports so stats dialogs, KPI tiles and the mini widget share ONE
|
|
5
|
+
// implementation (web-mojo carried two diverging copies of the stats math).
|
|
6
|
+
|
|
7
|
+
export interface SeriesLike {
|
|
8
|
+
label: string;
|
|
9
|
+
data: number[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SeriesStat {
|
|
13
|
+
label: string;
|
|
14
|
+
count: number;
|
|
15
|
+
sum: number;
|
|
16
|
+
avg: number;
|
|
17
|
+
median: number;
|
|
18
|
+
min: number;
|
|
19
|
+
max: number;
|
|
20
|
+
latest: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Coerce a wire value to a number the way the source did (parseFloat || 0). */
|
|
24
|
+
export function toNumber(v: unknown): number {
|
|
25
|
+
if (typeof v === 'number') return Number.isFinite(v) ? v : 0;
|
|
26
|
+
if (v == null || v === '') return 0;
|
|
27
|
+
const n = parseFloat(String(v));
|
|
28
|
+
return Number.isNaN(n) ? 0 : n;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-series summary stats. Empty datasets produce no entry (the caller
|
|
33
|
+
* renders its "No data" state); an empty series inside a non-empty set
|
|
34
|
+
* yields a zeroed row so the table stays aligned with the legend.
|
|
35
|
+
*/
|
|
36
|
+
export function computeSeriesStats(datasets: readonly SeriesLike[] | null | undefined): SeriesStat[] {
|
|
37
|
+
// (Array.isArray would widen the readonly array to any[] — a TS quirk.)
|
|
38
|
+
const list: readonly SeriesLike[] = datasets ?? [];
|
|
39
|
+
return list.map((d) => {
|
|
40
|
+
const values = (Array.isArray(d.data) ? d.data : []).map(toNumber);
|
|
41
|
+
if (values.length === 0) {
|
|
42
|
+
return { label: d.label ?? '', count: 0, sum: 0, avg: 0, median: 0, min: 0, max: 0, latest: 0 };
|
|
43
|
+
}
|
|
44
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
45
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
46
|
+
const mid = Math.floor(sorted.length / 2);
|
|
47
|
+
const median = sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!;
|
|
48
|
+
return {
|
|
49
|
+
label: d.label ?? '',
|
|
50
|
+
count: values.length,
|
|
51
|
+
sum,
|
|
52
|
+
avg: sum / values.length,
|
|
53
|
+
median,
|
|
54
|
+
min: sorted[0]!,
|
|
55
|
+
max: sorted[sorted.length - 1]!,
|
|
56
|
+
latest: values[values.length - 1]!,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** '–' for nothing; integers plain; decimals to 2 places (source parity). */
|
|
62
|
+
export function defaultStatFormat(n: number | null | undefined): string {
|
|
63
|
+
if (n == null || Number.isNaN(n)) return '–';
|
|
64
|
+
if (Number.isInteger(n)) return n.toLocaleString();
|
|
65
|
+
return Number(n).toLocaleString(undefined, { maximumFractionDigits: 2 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** "Hourly · 24 points" header noun per granularity (source map, verbatim). */
|
|
69
|
+
export const GRANULARITY_NOUN: Record<string, string> = {
|
|
70
|
+
minutes: 'Per minute',
|
|
71
|
+
hours: 'Hourly',
|
|
72
|
+
days: 'Daily',
|
|
73
|
+
weeks: 'Weekly',
|
|
74
|
+
months: 'Monthly',
|
|
75
|
+
years: 'Yearly',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ── CSV (source _downloadCsv, split so the string builder is testable) ──
|
|
79
|
+
|
|
80
|
+
export function csvEscape(v: unknown): string {
|
|
81
|
+
let s = String(v ?? '');
|
|
82
|
+
// Spreadsheet applications treat these string prefixes as formulas even
|
|
83
|
+
// inside a valid CSV cell. Numeric negatives remain numeric by checking
|
|
84
|
+
// the original value before adding the neutralizing apostrophe.
|
|
85
|
+
if (typeof v === 'string' && /^[=+\-@\t\r]/.test(s)) s = `'${s}`;
|
|
86
|
+
return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildCsv(headers: readonly string[], rows: readonly (readonly unknown[])[]): string {
|
|
90
|
+
const lines = [headers.map(csvEscape).join(',')];
|
|
91
|
+
for (const row of rows) lines.push(row.map(csvEscape).join(','));
|
|
92
|
+
return lines.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 'API Calls — prod' + today → 'api-calls-prod-2026-08-05.csv' (source slugging). */
|
|
96
|
+
export function csvFilename(title: string | null | undefined, now: Date = new Date()): string {
|
|
97
|
+
const slug = String(title ?? '')
|
|
98
|
+
.toLowerCase()
|
|
99
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
100
|
+
.replace(/^-+|-+$/g, '') || 'chart-data';
|
|
101
|
+
return `${slug}-${now.toISOString().slice(0, 10)}.csv`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Trend windows (MetricsMiniChartWidget.updateFromChartData, verbatim) ──
|
|
105
|
+
|
|
106
|
+
export interface TrendOptions {
|
|
107
|
+
/** Window size hint; k = max(1, floor(trendRange / 2)) buckets per window. */
|
|
108
|
+
trendRange?: number | null;
|
|
109
|
+
/** Skip the trailing N buckets before comparing (partial-bucket guard). */
|
|
110
|
+
trendOffset?: number | null;
|
|
111
|
+
/** Compare against the window N buckets earlier instead of the adjacent one. */
|
|
112
|
+
prevTrendOffset?: number | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface Trend {
|
|
116
|
+
/** Sum over the current window. */
|
|
117
|
+
lastSum: number;
|
|
118
|
+
/** Sum over the comparison window. */
|
|
119
|
+
prevSum: number;
|
|
120
|
+
/** Percent change; prev 0 → +100 when rising, 0 when flat (source rule). */
|
|
121
|
+
percent: number;
|
|
122
|
+
up: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Windowed percent trend over a bucket series. Falls back to single-point
|
|
127
|
+
* comparison when the series is too short for two full windows; returns null
|
|
128
|
+
* when there is nothing to compare (0–1 usable points).
|
|
129
|
+
*/
|
|
130
|
+
export function computeTrend(values: readonly number[] | null | undefined, opts: TrendOptions = {}): Trend | null {
|
|
131
|
+
const nums = (values ?? []).map(toNumber);
|
|
132
|
+
if (nums.length === 0) return null;
|
|
133
|
+
|
|
134
|
+
const offset = Math.max(0, Math.trunc(opts.trendOffset ?? 0) || 0);
|
|
135
|
+
const endIndex = Math.max(0, nums.length - 1 - offset);
|
|
136
|
+
const prevOffset = Math.max(0, Math.trunc(opts.prevTrendOffset ?? 0) || 0);
|
|
137
|
+
const k = opts.trendRange != null && opts.trendRange >= 2 ? Math.max(1, Math.floor(opts.trendRange / 2)) : 1;
|
|
138
|
+
|
|
139
|
+
const sumRange = (s: number, e: number): number => {
|
|
140
|
+
let sum = 0;
|
|
141
|
+
for (let i = s; i <= e; i++) sum += nums[i] ?? 0;
|
|
142
|
+
return sum;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let lastSum = 0;
|
|
146
|
+
let prevSum = 0;
|
|
147
|
+
let hasTrend = false;
|
|
148
|
+
|
|
149
|
+
const lastEnd = endIndex;
|
|
150
|
+
const lastStart = lastEnd - (k - 1);
|
|
151
|
+
let prevStart: number;
|
|
152
|
+
let prevEnd: number;
|
|
153
|
+
if (prevOffset > 0) {
|
|
154
|
+
prevStart = lastStart - prevOffset;
|
|
155
|
+
prevEnd = lastEnd - prevOffset;
|
|
156
|
+
} else {
|
|
157
|
+
prevEnd = lastStart - 1;
|
|
158
|
+
prevStart = prevEnd - (k - 1);
|
|
159
|
+
}
|
|
160
|
+
if (lastStart >= 0 && prevStart >= 0) {
|
|
161
|
+
lastSum = sumRange(lastStart, lastEnd);
|
|
162
|
+
prevSum = sumRange(prevStart, prevEnd);
|
|
163
|
+
hasTrend = true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!hasTrend) {
|
|
167
|
+
// Single-point fallback (source parity).
|
|
168
|
+
const prevIndex = endIndex - (prevOffset > 0 ? prevOffset : 1);
|
|
169
|
+
if (prevIndex < 0) return null;
|
|
170
|
+
lastSum = nums[endIndex] ?? 0;
|
|
171
|
+
prevSum = nums[prevIndex] ?? 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const percent = prevSum === 0 ? (lastSum > 0 ? 100 : 0) : ((lastSum - prevSum) / Math.abs(prevSum)) * 100;
|
|
175
|
+
return { lastSum, prevSum, percent, up: percent >= 0 };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Delta badge text + tone (KPITile._renderDelta, verbatim incl. the
|
|
180
|
+
* never-Infinity% rule): a finite deltaPct renders as a percent; otherwise a
|
|
181
|
+
* present absolute delta renders as a signed count; otherwise no badge.
|
|
182
|
+
* `direction` maps through `tone` ('bad' = rising is bad) to a display tone.
|
|
183
|
+
*/
|
|
184
|
+
export interface DeltaBadge {
|
|
185
|
+
text: string;
|
|
186
|
+
/** 'good' | 'bad' | 'flat' | 'neutral' — the CSS class family. */
|
|
187
|
+
tone: 'good' | 'bad' | 'flat' | 'neutral';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function deltaBadge(
|
|
191
|
+
delta: number | null | undefined,
|
|
192
|
+
deltaPct: number | null | undefined,
|
|
193
|
+
tone: 'good' | 'bad' | null | undefined,
|
|
194
|
+
): DeltaBadge | null {
|
|
195
|
+
let sign: '+' | '−' | '±';
|
|
196
|
+
let text: string;
|
|
197
|
+
if (deltaPct != null && Number.isFinite(deltaPct)) {
|
|
198
|
+
const rounded = Math.abs(deltaPct) >= 10 ? Math.round(deltaPct) : Math.round(deltaPct * 10) / 10;
|
|
199
|
+
sign = rounded > 0 ? '+' : rounded < 0 ? '−' : '±';
|
|
200
|
+
text = `${sign}${Math.abs(rounded)}%`;
|
|
201
|
+
} else if (delta != null && Number.isFinite(delta)) {
|
|
202
|
+
sign = delta > 0 ? '+' : delta < 0 ? '−' : '±';
|
|
203
|
+
text = `${sign}${Math.abs(delta)}`;
|
|
204
|
+
} else {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
let badgeTone: DeltaBadge['tone'];
|
|
208
|
+
if (sign === '±') badgeTone = 'flat';
|
|
209
|
+
else if (tone === 'bad') badgeTone = sign === '+' ? 'bad' : 'good';
|
|
210
|
+
else if (tone === 'good') badgeTone = sign === '+' ? 'good' : 'bad';
|
|
211
|
+
else badgeTone = 'neutral';
|
|
212
|
+
return { text, tone: badgeTone };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Date windows (MetricsChart.setQuickRange + custom-range support) ──
|
|
216
|
+
|
|
217
|
+
/** Quick-range span in ms. Unknown values warn at the caller; '24h' shape. */
|
|
218
|
+
export const QUICK_RANGE_MS: Record<string, number> = {
|
|
219
|
+
'1h': 3600e3,
|
|
220
|
+
'24h': 24 * 3600e3,
|
|
221
|
+
'7d': 7 * 864e5,
|
|
222
|
+
'30d': 30 * 864e5,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/** [start, end] ms epochs for a quick range anchored at `now` (source setQuickRange). */
|
|
226
|
+
export function quickRangeWindow(range: string, now: number = Date.now()): { startMs: number; endMs: number } {
|
|
227
|
+
const span = QUICK_RANGE_MS[range] ?? QUICK_RANGE_MS['24h']!;
|
|
228
|
+
return { startMs: now - span, endMs: now };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Which granularities make sense for an arbitrary span — the custom-range
|
|
233
|
+
* twin of MetricsChart's per-quick-range gating (mirrors the backend's
|
|
234
|
+
* bucket sizes; a 6-month range at 'minutes' would be a quarter-million
|
|
235
|
+
* buckets).
|
|
236
|
+
*/
|
|
237
|
+
export function granularitiesForSpanMs(spanMs: number): string[] {
|
|
238
|
+
const h = spanMs / 3600e3;
|
|
239
|
+
if (h <= 2) return ['minutes'];
|
|
240
|
+
if (h <= 48) return ['minutes', 'hours'];
|
|
241
|
+
if (h <= 8 * 24) return ['hours', 'days'];
|
|
242
|
+
if (h <= 92 * 24) return ['days', 'weeks', 'months'];
|
|
243
|
+
return ['days', 'weeks', 'months'];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Canonical YYYY-MM-DD pair (DateRangePicker output) → inclusive epoch-SECOND
|
|
248
|
+
* bounds in the viewer's local time: start 00:00:00 through end 23:59:59.
|
|
249
|
+
* These are the `dt_start`/`dt_end` values /api/metrics/fetch takes (epoch
|
|
250
|
+
* seconds — the django-mojo datetime wire unit). Invalid input → null.
|
|
251
|
+
*/
|
|
252
|
+
export function ymdRangeToEpochSeconds(startYmd: string, endYmd: string): { dtStart: number; dtEnd: number } | null {
|
|
253
|
+
const parse = (s: string): [number, number, number] | null => {
|
|
254
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
|
|
255
|
+
if (!m) return null;
|
|
256
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
257
|
+
};
|
|
258
|
+
const a = parse(startYmd);
|
|
259
|
+
const b = parse(endYmd);
|
|
260
|
+
if (!a || !b) return null;
|
|
261
|
+
const start = new Date(a[0], a[1] - 1, a[2], 0, 0, 0, 0).getTime();
|
|
262
|
+
const end = new Date(b[0], b[1] - 1, b[2] + 1, 0, 0, 0, 0).getTime() - 1000; // 23:59:59
|
|
263
|
+
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
|
264
|
+
return { dtStart: Math.floor(start / 1000), dtEnd: Math.floor(end / 1000) };
|
|
265
|
+
}
|