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,80 @@
|
|
|
1
|
+
// FlowStrip — the horizontal "STEP 1 → STEP 2 → STEP 3" strip for records
|
|
2
|
+
// that describe a process (RuleSet triggering: Match → Bundle → Threshold →
|
|
3
|
+
// Re-trigger; incident triage; job lifecycle).
|
|
4
|
+
// Port of web-mojo src/core/views/data/FlowStrip.js.
|
|
5
|
+
//
|
|
6
|
+
// ┌──────────┬──────────┬───────────┬────────────┐
|
|
7
|
+
// │ STEP 1 │ STEP 2 │ STEP 3 │ STEP 4 │
|
|
8
|
+
// │ Match ✎ │ Bundle │ Threshold │ Re-trigger │
|
|
9
|
+
// │ value │ value │ value │ value │
|
|
10
|
+
// │ hint │ hint │ hint │ hint │
|
|
11
|
+
// └──────────┴──────────┴───────────┴────────────┘
|
|
12
|
+
//
|
|
13
|
+
// Deviations from source: `value`/`hint` were TRUSTED-HTML strings — they are
|
|
14
|
+
// ReactNode slots here (architecture rule 6); the per-step `action`/`actionData`
|
|
15
|
+
// pair that dispatched through the global action pipeline becomes one
|
|
16
|
+
// `onEditStep(step, index)` callback.
|
|
17
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
18
|
+
import type { Tone } from '../format';
|
|
19
|
+
import { normalizeTone } from './StatusPanel';
|
|
20
|
+
|
|
21
|
+
export interface FlowStep {
|
|
22
|
+
/** Small uppercase eyebrow. Defaults to `STEP <n>`. */
|
|
23
|
+
num?: string;
|
|
24
|
+
title: string;
|
|
25
|
+
/** Primary value line — a slot, so `<code>`/`<strong>` compose safely. */
|
|
26
|
+
value?: ReactNode;
|
|
27
|
+
/** Small descriptor under the value. */
|
|
28
|
+
hint?: ReactNode;
|
|
29
|
+
/** Renders the value in the muted-italic "not configured" style. */
|
|
30
|
+
empty?: boolean;
|
|
31
|
+
/** Colors the step's eyebrow + rail (per-step signal: which stage failed). */
|
|
32
|
+
tone?: Tone;
|
|
33
|
+
/** Opt a step OUT of the edit affordance when `onEditStep` is wired. */
|
|
34
|
+
editable?: boolean;
|
|
35
|
+
/** Tooltip on the edit button; defaults to `Edit <title>`. */
|
|
36
|
+
editTitle?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function FlowStrip({ steps, onEditStep, editIcon = 'bi-pencil' }: {
|
|
40
|
+
steps: FlowStep[];
|
|
41
|
+
/** Wiring this renders a pencil on every step except `editable: false` ones. */
|
|
42
|
+
onEditStep?: (step: FlowStep, index: number) => void;
|
|
43
|
+
editIcon?: string;
|
|
44
|
+
}) {
|
|
45
|
+
if (!steps.length) return null;
|
|
46
|
+
// Column count rides a custom property so the container queries in CSS can
|
|
47
|
+
// reflow it (4 → 2 → 1) off the strip's OWN width, not the viewport — it is
|
|
48
|
+
// routinely nested inside an `lg` modal.
|
|
49
|
+
const style = { '--flow-strip-cols': steps.length } as CSSProperties;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="detail-flow-strip" style={style}>
|
|
53
|
+
{steps.map((step, i) => {
|
|
54
|
+
const tone = normalizeTone(step.tone, 'FlowStrip');
|
|
55
|
+
const canEdit = !!onEditStep && step.editable !== false;
|
|
56
|
+
return (
|
|
57
|
+
<div key={`${step.title}-${i}`} className={`flow-strip-step tone-${tone}`}>
|
|
58
|
+
<div className="flow-strip-num">{step.num ?? `STEP ${i + 1}`}</div>
|
|
59
|
+
<div className="flow-strip-title">
|
|
60
|
+
{step.title}
|
|
61
|
+
{canEdit && (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
className="btn-icon btn-icon-sm flow-strip-action"
|
|
65
|
+
title={step.editTitle ?? `Edit ${step.title.toLowerCase()}`}
|
|
66
|
+
aria-label={step.editTitle ?? `Edit ${step.title.toLowerCase()}`}
|
|
67
|
+
onClick={() => onEditStep!(step, i)}
|
|
68
|
+
>
|
|
69
|
+
<i className={`bi ${editIcon}`} />
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
<div className={`flow-strip-value${step.empty ? ' flow-strip-empty' : ''}`}>{step.value}</div>
|
|
74
|
+
{step.hint != null && step.hint !== '' && <div className="flow-strip-hint">{step.hint}</div>}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// KnownFieldsCard — the "known JSON keys promoted, raw blob below" pattern.
|
|
2
|
+
// Blob-shaped fields (`metadata`, `ip_info`, `device_info`, `payload`) carry a
|
|
3
|
+
// few keys the framework knows about plus an open-ended bag of extras; detail
|
|
4
|
+
// sections want the known ones in a clean label/value grid and the raw JSON
|
|
5
|
+
// accessible but subordinated.
|
|
6
|
+
// Port of web-mojo src/core/views/data/KnownFieldsCard.js.
|
|
7
|
+
//
|
|
8
|
+
// Created by ian@example.com
|
|
9
|
+
// Reasoning brute-force from same /24
|
|
10
|
+
// Last resolved 2026-04-21 11:42
|
|
11
|
+
// ▶ Raw metadata
|
|
12
|
+
//
|
|
13
|
+
// Deviation from source: `formatter` returned TRUSTED HTML — `format` returns
|
|
14
|
+
// ReactNode here (architecture rule 6). The string form still works and now
|
|
15
|
+
// names an `fmt.*` formatter; an unknown name warns and falls back to the plain
|
|
16
|
+
// value rather than rendering nothing (architecture rule 4).
|
|
17
|
+
import type { ReactNode } from 'react';
|
|
18
|
+
import * as fmt from '../format';
|
|
19
|
+
|
|
20
|
+
/** The `fmt.*` formatters a spec may name as a string. */
|
|
21
|
+
const NAMED_FORMATTERS = {
|
|
22
|
+
date: (v: unknown) => fmt.date(v as string | number | null),
|
|
23
|
+
datetime: (v: unknown) => fmt.datetime(v as string | number | null),
|
|
24
|
+
relative: (v: unknown) => fmt.relative(v as string | number | null),
|
|
25
|
+
} satisfies Record<string, (value: unknown) => ReactNode>;
|
|
26
|
+
|
|
27
|
+
export type NamedFormatter = keyof typeof NAMED_FORMATTERS;
|
|
28
|
+
|
|
29
|
+
export interface KnownField {
|
|
30
|
+
/** Key in the blob. Dotted paths traverse nested objects (`os.family`). */
|
|
31
|
+
key: string;
|
|
32
|
+
/** Row label; defaults to the key itself. */
|
|
33
|
+
label?: string;
|
|
34
|
+
/** An `fmt.*` name, or a function returning any node. */
|
|
35
|
+
format?: NamedFormatter | ((value: unknown, key: string, data: Record<string, unknown>) => ReactNode);
|
|
36
|
+
/** Drop the row entirely when the value is missing (default: show "—"). */
|
|
37
|
+
hideEmpty?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Dotted-path lookup. An own property wins over traversal, so a blob that
|
|
42
|
+
* literally holds the key `"os.family"` still resolves (source behavior).
|
|
43
|
+
*/
|
|
44
|
+
function lookup(data: Record<string, unknown>, key: string): unknown {
|
|
45
|
+
if (!key) return undefined;
|
|
46
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) return data[key];
|
|
47
|
+
if (!key.includes('.')) return undefined;
|
|
48
|
+
let cursor: unknown = data;
|
|
49
|
+
for (const part of key.split('.')) {
|
|
50
|
+
if (cursor == null || typeof cursor !== 'object') return undefined;
|
|
51
|
+
cursor = (cursor as Record<string, unknown>)[part];
|
|
52
|
+
}
|
|
53
|
+
return cursor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatValue(value: unknown, spec: KnownField, data: Record<string, unknown>): ReactNode {
|
|
57
|
+
const { format } = spec;
|
|
58
|
+
if (typeof format === 'function') return format(value, spec.key, data);
|
|
59
|
+
if (typeof format === 'string') {
|
|
60
|
+
const named = NAMED_FORMATTERS[format];
|
|
61
|
+
if (named) return named(value);
|
|
62
|
+
console.warn(`[KnownFieldsCard] unknown format ${JSON.stringify(format)} for key "${spec.key}" — rendering the raw value. Valid: ${Object.keys(NAMED_FORMATTERS).join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
// Default: scalars as text, objects as compact JSON in a code span.
|
|
65
|
+
if (value !== null && typeof value === 'object') {
|
|
66
|
+
return <code className="dim">{JSON.stringify(value)}</code>;
|
|
67
|
+
}
|
|
68
|
+
return String(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function KnownFieldsCard({ data, known = [], showRaw = true, rawLabel = 'Raw JSON', rawCollapsed = true, emptyText = 'No data.' }: {
|
|
72
|
+
data: Record<string, unknown>;
|
|
73
|
+
known?: KnownField[];
|
|
74
|
+
showRaw?: boolean;
|
|
75
|
+
rawLabel?: string;
|
|
76
|
+
/** `<details>` starts closed (default) or open. */
|
|
77
|
+
rawCollapsed?: boolean;
|
|
78
|
+
emptyText?: string;
|
|
79
|
+
}) {
|
|
80
|
+
if (!known.length && Object.keys(data).length === 0) {
|
|
81
|
+
return <div className="dim detail-known-fields-empty">{emptyText}</div>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rows = known
|
|
85
|
+
.map((spec) => ({ spec, value: lookup(data, spec.key) }))
|
|
86
|
+
.filter(({ spec, value }) => !(spec.hideEmpty && (value == null || value === '')));
|
|
87
|
+
|
|
88
|
+
let json: string;
|
|
89
|
+
try {
|
|
90
|
+
json = JSON.stringify(data, null, 2);
|
|
91
|
+
} catch {
|
|
92
|
+
// Circular blobs reach here — the grid above is still worth rendering.
|
|
93
|
+
json = String(data);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="detail-known-fields-card">
|
|
98
|
+
{rows.length > 0 && (
|
|
99
|
+
<div className="detail-known-fields-grid">
|
|
100
|
+
{rows.map(({ spec, value }) => {
|
|
101
|
+
const missing = value == null || value === '';
|
|
102
|
+
return (
|
|
103
|
+
<div key={spec.key} className="flat-row detail-known-field-row">
|
|
104
|
+
<div className="flat-label">{spec.label ?? spec.key}</div>
|
|
105
|
+
<div className="flat-value">
|
|
106
|
+
{missing ? <span className="dim-italic">—</span> : formatValue(value, spec, data)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{showRaw && Object.keys(data).length > 0 && (
|
|
114
|
+
<details className="detail-known-fields-raw" open={!rawCollapsed}>
|
|
115
|
+
<summary className="detail-known-fields-raw-summary">{rawLabel}</summary>
|
|
116
|
+
<pre className="detail-known-fields-raw-body">{json}</pre>
|
|
117
|
+
</details>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// MetadataSection — view/edit the `metadata` blob on any django-mojo record.
|
|
2
|
+
// Port of web-mojo src/extensions/admin/shared/AdminMetadataSection.js.
|
|
3
|
+
//
|
|
4
|
+
// Metadata [+ Add]
|
|
5
|
+
// ┌──────────────────────────────────────────────────────────┐
|
|
6
|
+
// │ timezone America/New_York ✎ 🗑 │
|
|
7
|
+
// │ signup_source {"campaign":"q3"} (object, read-only)│
|
|
8
|
+
// └──────────────────────────────────────────────────────────┘
|
|
9
|
+
//
|
|
10
|
+
// Deviations from source:
|
|
11
|
+
// · Editing is INLINE, not a modal round-trip. A rejected save must keep the
|
|
12
|
+
// editor state so the operator can retry without retyping — there is no
|
|
13
|
+
// editor state to keep once a modal has closed.
|
|
14
|
+
// · Source read `resp.status === 200` off a never-rejecting `model.save()`
|
|
15
|
+
// and toasted "Failed to save metadata" — the trap architecture rule 3
|
|
16
|
+
// retires. `mojoCall` REJECTS; the rejection surfaces the server's own
|
|
17
|
+
// message in a persistent inline banner (a toast alone is missable).
|
|
18
|
+
// · Controlled on `metadata` (architecture rule 5): the component never
|
|
19
|
+
// mirrors server state. `onSaved` is REQUIRED — it is how the owner
|
|
20
|
+
// (TanStack cache, parent state) learns about the write.
|
|
21
|
+
import { useState } from 'react';
|
|
22
|
+
import { mojoCall } from '../../client/client';
|
|
23
|
+
import { modal } from '../modal';
|
|
24
|
+
import { toast } from '../toast';
|
|
25
|
+
|
|
26
|
+
type Draft =
|
|
27
|
+
| { mode: 'add'; key: string; value: string }
|
|
28
|
+
| { mode: 'edit'; key: string; value: string }
|
|
29
|
+
| null;
|
|
30
|
+
|
|
31
|
+
/** Objects/arrays render read-only — a string editor round-trips them lossily. */
|
|
32
|
+
function isBlob(value: unknown): boolean {
|
|
33
|
+
return value !== null && typeof value === 'object';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function display(value: unknown): string {
|
|
37
|
+
return isBlob(value) ? JSON.stringify(value) : String(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Source semantics: a typed value is JSON-parsed when it can be, so `42`,
|
|
42
|
+
* `true` and `{"a":1}` land as their real types and only the rest stay strings.
|
|
43
|
+
*/
|
|
44
|
+
function parseValue(raw: string): unknown {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(raw);
|
|
47
|
+
} catch {
|
|
48
|
+
return raw;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function MetadataSection({ endpoint, id, metadata, onSaved, title = 'Metadata' }: {
|
|
53
|
+
/** Collection endpoint — the save posts to `<endpoint>/<id>`. */
|
|
54
|
+
endpoint: string;
|
|
55
|
+
id: number | string;
|
|
56
|
+
metadata: Record<string, unknown>;
|
|
57
|
+
/** Called with the new blob after the server accepted it. */
|
|
58
|
+
onSaved: (next: Record<string, unknown>) => void;
|
|
59
|
+
title?: string;
|
|
60
|
+
}) {
|
|
61
|
+
const [draft, setDraft] = useState<Draft>(null);
|
|
62
|
+
const [pending, setPending] = useState<string | null>(null);
|
|
63
|
+
const [error, setError] = useState<string | null>(null);
|
|
64
|
+
|
|
65
|
+
const keys = Object.keys(metadata).sort();
|
|
66
|
+
const busy = pending !== null;
|
|
67
|
+
|
|
68
|
+
/** The one write path. Resolves true only when the server accepted it. */
|
|
69
|
+
async function commit(next: Record<string, unknown>, token: string, message: string): Promise<boolean> {
|
|
70
|
+
setPending(token);
|
|
71
|
+
setError(null);
|
|
72
|
+
try {
|
|
73
|
+
await mojoCall(`${endpoint}/${id}`, { method: 'POST', body: { metadata: next } });
|
|
74
|
+
onSaved(next);
|
|
75
|
+
toast.success(message);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// Unmissable failure: the banner persists (a toast would not) and
|
|
79
|
+
// the caller's draft below is deliberately left untouched.
|
|
80
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
81
|
+
return false;
|
|
82
|
+
} finally {
|
|
83
|
+
setPending(null);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function saveDraft() {
|
|
88
|
+
if (!draft) return;
|
|
89
|
+
const key = draft.key.trim();
|
|
90
|
+
if (!key) {
|
|
91
|
+
setError('Key is required.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (draft.mode === 'add' && Object.prototype.hasOwnProperty.call(metadata, key)) {
|
|
95
|
+
setError(`Key "${key}" already exists — edit it instead.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const next = { ...metadata, [key]: parseValue(draft.value) };
|
|
99
|
+
const ok = await commit(next, draft.mode === 'add' ? 'add' : `edit:${key}`, draft.mode === 'add' ? 'Metadata entry added' : 'Metadata updated');
|
|
100
|
+
if (ok) setDraft(null); // a failed save keeps the editor open, populated
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function removeKey(key: string) {
|
|
104
|
+
const confirmed = await modal.confirm({
|
|
105
|
+
title: 'Remove entry',
|
|
106
|
+
message: <>Remove metadata key <b>{key}</b>?</>,
|
|
107
|
+
confirmText: 'Remove',
|
|
108
|
+
danger: true,
|
|
109
|
+
});
|
|
110
|
+
if (!confirmed) return;
|
|
111
|
+
const next = { ...metadata };
|
|
112
|
+
delete next[key];
|
|
113
|
+
await commit(next, `delete:${key}`, 'Metadata entry removed');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const renderEditor = (rowKey: string) => (
|
|
117
|
+
<div className="amd-item amd-editor" key={rowKey}>
|
|
118
|
+
{draft?.mode === 'add' ? (
|
|
119
|
+
<input
|
|
120
|
+
className="input input-compact amd-key-input"
|
|
121
|
+
placeholder="key"
|
|
122
|
+
autoFocus
|
|
123
|
+
value={draft.key}
|
|
124
|
+
onChange={(e) => setDraft({ ...draft, key: e.target.value })}
|
|
125
|
+
onKeyDown={(e) => {
|
|
126
|
+
if (e.key === 'Enter') void saveDraft();
|
|
127
|
+
if (e.key === 'Escape') setDraft(null);
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
) : (
|
|
131
|
+
<div className="amd-key">{draft?.key}</div>
|
|
132
|
+
)}
|
|
133
|
+
<input
|
|
134
|
+
className="input input-compact amd-value-input"
|
|
135
|
+
placeholder='value (JSON parsed when it can be: 42, true, {"a":1})'
|
|
136
|
+
autoFocus={draft?.mode === 'edit'}
|
|
137
|
+
value={draft?.value ?? ''}
|
|
138
|
+
onChange={(e) => draft && setDraft({ ...draft, value: e.target.value })}
|
|
139
|
+
onKeyDown={(e) => {
|
|
140
|
+
if (e.key === 'Enter') void saveDraft();
|
|
141
|
+
if (e.key === 'Escape') setDraft(null);
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
<div className="amd-actions">
|
|
145
|
+
<button type="button" className="btn btn-primary btn-compact" disabled={busy} onClick={() => void saveDraft()}>
|
|
146
|
+
{busy ? <i className="bi bi-arrow-repeat spin" /> : <i className="bi bi-check-lg" />} Save
|
|
147
|
+
</button>
|
|
148
|
+
<button type="button" className="btn btn-compact" disabled={busy} onClick={() => setDraft(null)}>Cancel</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="admin-metadata-section">
|
|
155
|
+
<div className="amd-header">
|
|
156
|
+
<h6 className="amd-title">{title}</h6>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
className="btn btn-primary btn-compact"
|
|
160
|
+
disabled={busy || draft?.mode === 'add'}
|
|
161
|
+
onClick={() => { setError(null); setDraft({ mode: 'add', key: '', value: '' }); }}
|
|
162
|
+
>
|
|
163
|
+
<i className="bi bi-plus-lg" /> Add
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{error && (
|
|
168
|
+
<div className="amd-error" role="alert">
|
|
169
|
+
<i className="bi bi-exclamation-triangle-fill" /> {error}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<div className="amd-list">
|
|
174
|
+
{keys.length === 0 && draft?.mode !== 'add' ? (
|
|
175
|
+
<div className="amd-empty">
|
|
176
|
+
<i className="bi bi-braces" />
|
|
177
|
+
No metadata entries
|
|
178
|
+
</div>
|
|
179
|
+
) : keys.map((key) => (
|
|
180
|
+
draft?.mode === 'edit' && draft.key === key ? (
|
|
181
|
+
renderEditor(key)
|
|
182
|
+
) : (
|
|
183
|
+
<div key={key} className="amd-item">
|
|
184
|
+
<div className="amd-key">{key}</div>
|
|
185
|
+
<div className="amd-value">
|
|
186
|
+
{isBlob(metadata[key])
|
|
187
|
+
? <code className="amd-blob" title="Object values are read-only here — remove and re-add to change them">{display(metadata[key])}</code>
|
|
188
|
+
: display(metadata[key])}
|
|
189
|
+
</div>
|
|
190
|
+
<div className="amd-actions">
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
className="btn-icon btn-icon-sm"
|
|
194
|
+
title={isBlob(metadata[key]) ? 'Object values are read-only' : `Edit ${key}`}
|
|
195
|
+
aria-label={`Edit ${key}`}
|
|
196
|
+
disabled={busy || isBlob(metadata[key])}
|
|
197
|
+
onClick={() => { setError(null); setDraft({ mode: 'edit', key, value: display(metadata[key]) }); }}
|
|
198
|
+
>
|
|
199
|
+
<i className="bi bi-pencil" />
|
|
200
|
+
</button>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
className="btn-icon btn-icon-sm amd-remove"
|
|
204
|
+
title={`Remove ${key}`}
|
|
205
|
+
aria-label={`Remove ${key}`}
|
|
206
|
+
disabled={busy}
|
|
207
|
+
onClick={() => void removeKey(key)}
|
|
208
|
+
>
|
|
209
|
+
{pending === `delete:${key}` ? <i className="bi bi-arrow-repeat spin" /> : <i className="bi bi-trash" />}
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
))}
|
|
215
|
+
{draft?.mode === 'add' && renderEditor('__add__')}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// StackTraceView — colorized stack traces, JavaScript AND Python.
|
|
2
|
+
// Port of web-mojo src/core/views/data/StackTraceView.js.
|
|
3
|
+
//
|
|
4
|
+
// TypeError: Cannot read properties of undefined ← error line
|
|
5
|
+
// at handleSubmit (app/views/JobDetails.js:214:19) ← function + location
|
|
6
|
+
// File "mojo/jobs/runner.py", line 412, in _execute ← python frame
|
|
7
|
+
//
|
|
8
|
+
// Deviations from source:
|
|
9
|
+
// · RE-THEMED. The source shipped a hardcoded light-only <style> block
|
|
10
|
+
// (#f8f9fa surface, #dc3545/#0d6efd/#6610f2 text) that was unreadable in
|
|
11
|
+
// dark mode. This renders semantic classes over theme tokens, correct in
|
|
12
|
+
// BOTH themes.
|
|
13
|
+
// · Long traces COLLAPSE to `collapseAfter` lines behind a toggle instead of
|
|
14
|
+
// only being capped by a scroll box.
|
|
15
|
+
// · The source only colored an exception on line 0, so every Python
|
|
16
|
+
// traceback — which opens with "Traceback (most recent call last):" and
|
|
17
|
+
// puts the exception LAST — rendered its most important line as plain
|
|
18
|
+
// context. An anchored exception-line pattern now catches it anywhere.
|
|
19
|
+
import { useMemo, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
// Ported verbatim from the source, in the source's match order.
|
|
22
|
+
const JS_CALL = /(.+?)\s*\(([^:]+):(\d+):(\d+)\)/; // at fn (file.js:123:45)
|
|
23
|
+
const JS_AT = /^\s*at\s+([^:]+):(\d+):(\d+)/; // at file.js:123:45
|
|
24
|
+
const PY_FRAME = /File\s+"([^"]+)",\s+line\s+(\d+),\s+in\s+(.+)/;
|
|
25
|
+
// Extension: `ValueError: …`, `django.db.utils.IntegrityError: …`, bare
|
|
26
|
+
// `KeyboardInterrupt`. The trailing `(?::|$)` is what keeps ordinary prose
|
|
27
|
+
// ("Error handling request") out of the error style.
|
|
28
|
+
const EXCEPTION = /^[A-Za-z_][\w.]*(?:Error|Exception|Warning|Interrupt|Exit)(?::|$)/;
|
|
29
|
+
|
|
30
|
+
type Line =
|
|
31
|
+
| { kind: 'blank' }
|
|
32
|
+
| { kind: 'error'; text: string }
|
|
33
|
+
| { kind: 'js-call'; indent: string; lead: string; fn: string; file: string; line: string; col: string }
|
|
34
|
+
| { kind: 'js-at'; indent: string; file: string; line: string; col: string }
|
|
35
|
+
| { kind: 'py'; indent: string; file: string; line: string; fn: string }
|
|
36
|
+
| { kind: 'frame'; text: string }
|
|
37
|
+
| { kind: 'context'; text: string };
|
|
38
|
+
|
|
39
|
+
function parseLine(raw: string, index: number): Line {
|
|
40
|
+
if (!raw.trim()) return { kind: 'blank' };
|
|
41
|
+
|
|
42
|
+
// Error message: the source's first-line rule, plus any anchored
|
|
43
|
+
// exception line (where Python puts it).
|
|
44
|
+
const trimmed = raw.trim();
|
|
45
|
+
if ((index === 0 && (raw.includes('Error:') || raw.includes('Exception:'))) || EXCEPTION.test(trimmed)) {
|
|
46
|
+
return { kind: 'error', text: raw };
|
|
47
|
+
}
|
|
48
|
+
// Matched frames are re-composed from their captures, so the original
|
|
49
|
+
// indentation is carried explicitly — it is what keeps a Python frame
|
|
50
|
+
// aligned with the source line under it, terminal-style.
|
|
51
|
+
const indent = raw.match(/^\s*/)![0];
|
|
52
|
+
|
|
53
|
+
const call = raw.match(JS_CALL);
|
|
54
|
+
if (call) {
|
|
55
|
+
// The capture greedily includes the frame keyword ("at handleSubmit").
|
|
56
|
+
// Split it off so only the identifier reads as an identifier.
|
|
57
|
+
const captured = call[1]!.trim();
|
|
58
|
+
const lead = captured.startsWith('at ') ? 'at ' : '';
|
|
59
|
+
return { kind: 'js-call', indent, lead, fn: captured.slice(lead.length), file: call[2]!, line: call[3]!, col: call[4]! };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const at = raw.match(JS_AT);
|
|
63
|
+
if (at) return { kind: 'js-at', indent, file: at[1]!, line: at[2]!, col: at[3]! };
|
|
64
|
+
|
|
65
|
+
const py = raw.match(PY_FRAME);
|
|
66
|
+
if (py) return { kind: 'py', indent, file: py[1]!, line: py[2]!, fn: py[3]! };
|
|
67
|
+
|
|
68
|
+
if (trimmed.startsWith('at ')) return { kind: 'frame', text: raw };
|
|
69
|
+
return { kind: 'context', text: raw };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Non-string traces (an error payload object) are pretty-printed first. */
|
|
73
|
+
function toText(trace: unknown): string {
|
|
74
|
+
if (typeof trace === 'string') return trace;
|
|
75
|
+
if (trace == null) return '';
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(trace, null, 2);
|
|
78
|
+
} catch {
|
|
79
|
+
return String(trace);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function StackTraceView({ trace, collapseAfter = 24, emptyText = 'No stack trace available' }: {
|
|
84
|
+
/** The raw trace; objects are JSON-pretty-printed. */
|
|
85
|
+
trace: unknown;
|
|
86
|
+
/** Lines shown before the "show all" toggle. 0 disables collapsing. */
|
|
87
|
+
collapseAfter?: number;
|
|
88
|
+
emptyText?: string;
|
|
89
|
+
}) {
|
|
90
|
+
const [expanded, setExpanded] = useState(false);
|
|
91
|
+
const text = toText(trace);
|
|
92
|
+
const lines = useMemo(() => text.split('\n').map(parseLine), [text]);
|
|
93
|
+
|
|
94
|
+
if (!text) return <div className="stack-trace-empty dim">{emptyText}</div>;
|
|
95
|
+
|
|
96
|
+
const collapsible = collapseAfter > 0 && lines.length > collapseAfter;
|
|
97
|
+
const shown = collapsible && !expanded ? lines.slice(0, collapseAfter) : lines;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="stack-trace-view">
|
|
101
|
+
<div className="stack-trace-body">
|
|
102
|
+
{shown.map((line, i) => {
|
|
103
|
+
switch (line.kind) {
|
|
104
|
+
case 'blank':
|
|
105
|
+
return <div key={i} className="stack-trace-line"> </div>;
|
|
106
|
+
case 'error':
|
|
107
|
+
return <div key={i} className="stack-trace-line stack-trace-error">{line.text}</div>;
|
|
108
|
+
case 'js-call':
|
|
109
|
+
return (
|
|
110
|
+
<div key={i} className="stack-trace-line stack-trace-frame">
|
|
111
|
+
<span className="stack-trace-loc">{line.indent}{line.lead}</span>
|
|
112
|
+
<span className="stack-trace-fn">{line.fn}</span>
|
|
113
|
+
<span className="stack-trace-loc">
|
|
114
|
+
{' ('}{line.file}:<span className="stack-trace-lineno">{line.line}</span>:{line.col}{')'}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
case 'js-at':
|
|
119
|
+
return (
|
|
120
|
+
<div key={i} className="stack-trace-line stack-trace-frame">
|
|
121
|
+
<span className="stack-trace-loc">
|
|
122
|
+
{line.indent}at {line.file}:<span className="stack-trace-lineno">{line.line}</span>:{line.col}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
case 'py':
|
|
127
|
+
return (
|
|
128
|
+
<div key={i} className="stack-trace-line stack-trace-frame">
|
|
129
|
+
<span className="stack-trace-loc">
|
|
130
|
+
{line.indent}File "{line.file}", line <span className="stack-trace-lineno">{line.line}</span>, in{' '}
|
|
131
|
+
</span>
|
|
132
|
+
<span className="stack-trace-fn">{line.fn}</span>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
case 'frame':
|
|
136
|
+
return <div key={i} className="stack-trace-line stack-trace-frame">{line.text}</div>;
|
|
137
|
+
default:
|
|
138
|
+
return <div key={i} className="stack-trace-line stack-trace-context">{line.text}</div>;
|
|
139
|
+
}
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
{collapsible && (
|
|
143
|
+
<button type="button" className="stack-trace-toggle" onClick={() => setExpanded((v) => !v)}>
|
|
144
|
+
<i className={`bi ${expanded ? 'bi-chevron-up' : 'bi-chevron-down'}`} />
|
|
145
|
+
{expanded ? `Show first ${collapseAfter} lines` : `Show all ${lines.length} lines`}
|
|
146
|
+
</button>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// StatusPanel — the hero "current state" banner that opens a record's
|
|
2
|
+
// Overview section (incident triage, job lifecycle, runner alive/dead).
|
|
3
|
+
// Port of web-mojo src/core/views/data/StatusPanel.js.
|
|
4
|
+
//
|
|
5
|
+
// ┌───────────────────────────────────────────────────────────┐
|
|
6
|
+
// │ ● STATE [ primary ] [ alt ] │
|
|
7
|
+
// │ Headline line │
|
|
8
|
+
// │ Optional supporting meta line · with <code>fragments</code>
|
|
9
|
+
// └───────────────────────────────────────────────────────────┘
|
|
10
|
+
//
|
|
11
|
+
// Deviation from source (architecture rule 6): `meta` was a TRUSTED-HTML
|
|
12
|
+
// string with a "caller must escape" contract, and `actions` was a
|
|
13
|
+
// {label, icon, variant, action} array dispatched through the global action
|
|
14
|
+
// pipeline. Both are ReactNode slots here — the caller composes `<code>` /
|
|
15
|
+
// `<button onClick>` directly, so the escaping contract cannot be broken.
|
|
16
|
+
import type { ReactNode } from 'react';
|
|
17
|
+
import type { Tone } from '../format';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The tone vocabulary shared by the detail primitives. web-mojo's set was
|
|
21
|
+
* {default, primary, success, info, warning, danger, secondary}; portal-mojo's
|
|
22
|
+
* `Tone` folds default/secondary into `muted`, so Badge, StatusPanel, FlowStrip
|
|
23
|
+
* and Timeline all speak one vocabulary.
|
|
24
|
+
*/
|
|
25
|
+
export const DETAIL_TONES = new Set<string>(['success', 'warning', 'danger', 'info', 'muted', 'primary']);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Runtime guard for tones that arrive as wire data (`tone: row.status_tone`)
|
|
29
|
+
* rather than literals. Unknown values fall back to `muted` WITH a warn —
|
|
30
|
+
* never to rendering nothing (architecture rule 4).
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeTone(tone: Tone | string | null | undefined, where: string): Tone {
|
|
33
|
+
if (tone == null || tone === '') return 'muted';
|
|
34
|
+
if (DETAIL_TONES.has(tone)) return tone as Tone;
|
|
35
|
+
console.warn(`[${where}] unknown tone ${JSON.stringify(tone)} — falling back to "muted". Valid: ${[...DETAIL_TONES].join(', ')}`);
|
|
36
|
+
return 'muted';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StatusPanelProps {
|
|
40
|
+
/** Tints the panel surface and colors the state eyebrow + dot. */
|
|
41
|
+
tone?: Tone;
|
|
42
|
+
/** Small uppercase eyebrow beside the dot ("RUNNING", "ACKNOWLEDGED"). */
|
|
43
|
+
state?: string;
|
|
44
|
+
/** The dominant line — what the operator reads first. */
|
|
45
|
+
headline?: ReactNode;
|
|
46
|
+
/** Supporting line; a slot, so `<code>`/`<strong>` compose safely. */
|
|
47
|
+
meta?: ReactNode;
|
|
48
|
+
/** bootstrap-icons name; replaces the tone dot when given. */
|
|
49
|
+
icon?: string;
|
|
50
|
+
/** Buttons row. A slot — the caller owns the handlers. */
|
|
51
|
+
actions?: ReactNode;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function StatusPanel({ tone, state, headline, meta, icon, actions }: StatusPanelProps) {
|
|
55
|
+
const t = normalizeTone(tone, 'StatusPanel');
|
|
56
|
+
return (
|
|
57
|
+
<div className={`detail-status-panel tone-${t}`}>
|
|
58
|
+
<div className="detail-status-headline">
|
|
59
|
+
{state && (
|
|
60
|
+
<div className="detail-status-state">
|
|
61
|
+
{icon
|
|
62
|
+
? <i className={`bi ${icon} detail-status-icon`} />
|
|
63
|
+
: <span className="detail-status-dot" />}
|
|
64
|
+
{state}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
{headline != null && headline !== '' && <div className="detail-status-line">{headline}</div>}
|
|
68
|
+
{meta != null && meta !== '' && <div className="detail-status-meta">{meta}</div>}
|
|
69
|
+
</div>
|
|
70
|
+
{actions && <div className="detail-status-actions">{actions}</div>}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|