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,216 @@
|
|
|
1
|
+
// JobRunnersPage — the fleet roster. Port of web-mojo JobRunnersPage.js +
|
|
2
|
+
// sections/JobRunnersSection.js.
|
|
3
|
+
//
|
|
4
|
+
// Deliberately NOT a ModelTable. `GET /api/jobs/runners` reads exactly one
|
|
5
|
+
// param (`channel`) and returns the COMPLETE list — no start/size, no sort, no
|
|
6
|
+
// search. Wrapping it in a server table would render a pager and sort headers
|
|
7
|
+
// that silently do nothing, so search and ordering happen client-side over the
|
|
8
|
+
// full list. Same precedent as MetricsPermissionsPage.
|
|
9
|
+
import { useMemo, useState } from 'react';
|
|
10
|
+
import { Badge, ExpandingSearch, fmt } from '../../ui';
|
|
11
|
+
import { showRunnerDetail } from './RunnerDetail';
|
|
12
|
+
import {
|
|
13
|
+
formatHeartbeatAge,
|
|
14
|
+
formatUptime,
|
|
15
|
+
heartbeatAgeSeconds,
|
|
16
|
+
runnerFailureRate,
|
|
17
|
+
runnerHealth,
|
|
18
|
+
runnerUptimeSeconds,
|
|
19
|
+
type RunnerHealthKey,
|
|
20
|
+
type RunnerRow,
|
|
21
|
+
} from './models';
|
|
22
|
+
import { useRunners } from './queries';
|
|
23
|
+
|
|
24
|
+
type SortKey = 'runner_id' | 'health' | 'channels' | 'jobs_processed' | 'jobs_failed' | 'last_heartbeat';
|
|
25
|
+
|
|
26
|
+
const HEALTH_ORDER: Record<RunnerHealthKey, number> = { down: 0, stale: 1, healthy: 2 };
|
|
27
|
+
|
|
28
|
+
const FILTERS: { key: 'all' | RunnerHealthKey; label: string }[] = [
|
|
29
|
+
{ key: 'all', label: 'All' },
|
|
30
|
+
{ key: 'healthy', label: 'Healthy' },
|
|
31
|
+
{ key: 'stale', label: 'Stale' },
|
|
32
|
+
{ key: 'down', label: 'Down' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function JobRunnersPage() {
|
|
36
|
+
const query = useRunners();
|
|
37
|
+
const [search, setSearch] = useState('');
|
|
38
|
+
const [filter, setFilter] = useState<'all' | RunnerHealthKey>('all');
|
|
39
|
+
const [sort, setSort] = useState<{ key: SortKey; desc: boolean }>({ key: 'health', desc: false });
|
|
40
|
+
|
|
41
|
+
const rows = useMemo(() => {
|
|
42
|
+
const all = query.data ?? [];
|
|
43
|
+
const term = search.trim().toLowerCase();
|
|
44
|
+
const filtered = all.filter((runner) => {
|
|
45
|
+
if (filter !== 'all' && runnerHealth(runner).key !== filter) return false;
|
|
46
|
+
if (!term) return true;
|
|
47
|
+
return `${runner.runner_id} ${runner.hostname} ${runner.channels.join(' ')}`.toLowerCase().includes(term);
|
|
48
|
+
});
|
|
49
|
+
const value = (runner: RunnerRow): number | string => {
|
|
50
|
+
switch (sort.key) {
|
|
51
|
+
case 'health': return HEALTH_ORDER[runnerHealth(runner).key];
|
|
52
|
+
case 'channels': return runner.channels.length;
|
|
53
|
+
case 'jobs_processed': return runner.jobs_processed;
|
|
54
|
+
case 'jobs_failed': return runner.jobs_failed;
|
|
55
|
+
case 'last_heartbeat': return heartbeatAgeSeconds(runner.last_heartbeat) ?? Number.MAX_SAFE_INTEGER;
|
|
56
|
+
default: return runner.runner_id;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return [...filtered].sort((a, b) => {
|
|
60
|
+
const av = value(a);
|
|
61
|
+
const bv = value(b);
|
|
62
|
+
const cmp = typeof av === 'number' && typeof bv === 'number'
|
|
63
|
+
? av - bv
|
|
64
|
+
: String(av).localeCompare(String(bv));
|
|
65
|
+
return sort.desc ? -cmp : cmp;
|
|
66
|
+
});
|
|
67
|
+
}, [query.data, search, filter, sort]);
|
|
68
|
+
|
|
69
|
+
const total = query.data?.length ?? 0;
|
|
70
|
+
const alive = (query.data ?? []).filter((runner) => runner.alive).length;
|
|
71
|
+
|
|
72
|
+
const header = (key: SortKey, label: string) => (
|
|
73
|
+
<th>
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className="th-sort"
|
|
77
|
+
onClick={() => setSort((prev) => ({ key, desc: prev.key === key ? !prev.desc : false }))}
|
|
78
|
+
>
|
|
79
|
+
{label}
|
|
80
|
+
<i className={`bi sort-icon ${sort.key !== key ? 'bi-arrow-down-up sort-idle' : sort.desc ? 'bi-arrow-down' : 'bi-arrow-up'}`} />
|
|
81
|
+
</button>
|
|
82
|
+
</th>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="panel jobs-runners-page">
|
|
87
|
+
<div className="toolbar">
|
|
88
|
+
<div className="toolbar-heading">
|
|
89
|
+
<div className="eyebrow">Jobs</div>
|
|
90
|
+
<h1 className="panel-title">Runners</h1>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="toolbar-controls">
|
|
93
|
+
<ExpandingSearch value={search} onChange={setSearch} placeholder="Search runner, host, channel…" />
|
|
94
|
+
<div className="tool-group">
|
|
95
|
+
<button
|
|
96
|
+
className="btn-icon"
|
|
97
|
+
title="Refresh · auto every 60s"
|
|
98
|
+
onClick={() => void query.refetch()}
|
|
99
|
+
>
|
|
100
|
+
<i className={`bi bi-arrow-repeat${query.isFetching ? ' spin' : ''}`} />
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="seg-row">
|
|
107
|
+
<div className="seg">
|
|
108
|
+
{FILTERS.map((entry) => (
|
|
109
|
+
<button
|
|
110
|
+
key={entry.key}
|
|
111
|
+
className={`seg-btn${filter === entry.key ? ' seg-active' : ''}`}
|
|
112
|
+
onClick={() => setFilter(entry.key)}
|
|
113
|
+
>
|
|
114
|
+
{entry.label}
|
|
115
|
+
</button>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
{total > 0 && (
|
|
119
|
+
<span className="result-count">
|
|
120
|
+
{alive} of {total} alive
|
|
121
|
+
{rows.length !== total && <span className="dim"> · {rows.length} shown</span>}
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="tbl-scroll">
|
|
127
|
+
<table className="tbl">
|
|
128
|
+
<thead>
|
|
129
|
+
<tr>
|
|
130
|
+
{header('runner_id', 'Runner')}
|
|
131
|
+
{header('health', 'Health')}
|
|
132
|
+
{header('channels', 'Channels')}
|
|
133
|
+
{header('jobs_processed', 'Processed')}
|
|
134
|
+
{header('jobs_failed', 'Failed')}
|
|
135
|
+
<th>Failure rate</th>
|
|
136
|
+
{header('last_heartbeat', 'Heartbeat')}
|
|
137
|
+
<th>Uptime</th>
|
|
138
|
+
</tr>
|
|
139
|
+
</thead>
|
|
140
|
+
<tbody>
|
|
141
|
+
{query.isPending && (
|
|
142
|
+
<tr><td colSpan={8}><div className="empty-state"><p>Loading runners…</p></div></td></tr>
|
|
143
|
+
)}
|
|
144
|
+
{query.isError && (
|
|
145
|
+
<tr><td colSpan={8}>
|
|
146
|
+
<div className="empty-state">
|
|
147
|
+
<i className="bi bi-exclamation-triangle" />
|
|
148
|
+
<h3>Could not load runners</h3>
|
|
149
|
+
<p>{query.error instanceof Error ? query.error.message : 'Request failed'}</p>
|
|
150
|
+
<button className="btn" onClick={() => void query.refetch()}>Retry</button>
|
|
151
|
+
</div>
|
|
152
|
+
</td></tr>
|
|
153
|
+
)}
|
|
154
|
+
{!query.isPending && !query.isError && rows.length === 0 && (
|
|
155
|
+
<tr><td colSpan={8}>
|
|
156
|
+
<div className="empty-state">
|
|
157
|
+
<i className="bi bi-cpu" />
|
|
158
|
+
{total === 0
|
|
159
|
+
? (
|
|
160
|
+
<>
|
|
161
|
+
<h3>No runners registered</h3>
|
|
162
|
+
<p>Nothing is consuming the queues — jobs will not be processed.</p>
|
|
163
|
+
</>
|
|
164
|
+
)
|
|
165
|
+
: (
|
|
166
|
+
<>
|
|
167
|
+
<h3>No runners match this view</h3>
|
|
168
|
+
<button className="btn" onClick={() => { setSearch(''); setFilter('all'); }}>Clear</button>
|
|
169
|
+
</>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</td></tr>
|
|
173
|
+
)}
|
|
174
|
+
{rows.map((runner) => {
|
|
175
|
+
const health = runnerHealth(runner);
|
|
176
|
+
const rate = runnerFailureRate(runner);
|
|
177
|
+
const uptime = runnerUptimeSeconds(runner);
|
|
178
|
+
return (
|
|
179
|
+
<tr key={runner.id} className="row-click" onClick={() => showRunnerDetail(runner)}>
|
|
180
|
+
<td>
|
|
181
|
+
<div className="jobs-runner-id"><code>{runner.runner_id}</code></div>
|
|
182
|
+
<div className="dim">{runner.hostname}</div>
|
|
183
|
+
</td>
|
|
184
|
+
<td>
|
|
185
|
+
<span className={`jobs-dot jobs-dot-${health.tone}`} />
|
|
186
|
+
<Badge tone={health.tone}>{health.label}</Badge>
|
|
187
|
+
</td>
|
|
188
|
+
<td>
|
|
189
|
+
{runner.channels.length > 0
|
|
190
|
+
? runner.channels.map((channel) => <code key={channel} className="jobs-channel-chip">{channel}</code>)
|
|
191
|
+
: <span className="dim">none</span>}
|
|
192
|
+
</td>
|
|
193
|
+
<td>{fmt.number(runner.jobs_processed)}</td>
|
|
194
|
+
<td>{runner.jobs_failed > 0 ? <span className="jobs-text-danger">{fmt.number(runner.jobs_failed)}</span> : '0'}</td>
|
|
195
|
+
<td>{rate == null ? '—' : `${rate.toFixed(2)}%`}</td>
|
|
196
|
+
<td title={fmt.datetime(runner.last_heartbeat)}>
|
|
197
|
+
{formatHeartbeatAge(heartbeatAgeSeconds(runner.last_heartbeat))}
|
|
198
|
+
</td>
|
|
199
|
+
<td>{uptime == null ? '—' : formatUptime(uptime)}</td>
|
|
200
|
+
</tr>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</tbody>
|
|
204
|
+
</table>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Stating the contract beats rendering a pager that would lie. */}
|
|
208
|
+
<div className="pager">
|
|
209
|
+
<span className="result-count">
|
|
210
|
+
{total === 0 ? 'No runners' : `${total} runner${total === 1 ? '' : 's'}`}
|
|
211
|
+
<span className="dim"> · the runners endpoint returns the complete fleet; sorting and search are applied here</span>
|
|
212
|
+
</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// JobsTablePage — every job in the engine, one segmented table. Port of
|
|
2
|
+
// web-mojo JobsTablePage.js + sections/JobTableSection.js.
|
|
3
|
+
//
|
|
4
|
+
// The source shipped a `Page` wrapping a reusable `JobTableSection` so the
|
|
5
|
+
// same table could be embedded elsewhere with a hardcoded `status`. Here the
|
|
6
|
+
// segment IS the params store — `status` (+ `run_at__isnull`) — so one
|
|
7
|
+
// ModelTable with preset bundles replaces the five configurations, and a
|
|
8
|
+
// deep link like `?channel=email&status=failed` lands on exactly the view it
|
|
9
|
+
// describes. That is also how JobChannelsPanel opens a backed-up channel.
|
|
10
|
+
//
|
|
11
|
+
// Wire notes that shape this page:
|
|
12
|
+
// · `defaultSort='-created'` is not a nicety. jobs.Job's backend default
|
|
13
|
+
// ordering is `-id` over 32-char uuid hex — an unsorted list is
|
|
14
|
+
// lexicographic noise. `normalizeJobListParams` guarantees a sort reaches
|
|
15
|
+
// the wire; this names the one an operator wants.
|
|
16
|
+
// · the export is a bounded CLIENT projection (see columns.tsx) because the
|
|
17
|
+
// server's `download_format` would ship payload, metadata and last_error.
|
|
18
|
+
// · batch Cancel sends `payload: true` — `{}` is falsy in Python and the
|
|
19
|
+
// handler answers "cancel_request must be true" at HTTP 200.
|
|
20
|
+
import { useMemo } from 'react';
|
|
21
|
+
import { useSearchParams } from 'react-router-dom';
|
|
22
|
+
import { useCan } from '../../client';
|
|
23
|
+
import { ModelTable, type BatchAction, type FilterDef, type RowTone } from '../../ui';
|
|
24
|
+
import { JOB_EXPORTER, JOB_SEGMENTS, jobColumns, jobSegmentOf } from './columns';
|
|
25
|
+
import { showJobDetail } from './JobDetail';
|
|
26
|
+
import {
|
|
27
|
+
canCancelJob,
|
|
28
|
+
isOverdueJob,
|
|
29
|
+
JOBS_MANAGE_PERMS,
|
|
30
|
+
JOB_STATUS_OPTIONS,
|
|
31
|
+
JobModel,
|
|
32
|
+
type JobRow,
|
|
33
|
+
} from './models';
|
|
34
|
+
import { useChannelOptions, useRunners } from './queries';
|
|
35
|
+
|
|
36
|
+
/** Same tick as the other operational tables; skipped while a dialog is open. */
|
|
37
|
+
const AUTO_REFRESH_SEC = 30;
|
|
38
|
+
|
|
39
|
+
export function JobsTablePage() {
|
|
40
|
+
const [searchParams] = useSearchParams();
|
|
41
|
+
// Columns follow the status being viewed, not the chip highlight — see
|
|
42
|
+
// jobSegmentOf. Reading the URL directly keeps this a pure observer of the
|
|
43
|
+
// params store the table owns.
|
|
44
|
+
const segment = jobSegmentOf(searchParams.get('status'), searchParams.get('run_at__isnull'));
|
|
45
|
+
|
|
46
|
+
const channels = useChannelOptions();
|
|
47
|
+
const runners = useRunners();
|
|
48
|
+
const { can: canManage } = useCan(JOBS_MANAGE_PERMS);
|
|
49
|
+
const cancel = JobModel.useAction('cancel_request');
|
|
50
|
+
|
|
51
|
+
const filters = useMemo<FilterDef[]>(() => {
|
|
52
|
+
const out: FilterDef[] = [
|
|
53
|
+
{ key: 'status', label: 'Status', type: 'multiselect', options: [...JOB_STATUS_OPTIONS] },
|
|
54
|
+
];
|
|
55
|
+
// An empty select is worse than no select: it looks broken and it can
|
|
56
|
+
// only ever write a filter nothing matches. Both of these lists come
|
|
57
|
+
// from live state (configured channels, the registered fleet), so both
|
|
58
|
+
// are offered only once they exist.
|
|
59
|
+
if (channels.channels.length > 0) {
|
|
60
|
+
out.push({ key: 'channel', label: 'Channel', type: 'select', options: channels.options });
|
|
61
|
+
}
|
|
62
|
+
out.push({ key: 'func', label: 'Function', type: 'text', placeholder: 'e.g. send_email' });
|
|
63
|
+
const fleet = runners.data ?? [];
|
|
64
|
+
if (fleet.length > 0) {
|
|
65
|
+
out.push({
|
|
66
|
+
// `runner_id` exact — the runner that CLAIMED the job. Free
|
|
67
|
+
// search also matches it (Job declares no SEARCH_FIELDS, so the
|
|
68
|
+
// backend falls back to every text field), which is how a job
|
|
69
|
+
// held by a runner that has since vanished stays findable.
|
|
70
|
+
key: 'runner_id', label: 'Runner', type: 'select',
|
|
71
|
+
options: fleet.map((runner) => ({ value: runner.runner_id, label: runner.runner_id })),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
out.push({ key: 'created', label: 'Created', type: 'daterange' });
|
|
75
|
+
return out;
|
|
76
|
+
}, [channels.channels.length, channels.options, runners.data]);
|
|
77
|
+
|
|
78
|
+
const batchActions = useMemo<BatchAction<JobRow>[]>(() => {
|
|
79
|
+
if (!canManage) return [];
|
|
80
|
+
return [{
|
|
81
|
+
key: 'cancel',
|
|
82
|
+
label: 'Cancel',
|
|
83
|
+
icon: 'bi-x-circle',
|
|
84
|
+
danger: true,
|
|
85
|
+
// A terminal job is refused by JobActionsService — never submit one.
|
|
86
|
+
eligible: canCancelJob,
|
|
87
|
+
confirm: 'Request cancellation of the selected job(s)? Running jobs stop cooperatively; queued jobs are canceled outright.',
|
|
88
|
+
run: (row) => cancel.mutateAsync({ id: row.id, payload: true }),
|
|
89
|
+
}];
|
|
90
|
+
}, [canManage, cancel]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<ModelTable<JobRow>
|
|
94
|
+
model={JobModel}
|
|
95
|
+
eyebrow="Job engine"
|
|
96
|
+
title="Jobs"
|
|
97
|
+
columns={jobColumns(segment)}
|
|
98
|
+
filters={filters}
|
|
99
|
+
presets={JOB_SEGMENTS}
|
|
100
|
+
defaultSort="-created"
|
|
101
|
+
searchable
|
|
102
|
+
searchPlaceholder="Search id, function, runner, error…"
|
|
103
|
+
rowTone={rowTone}
|
|
104
|
+
selectable={canManage}
|
|
105
|
+
batchActions={batchActions}
|
|
106
|
+
columnChooser
|
|
107
|
+
persistState
|
|
108
|
+
persistKey="admin-jobs-list"
|
|
109
|
+
autoRefresh={AUTO_REFRESH_SEC}
|
|
110
|
+
exporter={JOB_EXPORTER}
|
|
111
|
+
exportFormats={['csv', 'json']}
|
|
112
|
+
onRowClick={(row) => showJobDetail(row.id)}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Failures shout; a scheduled job the scheduler has walked past warns. */
|
|
118
|
+
function rowTone(row: JobRow): RowTone | null {
|
|
119
|
+
if (row.status === 'failed') return 'danger';
|
|
120
|
+
if (isOverdueJob(row)) return 'warning';
|
|
121
|
+
return null;
|
|
122
|
+
}
|