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,158 @@
|
|
|
1
|
+
/** Pure identifiers and wire-independent state for the recorded-metrics explorer. */
|
|
2
|
+
|
|
3
|
+
export const METRIC_ACCOUNT_MAX_LENGTH = 256;
|
|
4
|
+
export const METRIC_SEARCH_MAX_LENGTH = 128;
|
|
5
|
+
export const METRIC_DISCOVERY_PAGE_SIZE = 50;
|
|
6
|
+
|
|
7
|
+
export type MetricAccount =
|
|
8
|
+
| { kind: 'public'; value: 'public' }
|
|
9
|
+
| { kind: 'global'; value: 'global' }
|
|
10
|
+
| { kind: 'group'; value: string; id: number }
|
|
11
|
+
| { kind: 'user'; value: string; id: number }
|
|
12
|
+
| { kind: 'custom'; value: string };
|
|
13
|
+
|
|
14
|
+
export interface MetricCategory {
|
|
15
|
+
account: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MetricSeriesSelection {
|
|
20
|
+
account: string;
|
|
21
|
+
category: string | null;
|
|
22
|
+
slugs: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MetricPoint {
|
|
26
|
+
slug: string;
|
|
27
|
+
value: number;
|
|
28
|
+
previous: number;
|
|
29
|
+
delta: number;
|
|
30
|
+
deltaPct?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MetricGaugeValue {
|
|
34
|
+
account: string;
|
|
35
|
+
slug: string;
|
|
36
|
+
value: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type MetricsDiscoveryResource = 'accounts' | 'categories' | 'slugs';
|
|
40
|
+
|
|
41
|
+
export interface MetricsDiscoveryFilters {
|
|
42
|
+
account?: string;
|
|
43
|
+
category?: string | null;
|
|
44
|
+
search: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MetricsDiscoveryRequest {
|
|
48
|
+
resource: MetricsDiscoveryResource;
|
|
49
|
+
account?: string;
|
|
50
|
+
category?: string;
|
|
51
|
+
search?: string;
|
|
52
|
+
start?: number;
|
|
53
|
+
size?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface MetricsDiscoveryPage {
|
|
57
|
+
resource: MetricsDiscoveryResource;
|
|
58
|
+
filters: MetricsDiscoveryFilters;
|
|
59
|
+
data: string[];
|
|
60
|
+
start: number;
|
|
61
|
+
size: number;
|
|
62
|
+
count: number;
|
|
63
|
+
pageCount: number;
|
|
64
|
+
nextStart: number | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface MetricsFanoutSelection {
|
|
68
|
+
mode: 'off' | 'sum' | 'breakdown';
|
|
69
|
+
childKind: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse the public account grammar. Reserved prefixes fail closed instead of
|
|
76
|
+
* becoming custom account names when their positive integer suffix is bad.
|
|
77
|
+
*/
|
|
78
|
+
export function parseMetricAccount(input: string): MetricAccount {
|
|
79
|
+
const value = input.trim();
|
|
80
|
+
if (!value) throw new Error('Metric account is required.');
|
|
81
|
+
if (value.length > METRIC_ACCOUNT_MAX_LENGTH) {
|
|
82
|
+
throw new Error(`Metric account must be ${METRIC_ACCOUNT_MAX_LENGTH} characters or fewer.`);
|
|
83
|
+
}
|
|
84
|
+
if (CONTROL_CHARS.test(value)) throw new Error('Metric account cannot contain control characters.');
|
|
85
|
+
if (value === 'public') return { kind: 'public', value };
|
|
86
|
+
if (value === 'global') return { kind: 'global', value };
|
|
87
|
+
for (const kind of ['group', 'user'] as const) {
|
|
88
|
+
const prefix = `${kind}-`;
|
|
89
|
+
if (!value.startsWith(prefix)) continue;
|
|
90
|
+
const suffix = value.slice(prefix.length);
|
|
91
|
+
if (!/^[1-9]\d*$/.test(suffix) || !Number.isSafeInteger(Number(suffix))) {
|
|
92
|
+
throw new Error(`${kind === 'group' ? 'Group' : 'User'} metric accounts require a positive integer id.`);
|
|
93
|
+
}
|
|
94
|
+
return { kind, value, id: Number(suffix) };
|
|
95
|
+
}
|
|
96
|
+
return { kind: 'custom', value };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Preserve first occurrence and exact spelling; empty selections are ignored. */
|
|
100
|
+
export function dedupeMetricSlugs(slugs: readonly string[]): string[] {
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
const result: string[] = [];
|
|
103
|
+
for (const raw of slugs) {
|
|
104
|
+
const slug = raw;
|
|
105
|
+
if (!slug || seen.has(slug)) continue;
|
|
106
|
+
assertAddressableMetricSlug(slug);
|
|
107
|
+
seen.add(slug);
|
|
108
|
+
result.push(slug);
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function assertAddressableMetricSlug(slug: string): void {
|
|
114
|
+
if (slug.includes(',')) {
|
|
115
|
+
throw new Error(`Metric slug "${slug}" cannot be fetched because the backend uses comma-delimited slug lists.`);
|
|
116
|
+
}
|
|
117
|
+
if (CONTROL_CHARS.test(slug)) throw new Error('Metric slugs cannot contain control characters.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function metricSlugTail(slug: string): string {
|
|
121
|
+
const tail = slug.split(':').at(-1) ?? '';
|
|
122
|
+
if (!tail) throw new Error(`Metric slug "${slug}" has an empty final segment.`);
|
|
123
|
+
return tail;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The live history response loses everything before the last colon. Batch
|
|
128
|
+
* only globally unique tails and isolate each member of a colliding group.
|
|
129
|
+
*/
|
|
130
|
+
export function planMetricHistoryRequests(slugs: readonly string[]): string[][] {
|
|
131
|
+
const ordered = dedupeMetricSlugs(slugs);
|
|
132
|
+
const counts = new Map<string, number>();
|
|
133
|
+
for (const slug of ordered) {
|
|
134
|
+
const tail = metricSlugTail(slug);
|
|
135
|
+
counts.set(tail, (counts.get(tail) ?? 0) + 1);
|
|
136
|
+
}
|
|
137
|
+
const unique = ordered.filter((slug) => counts.get(metricSlugTail(slug)) === 1);
|
|
138
|
+
return [
|
|
139
|
+
...(unique.length ? [unique] : []),
|
|
140
|
+
...ordered.filter((slug) => counts.get(metricSlugTail(slug))! > 1).map((slug) => [slug]),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function metricsDiscoveryKey(callerId: number | string, request: MetricsDiscoveryRequest) {
|
|
145
|
+
return [
|
|
146
|
+
'metrics-discover', callerId, request.resource,
|
|
147
|
+
request.account ?? null, request.category ?? null, request.search ?? '',
|
|
148
|
+
request.start ?? 0, request.size ?? METRIC_DISCOVERY_PAGE_SIZE,
|
|
149
|
+
] as const;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Repeated URL parameters keep full slug boundaries; commas are never flattened. */
|
|
153
|
+
export function replaceMetricSlugParams(params: URLSearchParams, slugs: readonly string[]): URLSearchParams {
|
|
154
|
+
const next = new URLSearchParams(params);
|
|
155
|
+
next.delete('slug');
|
|
156
|
+
for (const slug of dedupeMetricSlugs(slugs)) next.append('slug', slug);
|
|
157
|
+
return next;
|
|
158
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { defineModel, mojoCall, mojoList, type Envelope } from '../../client';
|
|
2
|
+
|
|
3
|
+
/** Exact system-pinned gates from logit.Log.RestMeta.VIEW_PERMS. */
|
|
4
|
+
export const LOGS_ADMIN_PERMISSIONS = [
|
|
5
|
+
'sys.manage_logs',
|
|
6
|
+
'sys.view_logs',
|
|
7
|
+
'sys.security',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
/** Exact system-pinned gate on /api/metrics/permissions. */
|
|
11
|
+
export const METRICS_PERMISSIONS_ADMIN_PERMISSIONS = [
|
|
12
|
+
'sys.manage_incidents',
|
|
13
|
+
'sys.metrics',
|
|
14
|
+
'sys.manage_metrics',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
/** Exact global read clause used by metrics account discovery/history. */
|
|
18
|
+
export const METRICS_EXPLORER_PERMISSIONS = [
|
|
19
|
+
'sys.view_metrics',
|
|
20
|
+
'sys.metrics',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** /api/logs default graph. Stored request, response, and message rows share it. */
|
|
24
|
+
export interface LogRow {
|
|
25
|
+
id: number;
|
|
26
|
+
created: number;
|
|
27
|
+
level: string;
|
|
28
|
+
kind: string | null;
|
|
29
|
+
method: string | null;
|
|
30
|
+
path: string | null;
|
|
31
|
+
payload: string | null;
|
|
32
|
+
ip: string | null;
|
|
33
|
+
duid: string | null;
|
|
34
|
+
uid: number;
|
|
35
|
+
gid: number;
|
|
36
|
+
username: string | null;
|
|
37
|
+
user_agent: string | null;
|
|
38
|
+
log: string | null;
|
|
39
|
+
model_name: string | null;
|
|
40
|
+
model_id: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const LogModel = defineModel<LogRow>({
|
|
44
|
+
name: 'log',
|
|
45
|
+
endpoint: '/api/logs',
|
|
46
|
+
// This package exposes no save/delete controls. RestMeta's save gate is
|
|
47
|
+
// security|admin; the admin wildcard is handled by hasPermission.
|
|
48
|
+
permissions: {
|
|
49
|
+
view: LOGS_ADMIN_PERMISSIONS,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const LOG_LEVEL_OPTIONS = [
|
|
54
|
+
{ value: 'info', label: 'Info' },
|
|
55
|
+
{ value: 'warning', label: 'Warning' },
|
|
56
|
+
{ value: 'error', label: 'Error' },
|
|
57
|
+
{ value: 'critical', label: 'Critical' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export interface MetricsPermissionRow {
|
|
61
|
+
id: string;
|
|
62
|
+
account: string;
|
|
63
|
+
view_permissions: string[];
|
|
64
|
+
write_permissions: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface RawMetricsPermissionRow {
|
|
68
|
+
id?: unknown;
|
|
69
|
+
account?: unknown;
|
|
70
|
+
view_permissions?: unknown;
|
|
71
|
+
write_permissions?: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type MetricsMutationEnvelope = Envelope & RawMetricsPermissionRow & {
|
|
75
|
+
action?: unknown;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const METRICS_PERMISSIONS_ENDPOINT = '/api/metrics/permissions';
|
|
79
|
+
export const metricsPermissionKeys = {
|
|
80
|
+
all: [METRICS_PERMISSIONS_ENDPOINT] as const,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Redis returns a string for one slug, a list for many, and null for none. */
|
|
84
|
+
export function normalizeMetricPermissions(value: unknown): string[] {
|
|
85
|
+
const raw = Array.isArray(value)
|
|
86
|
+
? value
|
|
87
|
+
: typeof value === 'string'
|
|
88
|
+
? value.split(',')
|
|
89
|
+
: [];
|
|
90
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeMetricsPermissionRow(raw: RawMetricsPermissionRow): MetricsPermissionRow {
|
|
94
|
+
const account = typeof raw.account === 'string'
|
|
95
|
+
? raw.account
|
|
96
|
+
: typeof raw.id === 'string'
|
|
97
|
+
? raw.id
|
|
98
|
+
: '';
|
|
99
|
+
return {
|
|
100
|
+
id: account,
|
|
101
|
+
account,
|
|
102
|
+
view_permissions: normalizeMetricPermissions(raw.view_permissions),
|
|
103
|
+
write_permissions: normalizeMetricPermissions(raw.write_permissions),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The list endpoint is a normal `{data, count}` envelope, but Redis account
|
|
109
|
+
* order is undefined. This page deliberately client-sorts the complete list;
|
|
110
|
+
* the endpoint has no real paging/search contract.
|
|
111
|
+
*/
|
|
112
|
+
export async function listMetricsPermissions(): Promise<MetricsPermissionRow[]> {
|
|
113
|
+
const result = await mojoList<RawMetricsPermissionRow>(METRICS_PERMISSIONS_ENDPOINT);
|
|
114
|
+
return result.rows
|
|
115
|
+
.map(normalizeMetricsPermissionRow)
|
|
116
|
+
.filter((row) => row.account !== '')
|
|
117
|
+
.sort((a, b) => a.account.localeCompare(b.account));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SaveMetricsPermissions {
|
|
121
|
+
account: string;
|
|
122
|
+
view_permissions: string[];
|
|
123
|
+
write_permissions: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Mutations are intentionally read from TOP-LEVEL response fields. Unlike the
|
|
128
|
+
* list route, django-mojo does not put this result under `data`.
|
|
129
|
+
*/
|
|
130
|
+
export async function saveMetricsPermissions(input: SaveMetricsPermissions): Promise<MetricsPermissionRow> {
|
|
131
|
+
const response = await mojoCall(
|
|
132
|
+
`${METRICS_PERMISSIONS_ENDPOINT}/${encodeURIComponent(input.account)}`,
|
|
133
|
+
{
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: {
|
|
136
|
+
view_permissions: normalizeMetricPermissions(input.view_permissions).join(','),
|
|
137
|
+
write_permissions: normalizeMetricPermissions(input.write_permissions).join(','),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
) as MetricsMutationEnvelope;
|
|
141
|
+
return normalizeMetricsPermissionRow({
|
|
142
|
+
id: response.id ?? input.account,
|
|
143
|
+
account: response.account ?? input.account,
|
|
144
|
+
view_permissions: response.view_permissions,
|
|
145
|
+
write_permissions: response.write_permissions,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** DELETE clears both Redis permission keys but deliberately retains account. */
|
|
150
|
+
export async function clearMetricsPermissions(account: string): Promise<MetricsPermissionRow> {
|
|
151
|
+
await mojoCall(`${METRICS_PERMISSIONS_ENDPOINT}/${encodeURIComponent(account)}`, {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
});
|
|
154
|
+
return { id: account, account, view_permissions: [], write_permissions: [] };
|
|
155
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Blocked IPs — the `is_blocked=true` projection over the SHIPPED
|
|
2
|
+
// `GeoLocatedIPModel` (#1291 owns `/api/system/geoip`; this page defines no
|
|
3
|
+
// model of its own). Port of web-mojo `admin/security/BlockedIPsTablePage.js`.
|
|
4
|
+
//
|
|
5
|
+
// Corrections carried over the source, each verified against
|
|
6
|
+
// `mojo/apps/account/models/geolocated_ip.py`:
|
|
7
|
+
// · the threat-level filter offered a literal `none` value that matched no
|
|
8
|
+
// row on any deployment (the column is NULL, not 'none'). It is now a
|
|
9
|
+
// multiselect over the four real values plus an explicit isnull filter.
|
|
10
|
+
// · the search placeholder promised "IP, country, or rule". The model's real
|
|
11
|
+
// SEARCH_FIELDS are ip_address, city, country_name, asn_org, isp — there
|
|
12
|
+
// is no rule text on this model at all.
|
|
13
|
+
// · `blocked_until = null` means PERMANENT. The source rendered it through a
|
|
14
|
+
// datetime formatter, i.e. blank, which reads as "unknown".
|
|
15
|
+
// · Enforcement is COMPUTED (`block_active` / `whitelist_active` are
|
|
16
|
+
// properties serialized only on the `basic` graph): a whitelist beats a
|
|
17
|
+
// block and an expired block stops enforcing, while `is_blocked` stays set.
|
|
18
|
+
import {
|
|
19
|
+
Badge, ModelTable, fmt, formModal,
|
|
20
|
+
type BatchAction, type Column, type FilterDef, type Tone,
|
|
21
|
+
} from '../../ui';
|
|
22
|
+
import { useCan } from '../../client';
|
|
23
|
+
import { COUNTRY_OPTIONS } from '../../charts/worldmap/countryCentroids';
|
|
24
|
+
import {
|
|
25
|
+
GEOIP_MANAGE_PERMS, GEOIP_UNBLOCK_FIELDS, GeoLocatedIPModel, blockActive, blockExpired,
|
|
26
|
+
countryFlag, geoIpSafeExporter, threatTone, whitelistActive, whitelistExpired,
|
|
27
|
+
type GeoLocatedIPRow,
|
|
28
|
+
} from '../security/geoip';
|
|
29
|
+
import { NETWORK_WHITELIST_FIELDS, showBlockedIpDetail } from './BlockedIpDetail';
|
|
30
|
+
|
|
31
|
+
/** THREAT_LEVEL_ORDER minus the null slot — the four values the column holds. */
|
|
32
|
+
const REAL_THREAT_LEVELS = [
|
|
33
|
+
{ value: 'low', label: 'Low' },
|
|
34
|
+
{ value: 'medium', label: 'Medium' },
|
|
35
|
+
{ value: 'high', label: 'High' },
|
|
36
|
+
{ value: 'critical', label: 'Critical' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const COLUMNS: Column<GeoLocatedIPRow>[] = [
|
|
40
|
+
{
|
|
41
|
+
key: 'ip_address', label: 'IP address', sortable: true, hideable: false,
|
|
42
|
+
render: (row) => <code>{row.ip_address}</code>,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
// The honest state, not the raw flag.
|
|
46
|
+
key: 'is_blocked', label: 'State', sortable: true,
|
|
47
|
+
render: (row) => {
|
|
48
|
+
if (blockActive(row)) return <Badge tone="danger"><i className="bi bi-slash-circle" /> Blocked</Badge>;
|
|
49
|
+
if (whitelistActive(row)) return <Badge tone="info"><i className="bi bi-shield-check" /> Whitelisted</Badge>;
|
|
50
|
+
if (blockExpired(row)) return <Badge tone="warning">Block expired</Badge>;
|
|
51
|
+
if (whitelistExpired(row)) return <Badge tone="warning">Whitelist expired</Badge>;
|
|
52
|
+
return <span className="dim">—</span>;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: 'threat_level', label: 'Threat', sortable: true,
|
|
57
|
+
render: (row) => <Badge tone={threatTone(row.threat_level) as Tone}>{row.threat_level || 'none'}</Badge>,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'country_code', label: 'Country', sortable: true,
|
|
61
|
+
render: (row) => {
|
|
62
|
+
if (!row.country_code) return <span className="dim">—</span>;
|
|
63
|
+
const flag = countryFlag(row.country_code);
|
|
64
|
+
return <>{flag && <span className="geoip-flag">{flag}</span>}{row.country_code}</>;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{ key: 'city', label: 'City', sortable: true, render: (row) => row.city || <span className="dim">—</span> },
|
|
68
|
+
{
|
|
69
|
+
key: 'blocked_reason', label: 'Reason',
|
|
70
|
+
render: (row) => row.blocked_reason
|
|
71
|
+
? <span title={row.blocked_reason}>{fmt.truncate(row.blocked_reason, 44)}</span>
|
|
72
|
+
: <span className="dim">—</span>,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'blocked_at', label: 'Blocked at', sortable: true,
|
|
76
|
+
render: (row) => row.blocked_at == null
|
|
77
|
+
? <span className="dim">—</span>
|
|
78
|
+
: <span title={fmt.datetime(row.blocked_at)}>{fmt.relative(row.blocked_at)}</span>,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
// null = permanent. Rendering it as "" (a datetime formatter over
|
|
82
|
+
// null) was the source's most misleading cell.
|
|
83
|
+
key: 'blocked_until', label: 'Expires', sortable: true,
|
|
84
|
+
render: (row) => row.blocked_until == null
|
|
85
|
+
? <b>Never</b>
|
|
86
|
+
: <span title={fmt.datetime(row.blocked_until)}>{fmt.relative(row.blocked_until)}</span>,
|
|
87
|
+
},
|
|
88
|
+
{ key: 'block_count', label: 'Blocks', sortable: true, align: 'end', render: (row) => row.block_count },
|
|
89
|
+
{
|
|
90
|
+
key: 'is_whitelisted', label: 'Whitelisted', align: 'center',
|
|
91
|
+
render: (row) => <Badge tone={whitelistActive(row) ? 'info' : 'muted'}>{whitelistActive(row) ? 'Yes' : row.is_whitelisted ? 'Expired' : 'No'}</Badge>,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const FILTERS: FilterDef[] = [
|
|
96
|
+
{ key: 'threat_level', label: 'Threat level', type: 'multiselect', options: REAL_THREAT_LEVELS },
|
|
97
|
+
{ key: 'threat_level__isnull', label: 'Threat level unset', type: 'boolean', trueLabel: 'No threat level', falseLabel: 'Has a threat level' },
|
|
98
|
+
{ key: 'country_code', label: 'Country', type: 'select', options: [...COUNTRY_OPTIONS] },
|
|
99
|
+
{ key: 'city', label: 'City contains', type: 'text' },
|
|
100
|
+
{ key: 'asn_org', label: 'ASN org contains', type: 'text' },
|
|
101
|
+
{ key: 'isp', label: 'ISP contains', type: 'text' },
|
|
102
|
+
{ key: 'is_whitelisted', label: 'Whitelisted', type: 'boolean' },
|
|
103
|
+
{ key: 'blocked_until__isnull', label: 'Permanent', type: 'boolean', trueLabel: 'Never expires', falseLabel: 'Has an expiry' },
|
|
104
|
+
{ key: 'blocked_at', label: 'Blocked at', type: 'daterange' },
|
|
105
|
+
{ key: 'blocked_until', label: 'Expires', type: 'daterange' },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export function BlockedIPsPage() {
|
|
109
|
+
const canManage = useCan(GEOIP_MANAGE_PERMS).can;
|
|
110
|
+
const unblock = GeoLocatedIPModel.useAction('unblock');
|
|
111
|
+
const whitelist = GeoLocatedIPModel.useAction('whitelist');
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Both batch actions are RELAXING. `prepare` collects the reason ONCE per
|
|
115
|
+
* batch so the audit trail carries a real sentence rather than a
|
|
116
|
+
* hard-coded "Bulk … from admin" on every row (the source's behaviour).
|
|
117
|
+
*/
|
|
118
|
+
const batchActions: BatchAction<GeoLocatedIPRow>[] = [
|
|
119
|
+
{
|
|
120
|
+
key: 'unblock',
|
|
121
|
+
label: 'Unblock',
|
|
122
|
+
icon: 'bi-unlock',
|
|
123
|
+
eligible: (row) => row.is_blocked,
|
|
124
|
+
confirm: 'Unblock the selected addresses? Traffic from them is allowed fleet-wide again.',
|
|
125
|
+
prepare: async (rows) => {
|
|
126
|
+
const data = await formModal({
|
|
127
|
+
title: `Unblock ${rows.length} address${rows.length === 1 ? '' : 'es'}`,
|
|
128
|
+
fields: GEOIP_UNBLOCK_FIELDS,
|
|
129
|
+
submitText: 'Unblock',
|
|
130
|
+
initial: { reason: 'Bulk unblock from admin' },
|
|
131
|
+
});
|
|
132
|
+
return data ? String(data.reason) : null;
|
|
133
|
+
},
|
|
134
|
+
// `on_action_unblock` takes a STRING reason (a non-string falls
|
|
135
|
+
// back to "manual unblock: by <user>").
|
|
136
|
+
run: (row, prepared) => unblock.mutateAsync({ id: row.id, payload: String(prepared) }),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: 'whitelist',
|
|
140
|
+
label: 'Whitelist',
|
|
141
|
+
icon: 'bi-shield-check',
|
|
142
|
+
// A whitelist clears an active block, so an already-whitelisted
|
|
143
|
+
// row has nothing to gain and would only reset its expiry.
|
|
144
|
+
eligible: (row) => !whitelistActive(row),
|
|
145
|
+
confirm: 'Whitelist the selected addresses? They will pass every rule, including the VPN and Tor checks.',
|
|
146
|
+
prepare: async (rows) => {
|
|
147
|
+
const data = await formModal({
|
|
148
|
+
title: `Whitelist ${rows.length} address${rows.length === 1 ? '' : 'es'}`,
|
|
149
|
+
fields: NETWORK_WHITELIST_FIELDS,
|
|
150
|
+
submitText: 'Whitelist',
|
|
151
|
+
intro: 'One reason and expiry are recorded against every selected address.',
|
|
152
|
+
});
|
|
153
|
+
if (!data) return null;
|
|
154
|
+
const until = typeof data.until === 'string' && data.until ? data.until : null;
|
|
155
|
+
// The DICT form — the source sent a bare string, so the audit
|
|
156
|
+
// trail got a reason but `whitelisted_until` was never set.
|
|
157
|
+
return until ? { reason: String(data.reason), until } : { reason: String(data.reason) };
|
|
158
|
+
},
|
|
159
|
+
run: (row, prepared) => whitelist.mutateAsync({ id: row.id, payload: prepared }),
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<ModelTable<GeoLocatedIPRow>
|
|
165
|
+
model={GeoLocatedIPModel}
|
|
166
|
+
eyebrow="Security · Network"
|
|
167
|
+
title="Blocked IPs"
|
|
168
|
+
searchPlaceholder="Search IP, city, country, ASN org, or ISP"
|
|
169
|
+
columns={COLUMNS}
|
|
170
|
+
filters={FILTERS}
|
|
171
|
+
presets={[
|
|
172
|
+
{ key: 'all', label: 'All blocks', params: {} },
|
|
173
|
+
{ key: 'permanent', label: 'Permanent', params: { blocked_until__isnull: 'true' } },
|
|
174
|
+
{ key: 'whitelisted', label: 'Whitelisted', params: { is_whitelisted: 'true' } },
|
|
175
|
+
]}
|
|
176
|
+
// `is_blocked` is the projection; `dr_field` names the column a
|
|
177
|
+
// daterange lands on so a picked window filters the BLOCK, not
|
|
178
|
+
// last_seen. No window is preset: a Blocked IPs page whose default
|
|
179
|
+
// view hides every permanent block older than a week answers the
|
|
180
|
+
// wrong question (the source defaulted to 7d).
|
|
181
|
+
defaultParams={{ is_blocked: 'true', dr_field: 'blocked_at' }}
|
|
182
|
+
defaultSort="-modified"
|
|
183
|
+
selectable={canManage}
|
|
184
|
+
batchActions={canManage ? batchActions : []}
|
|
185
|
+
columnChooser
|
|
186
|
+
persistState
|
|
187
|
+
persistKey="admin:network:blocked-ips"
|
|
188
|
+
exporter={geoIpSafeExporter}
|
|
189
|
+
exportFormats={['csv', 'json']}
|
|
190
|
+
rowTone={(row) => blockActive(row) ? 'danger' : blockExpired(row) ? 'warning' : null}
|
|
191
|
+
onRowClick={(row) => showBlockedIpDetail(row.id)}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
}
|