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,383 @@
|
|
|
1
|
+
// portal-mojo/admin — the base admin portal as self-registering section
|
|
2
|
+
// bundles (PLAN.md "Admin distribution: dual-mount", decided 2026-08-04).
|
|
3
|
+
// The same sections mount two ways:
|
|
4
|
+
// 1. standalone — apps/portal mounts ALL sections at the root and IS the
|
|
5
|
+
// dedicated back office for any django-mojo deployment;
|
|
6
|
+
// 2. embedded — a product's own portal registers them under a "System"
|
|
7
|
+
// area (#/system/…) beside its product pages, gated by `view_admin`.
|
|
8
|
+
// Constraints built in from day one:
|
|
9
|
+
// · routes are MOUNT-POINT RELATIVE — a section never knows where it lives;
|
|
10
|
+
// · sections CONTRIBUTE sidebar entries; the sidebar engine (Chunk A4
|
|
11
|
+
// registry) owns the menu — a section never renders the sidebar itself;
|
|
12
|
+
// · visibility = permissions (now) ∧ backend capabilities (Phase 2
|
|
13
|
+
// capabilities endpoint), evaluated by the mounting shell.
|
|
14
|
+
// Sections land here as Phase 2 admin pages stabilize; deployed portals then
|
|
15
|
+
// pick up admin updates via `npm update portal-mojo` — clone the shell,
|
|
16
|
+
// never the admin.
|
|
17
|
+
import { createElement, useContext, type ComponentType, type ReactNode } from 'react';
|
|
18
|
+
import { Navigate, type RouteObject } from 'react-router-dom';
|
|
19
|
+
import { GroupContext, hasPermission, useMe, type PermSpec } from '../client';
|
|
20
|
+
import { Guarded } from '../ui/Guarded';
|
|
21
|
+
import type { MenuConfig, MenuItem } from '../ui/menu-registry';
|
|
22
|
+
import {
|
|
23
|
+
GLOBAL_CREDENTIAL_PERMS,
|
|
24
|
+
GroupApiKeysPage,
|
|
25
|
+
WebhookSubscriptionsPage,
|
|
26
|
+
} from './credentials';
|
|
27
|
+
import { MONITORING_ADMIN_SECTION } from './monitoring';
|
|
28
|
+
import { CLOUDWATCH_ADMIN_SECTION } from './cloudwatch';
|
|
29
|
+
import { SETTINGS_ADMIN_SECTION } from './settings';
|
|
30
|
+
import { BOUNCER_ADMIN_SECTION } from './bouncer';
|
|
31
|
+
import {
|
|
32
|
+
DEVICE_INTEL_ADMIN_SECTION,
|
|
33
|
+
GEOIP_ADMIN_SECTION,
|
|
34
|
+
SECURITY_OPERATIONS_ADMIN_SECTION,
|
|
35
|
+
} from './security';
|
|
36
|
+
import { MEMBERS_ADMIN_SECTION, USERS_ADMIN_SECTION } from './identity';
|
|
37
|
+
import { JOBS_ADMIN_SECTION } from './jobs';
|
|
38
|
+
import { NETWORK_SECURITY_ADMIN_SECTION } from './network';
|
|
39
|
+
import { DNS_ADMIN_SECTION } from './dns';
|
|
40
|
+
import { STORAGE_ADMIN_SECTION } from './storage';
|
|
41
|
+
import { SHORTLINKS_ADMIN_SECTION } from './shortlinks';
|
|
42
|
+
import { EMAIL_ADMIN_SECTION, PUBLIC_MESSAGES_ADMIN_SECTION, PUSH_ADMIN_SECTION } from './messaging';
|
|
43
|
+
import { PHONE_HUB_ADMIN_SECTION } from './phonehub';
|
|
44
|
+
import { ASSISTANT_ADMIN_SECTION } from './assistant';
|
|
45
|
+
import { ADMIN_DASHBOARD_PERMISSIONS, AdminDashboardPage } from './dashboard';
|
|
46
|
+
|
|
47
|
+
export * from './credentials';
|
|
48
|
+
export * from './monitoring';
|
|
49
|
+
export * from './cloudwatch';
|
|
50
|
+
export * from './settings';
|
|
51
|
+
export * from './bouncer';
|
|
52
|
+
export * from './security-permissions';
|
|
53
|
+
export * from './security';
|
|
54
|
+
export * from './incidents';
|
|
55
|
+
export * from './rules';
|
|
56
|
+
export * from './identity';
|
|
57
|
+
export * from './jobs';
|
|
58
|
+
export * from './network';
|
|
59
|
+
export * from './dns';
|
|
60
|
+
export * from './storage';
|
|
61
|
+
export * from './shortlinks';
|
|
62
|
+
export * from './messaging';
|
|
63
|
+
export * from './phonehub';
|
|
64
|
+
export * from './dashboard';
|
|
65
|
+
export * from './assistant';
|
|
66
|
+
|
|
67
|
+
export type AdminNavigationGroup =
|
|
68
|
+
| 'overview'
|
|
69
|
+
| 'identity-access'
|
|
70
|
+
| 'security'
|
|
71
|
+
| 'observability'
|
|
72
|
+
| 'operations'
|
|
73
|
+
| 'infrastructure'
|
|
74
|
+
| 'communications'
|
|
75
|
+
| 'assistant'
|
|
76
|
+
| 'other';
|
|
77
|
+
|
|
78
|
+
export const ADMIN_NAVIGATION_GROUPS: Readonly<Record<AdminNavigationGroup, {
|
|
79
|
+
id: string;
|
|
80
|
+
label: string;
|
|
81
|
+
icon: string;
|
|
82
|
+
order: number;
|
|
83
|
+
}>> = {
|
|
84
|
+
overview: { id: 'admin:overview', label: 'Overview', icon: 'bi-grid-1x2', order: 0 },
|
|
85
|
+
'identity-access': { id: 'admin:identity-access', label: 'Identity & Access', icon: 'bi-people', order: 10 },
|
|
86
|
+
security: { id: 'admin:security', label: 'Security', icon: 'bi-shield-check', order: 20 },
|
|
87
|
+
observability: { id: 'admin:observability', label: 'Observability', icon: 'bi-activity', order: 30 },
|
|
88
|
+
operations: { id: 'admin:operations', label: 'Operations', icon: 'bi-gear', order: 40 },
|
|
89
|
+
infrastructure: { id: 'admin:infrastructure', label: 'Infrastructure', icon: 'bi-cloud', order: 50 },
|
|
90
|
+
communications: { id: 'admin:communications', label: 'Communications', icon: 'bi-chat-dots', order: 60 },
|
|
91
|
+
assistant: { id: 'admin:assistant', label: 'Assistant', icon: 'bi-stars', order: 70 },
|
|
92
|
+
other: { id: 'admin:other', label: 'Other', icon: 'bi-grid', order: 80 },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** One mountable admin area: pages + routes + sidebar contribution + gates. */
|
|
96
|
+
export interface AdminSection {
|
|
97
|
+
/** Stable id; the default route segment (`'users'` → `#/users` standalone, `#/system/users` embedded). */
|
|
98
|
+
id: string;
|
|
99
|
+
/**
|
|
100
|
+
* Route prefix relative to the admin mount. Defaults to `id`. Use `''`
|
|
101
|
+
* when sibling routes should mount directly at the admin root.
|
|
102
|
+
*/
|
|
103
|
+
basePath?: string;
|
|
104
|
+
/** Sidebar entry label. */
|
|
105
|
+
title: string;
|
|
106
|
+
/** bootstrap-icons class for the sidebar entry (e.g. `'bi-people'`). */
|
|
107
|
+
icon: string;
|
|
108
|
+
/** Permissions that reveal the section (client errs permissive; the server stays authoritative). */
|
|
109
|
+
permissions: string[];
|
|
110
|
+
/** Backend capability key that must be enabled for the section to mount (Phase 2 handshake). */
|
|
111
|
+
capability?: string;
|
|
112
|
+
/** Operator-domain category. Omitted external sections fall under Other. */
|
|
113
|
+
navigationGroup?: AdminNavigationGroup;
|
|
114
|
+
/** Mount-point-relative routes (no leading slash). */
|
|
115
|
+
routes: AdminRoute[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AdminRoute {
|
|
119
|
+
/** Path relative to the section mount (`''` for the index, `':id'` for details). */
|
|
120
|
+
path: string;
|
|
121
|
+
component: ComponentType;
|
|
122
|
+
/** Additional route gate (ANY-of). It is ANDed with section permissions. */
|
|
123
|
+
permissions?: PermSpec;
|
|
124
|
+
/** Present → the route appears as a sidebar item under the section. */
|
|
125
|
+
label?: string;
|
|
126
|
+
/** A denied root landing redirects to the first visible admin route. */
|
|
127
|
+
fallbackToFirstVisible?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Registry of every shipped admin section. */
|
|
131
|
+
export const ADMIN_SECTIONS: readonly AdminSection[] = [
|
|
132
|
+
{
|
|
133
|
+
id: 'dashboard',
|
|
134
|
+
basePath: '',
|
|
135
|
+
title: 'Overview',
|
|
136
|
+
icon: 'bi-grid-1x2',
|
|
137
|
+
navigationGroup: 'overview',
|
|
138
|
+
permissions: ADMIN_DASHBOARD_PERMISSIONS,
|
|
139
|
+
routes: [{ path: '', label: 'Dashboard', component: AdminDashboardPage, permissions: ADMIN_DASHBOARD_PERMISSIONS, fallbackToFirstVisible: true }],
|
|
140
|
+
},
|
|
141
|
+
USERS_ADMIN_SECTION,
|
|
142
|
+
MEMBERS_ADMIN_SECTION,
|
|
143
|
+
{
|
|
144
|
+
id: 'credentials',
|
|
145
|
+
basePath: '',
|
|
146
|
+
title: 'Credentials',
|
|
147
|
+
icon: 'bi-key',
|
|
148
|
+
navigationGroup: 'identity-access',
|
|
149
|
+
permissions: GLOBAL_CREDENTIAL_PERMS,
|
|
150
|
+
routes: [
|
|
151
|
+
{
|
|
152
|
+
path: 'api-keys', label: 'Group API Keys',
|
|
153
|
+
component: GroupApiKeysPage, permissions: GLOBAL_CREDENTIAL_PERMS,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
path: 'webhook-subscriptions', label: 'Webhooks',
|
|
157
|
+
component: WebhookSubscriptionsPage, permissions: GLOBAL_CREDENTIAL_PERMS,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
MONITORING_ADMIN_SECTION,
|
|
162
|
+
CLOUDWATCH_ADMIN_SECTION,
|
|
163
|
+
SETTINGS_ADMIN_SECTION,
|
|
164
|
+
SECURITY_OPERATIONS_ADMIN_SECTION,
|
|
165
|
+
BOUNCER_ADMIN_SECTION,
|
|
166
|
+
DEVICE_INTEL_ADMIN_SECTION,
|
|
167
|
+
GEOIP_ADMIN_SECTION,
|
|
168
|
+
JOBS_ADMIN_SECTION,
|
|
169
|
+
NETWORK_SECURITY_ADMIN_SECTION,
|
|
170
|
+
DNS_ADMIN_SECTION,
|
|
171
|
+
STORAGE_ADMIN_SECTION,
|
|
172
|
+
SHORTLINKS_ADMIN_SECTION,
|
|
173
|
+
EMAIL_ADMIN_SECTION,
|
|
174
|
+
PUBLIC_MESSAGES_ADMIN_SECTION,
|
|
175
|
+
PUSH_ADMIN_SECTION,
|
|
176
|
+
PHONE_HUB_ADMIN_SECTION,
|
|
177
|
+
ASSISTANT_ADMIN_SECTION,
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
function relativePath(...parts: Array<string | undefined>): string {
|
|
181
|
+
return parts.filter((part) => part != null && part !== '')
|
|
182
|
+
.join('/')
|
|
183
|
+
.replace(/^\/+|\/+$/g, '')
|
|
184
|
+
.replace(/\/+/g, '/');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function absolutePath(...parts: Array<string | undefined>): string {
|
|
188
|
+
const relative = relativePath(...parts);
|
|
189
|
+
return relative ? `/${relative}` : '/';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function AdminDenied() {
|
|
193
|
+
return createElement('div', { className: 'panel' },
|
|
194
|
+
createElement('div', { className: 'empty' },
|
|
195
|
+
createElement('i', { className: 'bi bi-shield-lock' }),
|
|
196
|
+
createElement('h2', null, 'Access denied'),
|
|
197
|
+
createElement('p', { className: 'dim' }, 'Your account does not have permission to open this admin page.'),
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function guardedElement(section: AdminSection, route: AdminRoute, sections: readonly AdminSection[], mount: string): ReactNode {
|
|
202
|
+
const page = createElement(route.component);
|
|
203
|
+
const fallback = route.fallbackToFirstVisible
|
|
204
|
+
? createElement(AdminMountLanding, { sections: sections.filter((candidate) => candidate !== section), mount })
|
|
205
|
+
: createElement(AdminDenied);
|
|
206
|
+
const routeGuarded = route.permissions
|
|
207
|
+
? createElement(Guarded, {
|
|
208
|
+
permission: route.permissions,
|
|
209
|
+
fallback,
|
|
210
|
+
children: page,
|
|
211
|
+
})
|
|
212
|
+
: page;
|
|
213
|
+
const guarded = createElement(Guarded, {
|
|
214
|
+
permission: section.permissions,
|
|
215
|
+
fallback,
|
|
216
|
+
children: routeGuarded,
|
|
217
|
+
});
|
|
218
|
+
return createElement(AdminGlobalScope, { children: guarded });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Admin is global even when embedded beneath a product portal's provider. */
|
|
222
|
+
function AdminGlobalScope({ children }: { children: ReactNode }) {
|
|
223
|
+
const outer = useContext(GroupContext);
|
|
224
|
+
if (!outer) return children;
|
|
225
|
+
return createElement(GroupContext.Provider, {
|
|
226
|
+
value: { ...outer, group: null, member: null, loading: false },
|
|
227
|
+
children,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function routeVisible(section: AdminSection, route: AdminRoute, me: ReturnType<typeof useMe>['data']): boolean {
|
|
232
|
+
return hasPermission(me ?? null, section.permissions, null)
|
|
233
|
+
&& (!route.permissions || hasPermission(me ?? null, route.permissions, null));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function AdminLanding({ section, mount }: { section: AdminSection; mount: string }) {
|
|
237
|
+
const { data: me } = useMe();
|
|
238
|
+
const first = section.routes.find((route) => routeVisible(section, route, me));
|
|
239
|
+
if (!first) return createElement(AdminDenied);
|
|
240
|
+
const base = section.basePath ?? section.id;
|
|
241
|
+
return createElement(Navigate, {
|
|
242
|
+
replace: true,
|
|
243
|
+
to: absolutePath(mount, base, first.path),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function AdminMountLanding({ sections, mount }: { sections: readonly AdminSection[]; mount: string }) {
|
|
248
|
+
const { data: me } = useMe();
|
|
249
|
+
for (const section of sections) {
|
|
250
|
+
const first = section.routes.find((route) => routeVisible(section, route, me));
|
|
251
|
+
if (first) {
|
|
252
|
+
return createElement(Navigate, {
|
|
253
|
+
replace: true,
|
|
254
|
+
to: absolutePath(mount, section.basePath ?? section.id, first.path),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return createElement(AdminDenied);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate mount-relative React Router entries with section AND route gates.
|
|
263
|
+
* A non-root section receives a dynamic index redirect to its first visible
|
|
264
|
+
* route, so direct navigation never lands on a route the operator cannot see.
|
|
265
|
+
*/
|
|
266
|
+
export function adminSectionRoutes(
|
|
267
|
+
sections: readonly AdminSection[],
|
|
268
|
+
opts: { mount?: string } = {},
|
|
269
|
+
): RouteObject[] {
|
|
270
|
+
const mount = relativePath(opts.mount ?? '');
|
|
271
|
+
const routes: RouteObject[] = [];
|
|
272
|
+
const rootSections = sections.filter((section) => relativePath(section.basePath ?? section.id) === '');
|
|
273
|
+
const rootHasIndex = rootSections.some((section) =>
|
|
274
|
+
section.routes.some((route) => relativePath(route.path) === ''));
|
|
275
|
+
if (mount && rootSections.length > 0 && !rootHasIndex) {
|
|
276
|
+
routes.push({
|
|
277
|
+
path: mount,
|
|
278
|
+
element: createElement(AdminGlobalScope, {
|
|
279
|
+
children: createElement(AdminMountLanding, { sections: rootSections, mount }),
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
for (const section of sections) {
|
|
284
|
+
const base = relativePath(section.basePath ?? section.id);
|
|
285
|
+
const landing = relativePath(mount, base);
|
|
286
|
+
const hasIndex = section.routes.some((route) => relativePath(route.path) === '');
|
|
287
|
+
if (base && landing && !hasIndex) {
|
|
288
|
+
routes.push({
|
|
289
|
+
path: landing,
|
|
290
|
+
element: createElement(AdminGlobalScope, {
|
|
291
|
+
children: createElement(AdminLanding, { section, mount }),
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
for (const route of section.routes) {
|
|
296
|
+
routes.push({
|
|
297
|
+
path: relativePath(mount, base, route.path),
|
|
298
|
+
element: guardedElement(section, route, sections, mount),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return routes;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Bridge admin sections into the A4 sidebar registry — sections CONTRIBUTE
|
|
307
|
+
* menu entries; they never own the sidebar. `mount` prefixes every route:
|
|
308
|
+
* '' for the standalone back office (section routes at root), '/system' when
|
|
309
|
+
* embedded in a product portal. Permission gates carry over per item;
|
|
310
|
+
* capability filtering slots in here when the Phase 2 endpoint lands.
|
|
311
|
+
*/
|
|
312
|
+
export function adminSectionsMenu(
|
|
313
|
+
sections: readonly AdminSection[],
|
|
314
|
+
opts: {
|
|
315
|
+
name?: string;
|
|
316
|
+
mount?: string;
|
|
317
|
+
divider?: string;
|
|
318
|
+
/** Category grouping is opt-in for compatibility with shipped menus. */
|
|
319
|
+
grouped?: boolean;
|
|
320
|
+
presentation?: MenuConfig['presentation'];
|
|
321
|
+
} = {},
|
|
322
|
+
): MenuConfig {
|
|
323
|
+
const mount = relativePath(opts.mount ?? '');
|
|
324
|
+
const items: MenuItem[] = [];
|
|
325
|
+
if (opts.divider) items.push({ divider: opts.divider });
|
|
326
|
+
|
|
327
|
+
if (opts.grouped) {
|
|
328
|
+
const groups = new Map<AdminNavigationGroup, MenuItem[]>();
|
|
329
|
+
for (const section of sections) {
|
|
330
|
+
const group = section.navigationGroup ?? 'other';
|
|
331
|
+
const base = relativePath(mount, section.basePath ?? section.id);
|
|
332
|
+
const labeled = section.routes.filter((route) => route.label);
|
|
333
|
+
const destinations: MenuItem[] = labeled.length > 0
|
|
334
|
+
? labeled.map((route) => ({
|
|
335
|
+
id: `admin:${section.id}:${route.path || 'index'}`,
|
|
336
|
+
label: route.label!,
|
|
337
|
+
keywords: [section.title],
|
|
338
|
+
route: absolutePath(base, route.path),
|
|
339
|
+
permissionClauses: [section.permissions, ...(route.permissions ? [route.permissions] : [])],
|
|
340
|
+
}))
|
|
341
|
+
: [{
|
|
342
|
+
id: `admin:${section.id}`,
|
|
343
|
+
label: section.title,
|
|
344
|
+
keywords: [section.title],
|
|
345
|
+
route: absolutePath(base),
|
|
346
|
+
permissionClauses: [section.permissions],
|
|
347
|
+
}];
|
|
348
|
+
groups.set(group, [...(groups.get(group) ?? []), ...destinations]);
|
|
349
|
+
}
|
|
350
|
+
for (const [group, children] of [...groups.entries()].sort((a, b) =>
|
|
351
|
+
ADMIN_NAVIGATION_GROUPS[a[0]].order - ADMIN_NAVIGATION_GROUPS[b[0]].order)) {
|
|
352
|
+
const meta = ADMIN_NAVIGATION_GROUPS[group];
|
|
353
|
+
items.push({ id: meta.id, label: meta.label, icon: meta.icon, children });
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
name: opts.name ?? 'admin',
|
|
357
|
+
scope: 'admin',
|
|
358
|
+
presentation: opts.presentation ?? 'static',
|
|
359
|
+
items,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const section of sections) {
|
|
364
|
+
const labeled = section.routes.filter((r) => r.label);
|
|
365
|
+
const base = relativePath(mount, section.basePath ?? section.id);
|
|
366
|
+
const children = labeled.map((route) => ({
|
|
367
|
+
label: route.label!,
|
|
368
|
+
route: absolutePath(base, route.path),
|
|
369
|
+
permissions: route.permissions,
|
|
370
|
+
}));
|
|
371
|
+
items.push({
|
|
372
|
+
label: section.title,
|
|
373
|
+
icon: section.icon,
|
|
374
|
+
permissions: section.permissions,
|
|
375
|
+
// Root-mounted sections cannot own `/`; render a pure parent and
|
|
376
|
+
// let the first visible child be the landing. Prefixed sections
|
|
377
|
+
// use the generated dynamic index redirect above.
|
|
378
|
+
...(base ? { route: absolutePath(base) } : {}),
|
|
379
|
+
children,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return { name: opts.name ?? 'admin', scope: 'admin', presentation: opts.presentation ?? 'static', items };
|
|
383
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// JobDashboardPage — the jobs engine at a glance. Port of web-mojo
|
|
2
|
+
// JobDashboardPage.js, reading top to bottom the way an operator triages:
|
|
3
|
+
// is the fleet alive → which channel is backed up → what is the throughput →
|
|
4
|
+
// what can I do about it.
|
|
5
|
+
//
|
|
6
|
+
// EVERY number comes from `GET /api/jobs/stats`. `GET /api/jobs/health` raises
|
|
7
|
+
// server-side (get_channel_health reads a key get_queue_state stopped
|
|
8
|
+
// returning), and web-mojo's JobHealthView — the only consumer — was never
|
|
9
|
+
// mounted. Stats already carries channels, runners, totals and the scheduler.
|
|
10
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
11
|
+
import { useLocation, useNavigate } from 'react-router-dom';
|
|
12
|
+
import { KPITile } from '../../charts';
|
|
13
|
+
import { JobChannelsPanel } from './sections/JobChannelsPanel';
|
|
14
|
+
import { JobOperationsSection } from './sections/JobOperationsSection';
|
|
15
|
+
import { JobRunnersStrip } from './sections/JobRunnersStrip';
|
|
16
|
+
import { JobThroughputSection } from './sections/JobThroughputSection';
|
|
17
|
+
import { jobsKeys, useChannelOptions, useJobStats } from './queries';
|
|
18
|
+
|
|
19
|
+
export function JobDashboardPage() {
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
const stats = useJobStats();
|
|
22
|
+
const channelOptions = useChannelOptions();
|
|
23
|
+
const navigate = useNavigate();
|
|
24
|
+
const location = useLocation();
|
|
25
|
+
|
|
26
|
+
const totals = stats.data?.totals;
|
|
27
|
+
const refreshAll = () => { void queryClient.invalidateQueries({ queryKey: jobsKeys.root }); };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Open the jobs table already filtered to a channel. The path is derived
|
|
31
|
+
* from the CURRENT location rather than hardcoded, because an admin
|
|
32
|
+
* section never knows whether it is mounted at the root or under /system.
|
|
33
|
+
*/
|
|
34
|
+
const openChannel = (channel: string) => {
|
|
35
|
+
const base = location.pathname.replace(/\/+$/, '');
|
|
36
|
+
navigate({ pathname: `${base}/list`, search: `?channel=${encodeURIComponent(channel)}` });
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const loading = stats.isPending;
|
|
40
|
+
const tile = (label: string, value: number | undefined, severity: 'critical' | 'warn' | 'info' | 'good' | null, hint?: string) => (
|
|
41
|
+
<KPITile
|
|
42
|
+
label={label}
|
|
43
|
+
value={value ?? null}
|
|
44
|
+
severity={severity}
|
|
45
|
+
hint={hint}
|
|
46
|
+
loading={loading}
|
|
47
|
+
formatter={(v) => v.toLocaleString()}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="jobs-dashboard">
|
|
53
|
+
<header className="jobs-page-head">
|
|
54
|
+
<div>
|
|
55
|
+
<div className="eyebrow">Jobs</div>
|
|
56
|
+
<h1>Job engine</h1>
|
|
57
|
+
<p>Runner fleet, queue depth and the operations that unstick them.</p>
|
|
58
|
+
</div>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="btn btn-compact"
|
|
62
|
+
disabled={stats.isFetching}
|
|
63
|
+
onClick={refreshAll}
|
|
64
|
+
>
|
|
65
|
+
<i className={`bi bi-arrow-clockwise${stats.isFetching ? ' spin' : ''}`} /> Refresh
|
|
66
|
+
</button>
|
|
67
|
+
</header>
|
|
68
|
+
|
|
69
|
+
{stats.isError && (
|
|
70
|
+
<div className="panel jobs-note jobs-note-bad">
|
|
71
|
+
<i className="bi bi-exclamation-triangle" />
|
|
72
|
+
{stats.error instanceof Error ? stats.error.message : 'Could not load job statistics.'}
|
|
73
|
+
<button type="button" className="btn btn-compact" onClick={() => void stats.refetch()}>Retry</button>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<JobRunnersStrip />
|
|
78
|
+
|
|
79
|
+
<div className="jobs-kpi-strip">
|
|
80
|
+
{tile('Queued', totals?.queued, (totals?.queued ?? 0) > 100 ? 'critical' : (totals?.queued ?? 0) > 50 ? 'warn' : null)}
|
|
81
|
+
{tile('In flight', totals?.inflight, null)}
|
|
82
|
+
{/* Running splits active from stale: a "running" job whose
|
|
83
|
+
runner is no longer alive is not running at all. */}
|
|
84
|
+
{tile(
|
|
85
|
+
'Running',
|
|
86
|
+
totals?.running,
|
|
87
|
+
(totals?.running_stale ?? 0) > 0 ? 'warn' : null,
|
|
88
|
+
totals ? `${totals.running_active} active · ${totals.running_stale} stale` : undefined,
|
|
89
|
+
)}
|
|
90
|
+
{tile('Scheduled', totals?.scheduled, null)}
|
|
91
|
+
{tile('Completed', totals?.completed, 'good')}
|
|
92
|
+
{tile('Failed', totals?.failed, (totals?.failed ?? 0) > 0 ? 'critical' : null)}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<JobChannelsPanel
|
|
96
|
+
stats={stats.data}
|
|
97
|
+
isPending={stats.isPending}
|
|
98
|
+
onSelectChannel={openChannel}
|
|
99
|
+
onSchedulerChanged={refreshAll}
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<JobThroughputSection channels={channelOptions.channels} />
|
|
103
|
+
|
|
104
|
+
<JobOperationsSection
|
|
105
|
+
channels={channelOptions.channels}
|
|
106
|
+
channelsPending={channelOptions.isPending}
|
|
107
|
+
onChanged={refreshAll}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|