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,459 @@
|
|
|
1
|
+
// MetricsChart — the django-mojo metrics surface: fetches /api/metrics/fetch
|
|
2
|
+
// and owns the control bar. Ported from web-mojo's MetricsChart:
|
|
3
|
+
// · inline granularity toggle MIN HR DAY WK MO (collapses to a <select>)
|
|
4
|
+
// · quick ranges 1H 24H 7D 30D + Custom range… dialog
|
|
5
|
+
// · chart type switch line / bar / area (bars stack by default)
|
|
6
|
+
// · stats summary + view-data dialogs (native <dialog>, CSV download)
|
|
7
|
+
// · refresh + loading/error overlays with retry
|
|
8
|
+
//
|
|
9
|
+
// Wire note (C2): /api/metrics/fetch takes `dt_start`/`dt_end` (epoch
|
|
10
|
+
// SECONDS — objict.parse_date reads digit strings as epochs). web-mojo sent
|
|
11
|
+
// `dr_start`/`dr_end` here, which the metrics endpoint silently IGNORES
|
|
12
|
+
// (`dr_*` is the model-LIST daterange triple) — its custom ranges never
|
|
13
|
+
// reached the backend. This port sends dt_*. The `range` param rides along
|
|
14
|
+
// for quick ranges because the shipped mock windows off it; each side
|
|
15
|
+
// ignores the other's param.
|
|
16
|
+
import { useEffect, useRef, useState } from 'react';
|
|
17
|
+
import { useQuery } from '@tanstack/react-query';
|
|
18
|
+
import { mojoMetrics, type MetricsResponse } from '../client/client';
|
|
19
|
+
import type { Params } from '../client/types';
|
|
20
|
+
import { DateRangePicker } from '../ui/date/DateRangePicker';
|
|
21
|
+
import { modal } from '../ui/modal';
|
|
22
|
+
import { showSeriesData, showSeriesStats } from './chart-dialogs';
|
|
23
|
+
import { SeriesChart, type ChartType } from './SeriesChart';
|
|
24
|
+
import { granularitiesForSpanMs, quickRangeWindow, ymdRangeToEpochSeconds } from './stats';
|
|
25
|
+
|
|
26
|
+
export interface Granularity { value: string; label: string; short: string }
|
|
27
|
+
|
|
28
|
+
export interface MetricsFetchParams extends Params {
|
|
29
|
+
slugs: string;
|
|
30
|
+
account: string;
|
|
31
|
+
granularity: string;
|
|
32
|
+
dt_start: number;
|
|
33
|
+
dt_end: number;
|
|
34
|
+
child_kind?: string;
|
|
35
|
+
breakdown?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type MetricsSeriesLoader = (params: MetricsFetchParams) => Promise<MetricsResponse>;
|
|
39
|
+
|
|
40
|
+
export const GRANULARITIES: Granularity[] = [
|
|
41
|
+
{ value: 'minutes', label: 'Minutes', short: 'MIN' },
|
|
42
|
+
{ value: 'hours', label: 'Hours', short: 'HR' },
|
|
43
|
+
{ value: 'days', label: 'Days', short: 'DAY' },
|
|
44
|
+
{ value: 'weeks', label: 'Weeks', short: 'WK' },
|
|
45
|
+
{ value: 'months', label: 'Months', short: 'MO' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export const QUICK_RANGES = [
|
|
49
|
+
{ value: '1h', label: '1H' },
|
|
50
|
+
{ value: '24h', label: '24H' },
|
|
51
|
+
{ value: '7d', label: '7D' },
|
|
52
|
+
{ value: '30d', label: '30D' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** Granularities that make sense for a range — mirrors the backend's buckets. */
|
|
56
|
+
const RANGE_GRANULARITY: Record<string, string[]> = {
|
|
57
|
+
'1h': ['minutes'],
|
|
58
|
+
'24h': ['minutes', 'hours'],
|
|
59
|
+
'7d': ['hours', 'days'],
|
|
60
|
+
'30d': ['days', 'weeks', 'months'],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const TYPES: { value: ChartType; icon: string; title: string }[] = [
|
|
64
|
+
{ value: 'line', icon: 'bi-graph-up', title: 'Line' },
|
|
65
|
+
{ value: 'bar', icon: 'bi-bar-chart-fill', title: 'Bar (stacked)' },
|
|
66
|
+
{ value: 'area', icon: 'bi-graph-up-arrow', title: 'Area' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** 'api_calls' → 'Api Calls' — the default legend name for an unmapped slug. */
|
|
70
|
+
function humanizeSlug(slug: string): string {
|
|
71
|
+
return slug.split(/[_-]/).map((w) => (w ? w[0]!.toUpperCase() + w.slice(1) : w)).join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function metricTail(slug: string): string {
|
|
75
|
+
return slug.split(':').at(-1) ?? slug;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Live `/fetch` labels datasets with only the final colon segment. Repair
|
|
80
|
+
* that identity for default chart callers when the configured slug set makes
|
|
81
|
+
* the mapping unique. A duplicate tail remains an error: only an exact loader
|
|
82
|
+
* can safely split and reassemble those requests.
|
|
83
|
+
*/
|
|
84
|
+
export function remapMetricSeriesIdentity(response: MetricsResponse, requestedSlugs: readonly string[]): MetricsResponse {
|
|
85
|
+
const requested = [...new Set(requestedSlugs)];
|
|
86
|
+
const requestedSet = new Set(requested);
|
|
87
|
+
const byTail = new Map<string, string[]>();
|
|
88
|
+
for (const slug of requested) {
|
|
89
|
+
const tail = metricTail(slug);
|
|
90
|
+
byTail.set(tail, [...(byTail.get(tail) ?? []), slug]);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...response,
|
|
94
|
+
datasets: response.datasets.map((dataset) => {
|
|
95
|
+
if (requestedSet.has(dataset.label)) return dataset;
|
|
96
|
+
const candidates = byTail.get(dataset.label) ?? [];
|
|
97
|
+
if (candidates.length > 1) {
|
|
98
|
+
throw new Error(`MetricsChart: response key "${dataset.label}" is ambiguous for ${candidates.join(', ')}; use an exact series loader.`);
|
|
99
|
+
}
|
|
100
|
+
return candidates.length === 1 ? { ...dataset, label: candidates[0]! } : dataset;
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The active window: a quick range anchored at pick time, or custom days. */
|
|
106
|
+
type RangeState =
|
|
107
|
+
| { kind: 'quick'; value: string; anchor: number }
|
|
108
|
+
| { kind: 'custom'; start: string; end: string };
|
|
109
|
+
|
|
110
|
+
// ── Custom-range dialog (DateRangePicker inline + explicit Apply) ─────
|
|
111
|
+
function RangeDialog({ initial, onDone }: {
|
|
112
|
+
initial: { start: string; end: string };
|
|
113
|
+
onDone: (range: { start: string; end: string } | null) => void;
|
|
114
|
+
}) {
|
|
115
|
+
const [range, setRange] = useState(initial);
|
|
116
|
+
const complete = !!range.start && !!range.end;
|
|
117
|
+
return (
|
|
118
|
+
<div className="modal-pad chart-range-dialog">
|
|
119
|
+
<h2 className="modal-title">Custom date range</h2>
|
|
120
|
+
<DateRangePicker
|
|
121
|
+
inline
|
|
122
|
+
presets="default"
|
|
123
|
+
start={range.start}
|
|
124
|
+
end={range.end}
|
|
125
|
+
onChange={(e) => setRange({ start: e.start, end: e.end })}
|
|
126
|
+
/>
|
|
127
|
+
<div className="modal-actions">
|
|
128
|
+
<button className="btn" onClick={() => onDone(null)}>Cancel</button>
|
|
129
|
+
<button className="btn btn-primary" disabled={!complete} onClick={() => onDone(range)}>
|
|
130
|
+
Apply
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface MetricsChartProps {
|
|
138
|
+
title: string;
|
|
139
|
+
slugs: string[];
|
|
140
|
+
/** Display names per slug — the wire only carries raw slugs. */
|
|
141
|
+
seriesLabels?: Record<string, string>;
|
|
142
|
+
/** Keep loader-provided identities verbatim (full slugs or fan-out child labels). */
|
|
143
|
+
preserveSeriesLabels?: boolean;
|
|
144
|
+
/** 'public' | 'global' | 'group-<id>' | 'user-<id>'. */
|
|
145
|
+
account?: string;
|
|
146
|
+
defaultRange?: string;
|
|
147
|
+
defaultGranularity?: string;
|
|
148
|
+
defaultType?: ChartType;
|
|
149
|
+
height?: number;
|
|
150
|
+
/** Rendered in tooltips, Y ticks, and the stats/data dialogs. */
|
|
151
|
+
valueFormatter?: (v: number) => string;
|
|
152
|
+
/**
|
|
153
|
+
* Forward-compatible passthrough for /api/metrics/fetch params the
|
|
154
|
+
* component doesn't promote (for example category). Fan-out uses the
|
|
155
|
+
* typed childKind/breakdown props below.
|
|
156
|
+
* Spread FIRST — the built-ins overwrite any overlap. Developer-
|
|
157
|
+
* controlled values only; they land on the URL.
|
|
158
|
+
*/
|
|
159
|
+
apiParams?: Params;
|
|
160
|
+
/** Typed history boundary. Existing callers retain the lossy shared loader. */
|
|
161
|
+
loadSeries?: MetricsSeriesLoader;
|
|
162
|
+
/** Stable cache namespace; custom loaders must not share default results. */
|
|
163
|
+
seriesCacheKey?: string;
|
|
164
|
+
/** Group fan-out child kind. Built in here so it overwrites apiParams. */
|
|
165
|
+
childKind?: string;
|
|
166
|
+
/** Fan-out breakdown. Built in here so it overwrites apiParams. */
|
|
167
|
+
breakdown?: boolean;
|
|
168
|
+
showGranularity?: boolean;
|
|
169
|
+
/** Gates the quick ranges AND the custom-range dialog. */
|
|
170
|
+
showDateRange?: boolean;
|
|
171
|
+
showTypeSwitch?: boolean;
|
|
172
|
+
showRefresh?: boolean;
|
|
173
|
+
showStats?: boolean;
|
|
174
|
+
showDataTable?: boolean;
|
|
175
|
+
/** Optional backend capability subset; unsupported controls are omitted. */
|
|
176
|
+
allowedGranularities?: string[];
|
|
177
|
+
/** Changing this value requests one refresh from an owning parent. */
|
|
178
|
+
refreshSignal?: unknown;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function constrainGranularities(values: readonly string[], configured?: readonly string[]): string[] {
|
|
182
|
+
if (!configured) return [...values];
|
|
183
|
+
const supported = new Set(GRANULARITIES.map((item) => item.value));
|
|
184
|
+
return values.filter((value) => configured.includes(value) && supported.has(value));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function MetricsChart({
|
|
188
|
+
title,
|
|
189
|
+
slugs,
|
|
190
|
+
seriesLabels = {},
|
|
191
|
+
preserveSeriesLabels = false,
|
|
192
|
+
account = 'global',
|
|
193
|
+
defaultRange = '24h',
|
|
194
|
+
defaultGranularity = 'hours',
|
|
195
|
+
defaultType = 'line',
|
|
196
|
+
height = 280,
|
|
197
|
+
valueFormatter,
|
|
198
|
+
apiParams,
|
|
199
|
+
loadSeries = mojoMetrics,
|
|
200
|
+
seriesCacheKey = 'metrics/fetch',
|
|
201
|
+
childKind,
|
|
202
|
+
breakdown,
|
|
203
|
+
showGranularity = true,
|
|
204
|
+
showDateRange = true,
|
|
205
|
+
showTypeSwitch = true,
|
|
206
|
+
showRefresh = true,
|
|
207
|
+
showStats = true,
|
|
208
|
+
showDataTable = true,
|
|
209
|
+
allowedGranularities,
|
|
210
|
+
refreshSignal,
|
|
211
|
+
}: MetricsChartProps) {
|
|
212
|
+
const [range, setRange] = useState<RangeState>(() => ({ kind: 'quick', value: defaultRange, anchor: Date.now() }));
|
|
213
|
+
const [granularity, setGranularity] = useState(defaultGranularity);
|
|
214
|
+
const [chartType, setChartType] = useState<ChartType>(defaultType);
|
|
215
|
+
|
|
216
|
+
// ── Window → wire params + granularity gating ─────────────────────
|
|
217
|
+
let allowed: string[];
|
|
218
|
+
let dtStart: number;
|
|
219
|
+
let dtEnd: number;
|
|
220
|
+
if (range.kind === 'quick') {
|
|
221
|
+
allowed = RANGE_GRANULARITY[range.value] ?? ['days'];
|
|
222
|
+
const w = quickRangeWindow(range.value, range.anchor);
|
|
223
|
+
dtStart = Math.floor(w.startMs / 1000);
|
|
224
|
+
dtEnd = Math.floor(w.endMs / 1000);
|
|
225
|
+
} else {
|
|
226
|
+
const epochs = ymdRangeToEpochSeconds(range.start, range.end);
|
|
227
|
+
// The dialog can only apply a complete pair; a bad pair degrades to
|
|
228
|
+
// the default quick window rather than crashing the fetch.
|
|
229
|
+
if (!epochs) {
|
|
230
|
+
console.warn('MetricsChart: invalid custom range — falling back to 24h', range);
|
|
231
|
+
const w = quickRangeWindow('24h');
|
|
232
|
+
dtStart = Math.floor(w.startMs / 1000);
|
|
233
|
+
dtEnd = Math.floor(w.endMs / 1000);
|
|
234
|
+
allowed = RANGE_GRANULARITY['24h']!;
|
|
235
|
+
} else {
|
|
236
|
+
dtStart = epochs.dtStart;
|
|
237
|
+
dtEnd = epochs.dtEnd;
|
|
238
|
+
allowed = granularitiesForSpanMs((dtEnd - dtStart) * 1000);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const configured = constrainGranularities(GRANULARITIES.map((item) => item.value), allowedGranularities);
|
|
242
|
+
const constrained = constrainGranularities(allowed, allowedGranularities);
|
|
243
|
+
const available = constrained.length > 0 ? constrained : configured.length > 0 ? configured : constrainGranularities(allowed);
|
|
244
|
+
const effective = available.includes(granularity) ? granularity : available[available.length - 1]!;
|
|
245
|
+
|
|
246
|
+
const wire: MetricsFetchParams = {
|
|
247
|
+
...apiParams,
|
|
248
|
+
...(childKind ? { child_kind: childKind } : {}),
|
|
249
|
+
...(breakdown !== undefined ? { breakdown } : {}),
|
|
250
|
+
slugs: slugs.join(','),
|
|
251
|
+
granularity: effective,
|
|
252
|
+
account,
|
|
253
|
+
// `range` windows the shipped mock; dt_start/dt_end (epoch seconds)
|
|
254
|
+
// window the real backend. Each ignores the other's param.
|
|
255
|
+
...(range.kind === 'quick' ? { range: range.value } : {}),
|
|
256
|
+
dt_start: dtStart,
|
|
257
|
+
dt_end: dtEnd,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const query = useQuery({
|
|
261
|
+
queryKey: ['metrics', seriesCacheKey, wire],
|
|
262
|
+
queryFn: () => loadSeries(wire),
|
|
263
|
+
select: (res) => {
|
|
264
|
+
const identitySafe = loadSeries === mojoMetrics
|
|
265
|
+
? remapMetricSeriesIdentity(res, slugs)
|
|
266
|
+
: res;
|
|
267
|
+
return {
|
|
268
|
+
...identitySafe,
|
|
269
|
+
datasets: identitySafe.datasets.map((d) => ({
|
|
270
|
+
...d,
|
|
271
|
+
label: seriesLabels[d.label] ?? (preserveSeriesLabels ? d.label : humanizeSlug(d.label)),
|
|
272
|
+
})),
|
|
273
|
+
};
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Switching range re-points granularity at the nearest sensible bucket
|
|
278
|
+
// rather than firing a request the backend would bucket differently.
|
|
279
|
+
const pickRange = (next: string) => {
|
|
280
|
+
setRange({ kind: 'quick', value: next, anchor: Date.now() });
|
|
281
|
+
const rangeGranularities = RANGE_GRANULARITY[next] ?? ['days'];
|
|
282
|
+
const constrainedNext = constrainGranularities(rangeGranularities, allowedGranularities);
|
|
283
|
+
const ok = constrainedNext.length > 0 ? constrainedNext : configured.length > 0 ? configured : rangeGranularities;
|
|
284
|
+
if (!ok.includes(granularity)) setGranularity(ok[ok.length - 1]!);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const openCustomRange = async () => {
|
|
288
|
+
const initial = range.kind === 'custom' ? { start: range.start, end: range.end } : { start: '', end: '' };
|
|
289
|
+
const picked = await modal.open<{ start: string; end: string } | null>((close) => (
|
|
290
|
+
<RangeDialog initial={initial} onDone={close} />
|
|
291
|
+
), { size: 'md' });
|
|
292
|
+
if (!picked) return;
|
|
293
|
+
setRange({ kind: 'custom', start: picked.start, end: picked.end });
|
|
294
|
+
const epochs = ymdRangeToEpochSeconds(picked.start, picked.end);
|
|
295
|
+
if (epochs) {
|
|
296
|
+
const rangeGranularities = granularitiesForSpanMs((epochs.dtEnd - epochs.dtStart) * 1000);
|
|
297
|
+
const constrainedNext = constrainGranularities(rangeGranularities, allowedGranularities);
|
|
298
|
+
const ok = constrainedNext.length > 0 ? constrainedNext : configured.length > 0 ? configured : rangeGranularities;
|
|
299
|
+
if (!ok.includes(granularity)) setGranularity(ok[ok.length - 1]!);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Refresh re-anchors a quick window at NOW (web-mojo kept the stale
|
|
304
|
+
// window — deliberate fix, documented in charts.md).
|
|
305
|
+
const refresh = () => {
|
|
306
|
+
if (range.kind === 'quick') setRange({ ...range, anchor: Math.max(Date.now(), range.anchor + 1) });
|
|
307
|
+
else void query.refetch();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const priorRefreshSignal = useRef(refreshSignal);
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (Object.is(priorRefreshSignal.current, refreshSignal)) return;
|
|
313
|
+
priorRefreshSignal.current = refreshSignal;
|
|
314
|
+
refresh();
|
|
315
|
+
}, [refreshSignal]);
|
|
316
|
+
|
|
317
|
+
const openStats = () => {
|
|
318
|
+
void showSeriesStats({
|
|
319
|
+
title: `${title} — Stats`,
|
|
320
|
+
labels: query.data?.labels ?? [],
|
|
321
|
+
datasets: query.data?.datasets ?? [],
|
|
322
|
+
granularity: effective,
|
|
323
|
+
formatter: valueFormatter,
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const openData = () => {
|
|
327
|
+
void showSeriesData({
|
|
328
|
+
title,
|
|
329
|
+
labels: query.data?.labels ?? [],
|
|
330
|
+
datasets: query.data?.datasets ?? [],
|
|
331
|
+
granularity: effective,
|
|
332
|
+
formatter: valueFormatter,
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const customActive = range.kind === 'custom';
|
|
337
|
+
const customTitle = customActive ? `Custom range: ${range.start} – ${range.end}` : 'Custom range…';
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<div className="panel panel-pad chart-panel">
|
|
341
|
+
<div className="chart-head">
|
|
342
|
+
<div>
|
|
343
|
+
<div className="eyebrow">Metrics</div>
|
|
344
|
+
<h3 className="panel-subtitle">
|
|
345
|
+
{title}
|
|
346
|
+
{customActive && <span className="chart-range-chip">{range.start} – {range.end}</span>}
|
|
347
|
+
</h3>
|
|
348
|
+
</div>
|
|
349
|
+
<div className="chart-controls">
|
|
350
|
+
{showGranularity && (
|
|
351
|
+
<>
|
|
352
|
+
<div className="seg seg-xs gran-toggle" role="group" aria-label="Granularity">
|
|
353
|
+
{GRANULARITIES.filter((g) => !allowedGranularities || configured.length === 0 || configured.includes(g.value)).map((g) => {
|
|
354
|
+
const enabled = available.includes(g.value);
|
|
355
|
+
return (
|
|
356
|
+
<button
|
|
357
|
+
key={g.value}
|
|
358
|
+
className={`seg-btn${effective === g.value ? ' seg-active' : ''}`}
|
|
359
|
+
disabled={!enabled}
|
|
360
|
+
title={enabled ? g.label : `${g.label} — not available for this range`}
|
|
361
|
+
onClick={() => setGranularity(g.value)}
|
|
362
|
+
>
|
|
363
|
+
{g.short}
|
|
364
|
+
</button>
|
|
365
|
+
);
|
|
366
|
+
})}
|
|
367
|
+
</div>
|
|
368
|
+
<select
|
|
369
|
+
className="input input-compact gran-select"
|
|
370
|
+
value={effective}
|
|
371
|
+
onChange={(e) => setGranularity(e.target.value)}
|
|
372
|
+
aria-label="Granularity"
|
|
373
|
+
>
|
|
374
|
+
{GRANULARITIES.filter((g) => available.includes(g.value)).map((g) => (
|
|
375
|
+
<option key={g.value} value={g.value}>{g.label}</option>
|
|
376
|
+
))}
|
|
377
|
+
</select>
|
|
378
|
+
</>
|
|
379
|
+
)}
|
|
380
|
+
|
|
381
|
+
{showDateRange && (
|
|
382
|
+
<div className="seg seg-xs" role="group" aria-label="Date range">
|
|
383
|
+
{QUICK_RANGES.map((r) => (
|
|
384
|
+
<button
|
|
385
|
+
key={r.value}
|
|
386
|
+
className={`seg-btn${range.kind === 'quick' && range.value === r.value ? ' seg-active' : ''}`}
|
|
387
|
+
onClick={() => pickRange(r.value)}
|
|
388
|
+
>
|
|
389
|
+
{r.label}
|
|
390
|
+
</button>
|
|
391
|
+
))}
|
|
392
|
+
<button
|
|
393
|
+
className={`seg-btn${customActive ? ' seg-active' : ''}`}
|
|
394
|
+
title={customTitle}
|
|
395
|
+
aria-label="Custom date range"
|
|
396
|
+
onClick={() => void openCustomRange()}
|
|
397
|
+
>
|
|
398
|
+
<i className="bi bi-calendar-range" />
|
|
399
|
+
</button>
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
|
|
403
|
+
{showTypeSwitch && (
|
|
404
|
+
<div className="seg seg-xs" role="group" aria-label="Chart type">
|
|
405
|
+
{TYPES.map((t) => (
|
|
406
|
+
<button
|
|
407
|
+
key={t.value}
|
|
408
|
+
className={`seg-btn${chartType === t.value ? ' seg-active' : ''}`}
|
|
409
|
+
title={t.title}
|
|
410
|
+
onClick={() => setChartType(t.value)}
|
|
411
|
+
>
|
|
412
|
+
<i className={`bi ${t.icon}`} />
|
|
413
|
+
</button>
|
|
414
|
+
))}
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
|
|
418
|
+
{showStats && (
|
|
419
|
+
<button className="btn-icon" title="Stats" aria-label="Stats" onClick={openStats}>
|
|
420
|
+
<i className="bi bi-info-circle" />
|
|
421
|
+
</button>
|
|
422
|
+
)}
|
|
423
|
+
{showDataTable && (
|
|
424
|
+
<button className="btn-icon" title="View data" aria-label="View data" onClick={openData}>
|
|
425
|
+
<i className="bi bi-table" />
|
|
426
|
+
</button>
|
|
427
|
+
)}
|
|
428
|
+
{showRefresh && (
|
|
429
|
+
<button className="btn-icon" title="Refresh" aria-label="Refresh" onClick={refresh}>
|
|
430
|
+
<i className={`bi bi-arrow-repeat${query.isFetching ? ' spin' : ''}`} />
|
|
431
|
+
</button>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
|
|
436
|
+
<div className="chart-body" style={{ minHeight: height }}>
|
|
437
|
+
{query.isPending ? (
|
|
438
|
+
<div className="chart-overlay"><span className="skel skel-block" /></div>
|
|
439
|
+
) : query.isError ? (
|
|
440
|
+
<div className="chart-overlay">
|
|
441
|
+
<div className="chart-error">
|
|
442
|
+
<i className="bi bi-exclamation-triangle" />
|
|
443
|
+
<span>{query.error instanceof Error ? query.error.message : 'Failed to load metrics'}</span>
|
|
444
|
+
<button className="btn btn-compact" onClick={() => void query.refetch()}>Retry</button>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
) : (
|
|
448
|
+
<SeriesChart
|
|
449
|
+
labels={query.data!.labels}
|
|
450
|
+
datasets={query.data!.datasets}
|
|
451
|
+
chartType={chartType}
|
|
452
|
+
height={height}
|
|
453
|
+
valueFormatter={valueFormatter}
|
|
454
|
+
/>
|
|
455
|
+
)}
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
}
|