@raybase/core 0.0.2 → 0.3.3
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 +21 -0
- package/dist/.admin-build/AdminApp.css +2 -0
- package/dist/.admin-build/AdminApp.js +77 -0
- package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
- package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
- package/dist/actions.d.ts +347 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +661 -0
- package/dist/actions.js.map +1 -0
- package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
- package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
- package/dist/admin-boot.d.ts +94 -0
- package/dist/admin-boot.d.ts.map +1 -0
- package/dist/admin-boot.js +139 -0
- package/dist/admin-boot.js.map +1 -0
- package/dist/admin-docs-content.d.ts +95 -0
- package/dist/admin-docs-content.d.ts.map +1 -0
- package/dist/admin-docs-content.js +463 -0
- package/dist/admin-docs-content.js.map +1 -0
- package/dist/admin-docs-ui.d.ts +11 -0
- package/dist/admin-docs-ui.d.ts.map +1 -0
- package/dist/admin-docs-ui.js +23 -0
- package/dist/admin-docs-ui.js.map +1 -0
- package/dist/admin-kit.d.ts +29 -0
- package/dist/admin-kit.js +1557 -0
- package/dist/admin-role.d.ts +8 -0
- package/dist/admin-role.d.ts.map +1 -0
- package/dist/admin-role.js +22 -0
- package/dist/admin-role.js.map +1 -0
- package/dist/admin-ui.d.ts +3 -0
- package/dist/admin-ui.d.ts.map +1 -0
- package/dist/admin-ui.js +16 -0
- package/dist/admin-ui.js.map +1 -0
- package/dist/agent-ai-chat.d.ts +127 -0
- package/dist/agent-ai-chat.d.ts.map +1 -0
- package/dist/agent-ai-chat.js +5975 -0
- package/dist/agent-ai-chat.js.map +1 -0
- package/dist/agent-chapter-archive.d.ts +214 -0
- package/dist/agent-chapter-archive.d.ts.map +1 -0
- package/dist/agent-chapter-archive.js +696 -0
- package/dist/agent-chapter-archive.js.map +1 -0
- package/dist/agent-chapter-search.d.ts +116 -0
- package/dist/agent-chapter-search.d.ts.map +1 -0
- package/dist/agent-chapter-search.js +605 -0
- package/dist/agent-chapter-search.js.map +1 -0
- package/dist/agent-chapter-tools.d.ts +45 -0
- package/dist/agent-chapter-tools.d.ts.map +1 -0
- package/dist/agent-chapter-tools.js +344 -0
- package/dist/agent-chapter-tools.js.map +1 -0
- package/dist/agent-chapters.d.ts +228 -0
- package/dist/agent-chapters.d.ts.map +1 -0
- package/dist/agent-chapters.js +737 -0
- package/dist/agent-chapters.js.map +1 -0
- package/dist/agent-data-parts.d.ts +167 -0
- package/dist/agent-data-parts.d.ts.map +1 -0
- package/dist/agent-data-parts.js +68 -0
- package/dist/agent-data-parts.js.map +1 -0
- package/dist/agent-errors.d.ts +41 -0
- package/dist/agent-errors.d.ts.map +1 -0
- package/dist/agent-errors.js +191 -0
- package/dist/agent-errors.js.map +1 -0
- package/dist/agent-follow-up-store.d.ts +273 -0
- package/dist/agent-follow-up-store.d.ts.map +1 -0
- package/dist/agent-follow-up-store.js +703 -0
- package/dist/agent-follow-up-store.js.map +1 -0
- package/dist/agent-follow-ups.d.ts +314 -0
- package/dist/agent-follow-ups.d.ts.map +1 -0
- package/dist/agent-follow-ups.js +840 -0
- package/dist/agent-follow-ups.js.map +1 -0
- package/dist/agent-memory.d.ts +323 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +1270 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/agent-message-parts.d.ts +58 -0
- package/dist/agent-message-parts.d.ts.map +1 -0
- package/dist/agent-message-parts.js +163 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-remote-mcp-credentials.d.ts +154 -0
- package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
- package/dist/agent-remote-mcp-credentials.js +287 -0
- package/dist/agent-remote-mcp-credentials.js.map +1 -0
- package/dist/agent-remote-mcp-crypto.d.ts +32 -0
- package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
- package/dist/agent-remote-mcp-crypto.js +101 -0
- package/dist/agent-remote-mcp-crypto.js.map +1 -0
- package/dist/agent-remote-mcp-install.d.ts +299 -0
- package/dist/agent-remote-mcp-install.d.ts.map +1 -0
- package/dist/agent-remote-mcp-install.js +785 -0
- package/dist/agent-remote-mcp-install.js.map +1 -0
- package/dist/agent-remote-mcp-oauth.d.ts +73 -0
- package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
- package/dist/agent-remote-mcp-oauth.js +344 -0
- package/dist/agent-remote-mcp-oauth.js.map +1 -0
- package/dist/agent-remote-mcp-transport.d.ts +46 -0
- package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
- package/dist/agent-remote-mcp-transport.js +225 -0
- package/dist/agent-remote-mcp-transport.js.map +1 -0
- package/dist/agent-remote-mcp.d.ts +147 -0
- package/dist/agent-remote-mcp.d.ts.map +1 -0
- package/dist/agent-remote-mcp.js +681 -0
- package/dist/agent-remote-mcp.js.map +1 -0
- package/dist/agent-room-redaction.d.ts +66 -0
- package/dist/agent-room-redaction.d.ts.map +1 -0
- package/dist/agent-room-redaction.js +264 -0
- package/dist/agent-room-redaction.js.map +1 -0
- package/dist/agent-rooms.d.ts +209 -0
- package/dist/agent-rooms.d.ts.map +1 -0
- package/dist/agent-rooms.js +244 -0
- package/dist/agent-rooms.js.map +1 -0
- package/dist/agent-runtime.d.ts +541 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +4260 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent-skill-archive.d.ts +20 -0
- package/dist/agent-skill-archive.d.ts.map +1 -0
- package/dist/agent-skill-archive.js +316 -0
- package/dist/agent-skill-archive.js.map +1 -0
- package/dist/agent-skill-discovery.d.ts +96 -0
- package/dist/agent-skill-discovery.d.ts.map +1 -0
- package/dist/agent-skill-discovery.js +464 -0
- package/dist/agent-skill-discovery.js.map +1 -0
- package/dist/agent-skill-format.d.ts +57 -0
- package/dist/agent-skill-format.d.ts.map +1 -0
- package/dist/agent-skill-format.js +238 -0
- package/dist/agent-skill-format.js.map +1 -0
- package/dist/agent-skill-import.d.ts +64 -0
- package/dist/agent-skill-import.d.ts.map +1 -0
- package/dist/agent-skill-import.js +295 -0
- package/dist/agent-skill-import.js.map +1 -0
- package/dist/agent-skill-publish.d.ts +51 -0
- package/dist/agent-skill-publish.d.ts.map +1 -0
- package/dist/agent-skill-publish.js +136 -0
- package/dist/agent-skill-publish.js.map +1 -0
- package/dist/agent-skill-store.d.ts +252 -0
- package/dist/agent-skill-store.d.ts.map +1 -0
- package/dist/agent-skill-store.js +514 -0
- package/dist/agent-skill-store.js.map +1 -0
- package/dist/agent-skills.d.ts +85 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +291 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-transcript-pager.d.ts +77 -0
- package/dist/agent-transcript-pager.d.ts.map +1 -0
- package/dist/agent-transcript-pager.js +210 -0
- package/dist/agent-transcript-pager.js.map +1 -0
- package/dist/agent-user-data-parts.d.ts +20 -0
- package/dist/agent-user-data-parts.d.ts.map +1 -0
- package/dist/agent-user-data-parts.js +161 -0
- package/dist/agent-user-data-parts.js.map +1 -0
- package/dist/agent-user-transcript.d.ts +46 -0
- package/dist/agent-user-transcript.d.ts.map +1 -0
- package/dist/agent-user-transcript.js +106 -0
- package/dist/agent-user-transcript.js.map +1 -0
- package/dist/agent-voice-notes.d.ts +15 -0
- package/dist/agent-voice-notes.d.ts.map +1 -0
- package/dist/agent-voice-notes.js +17 -0
- package/dist/agent-voice-notes.js.map +1 -0
- package/dist/agent-wakes.d.ts +213 -0
- package/dist/agent-wakes.d.ts.map +1 -0
- package/dist/agent-wakes.js +534 -0
- package/dist/agent-wakes.js.map +1 -0
- package/dist/agent.d.ts +1780 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +3123 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai.d.ts +583 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1858 -0
- package/dist/ai.js.map +1 -0
- package/dist/app-manifest.d.ts +255 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +372 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/app-users.d.ts +30 -0
- package/dist/app-users.d.ts.map +1 -0
- package/dist/app-users.js +91 -0
- package/dist/app-users.js.map +1 -0
- package/dist/audit-archive.d.ts +85 -0
- package/dist/audit-archive.d.ts.map +1 -0
- package/dist/audit-archive.js +178 -0
- package/dist/audit-archive.js.map +1 -0
- package/dist/audit.d.ts +54 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +95 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth-logger.d.ts +28 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +53 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-password-secret.d.ts +59 -0
- package/dist/auth-password-secret.d.ts.map +1 -0
- package/dist/auth-password-secret.js +409 -0
- package/dist/auth-password-secret.js.map +1 -0
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +285 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/auth.d.ts +321 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +1562 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup-archive.d.ts +257 -0
- package/dist/backup-archive.d.ts.map +1 -0
- package/dist/backup-archive.js +2068 -0
- package/dist/backup-archive.js.map +1 -0
- package/dist/backup-crypto.d.ts +313 -0
- package/dist/backup-crypto.d.ts.map +1 -0
- package/dist/backup-crypto.js +693 -0
- package/dist/backup-crypto.js.map +1 -0
- package/dist/backup-format.d.ts +200 -0
- package/dist/backup-format.d.ts.map +1 -0
- package/dist/backup-format.js +1247 -0
- package/dist/backup-format.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +35 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +83 -0
- package/dist/clock.js.map +1 -0
- package/dist/data-client.d.ts +187 -0
- package/dist/data-client.d.ts.map +1 -0
- package/dist/data-client.js +590 -0
- package/dist/data-client.js.map +1 -0
- package/dist/data-secrets.d.ts +164 -0
- package/dist/data-secrets.d.ts.map +1 -0
- package/dist/data-secrets.js +383 -0
- package/dist/data-secrets.js.map +1 -0
- package/dist/db.d.ts +453 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +324 -0
- package/dist/db.js.map +1 -0
- package/dist/dev-activity.d.ts +48 -0
- package/dist/dev-activity.d.ts.map +1 -0
- package/dist/dev-activity.js +50 -0
- package/dist/dev-activity.js.map +1 -0
- package/dist/do.d.ts +429 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +4526 -0
- package/dist/do.js.map +1 -0
- package/dist/email.d.ts +137 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +527 -0
- package/dist/email.js.map +1 -0
- package/dist/generated/admin-assets.d.ts +16 -0
- package/dist/generated/admin-assets.d.ts.map +1 -0
- package/dist/generated/admin-assets.js +16 -0
- package/dist/generated/admin-assets.js.map +1 -0
- package/dist/generated/admin-docs-assets.d.ts +3 -0
- package/dist/generated/admin-docs-assets.d.ts.map +1 -0
- package/dist/generated/admin-docs-assets.js +3 -0
- package/dist/generated/admin-docs-assets.js.map +1 -0
- package/dist/handoff-consumer.d.ts +79 -0
- package/dist/handoff-consumer.d.ts.map +1 -0
- package/dist/handoff-consumer.js +300 -0
- package/dist/handoff-consumer.js.map +1 -0
- package/dist/handoff.d.ts +70 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +228 -0
- package/dist/handoff.js.map +1 -0
- package/dist/host-auth.d.ts +47 -0
- package/dist/host-auth.d.ts.map +1 -0
- package/dist/host-auth.js +59 -0
- package/dist/host-auth.js.map +1 -0
- package/dist/host-user.d.ts +15 -0
- package/dist/host-user.d.ts.map +1 -0
- package/dist/host-user.js +23 -0
- package/dist/host-user.js.map +1 -0
- package/dist/html.d.ts +35 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +634 -0
- package/dist/html.js.map +1 -0
- package/dist/http-error.d.ts +13 -0
- package/dist/http-error.d.ts.map +1 -0
- package/dist/http-error.js +15 -0
- package/dist/http-error.js.map +1 -0
- package/dist/imports.d.ts +219 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +571 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-tape.d.ts +64 -0
- package/dist/inference-tape.d.ts.map +1 -0
- package/dist/inference-tape.js +278 -0
- package/dist/inference-tape.js.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/jobs.d.ts +796 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +538 -0
- package/dist/jobs.js.map +1 -0
- package/dist/json-schema.d.ts +91 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +884 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/knowledge.d.ts +33 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +348 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/local-dev-chrome.d.ts +29 -0
- package/dist/local-dev-chrome.d.ts.map +1 -0
- package/dist/local-dev-chrome.js +514 -0
- package/dist/local-dev-chrome.js.map +1 -0
- package/dist/local-dev-personas.d.ts +15 -0
- package/dist/local-dev-personas.d.ts.map +1 -0
- package/dist/local-dev-personas.js +46 -0
- package/dist/local-dev-personas.js.map +1 -0
- package/dist/mcp.d.ts +567 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2480 -0
- package/dist/mcp.js.map +1 -0
- package/dist/modules.d.ts +145 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +164 -0
- package/dist/modules.js.map +1 -0
- package/dist/notification-client.d.ts +94 -0
- package/dist/notification-client.d.ts.map +1 -0
- package/dist/notification-client.js +127 -0
- package/dist/notification-client.js.map +1 -0
- package/dist/notifications.d.ts +447 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +1625 -0
- package/dist/notifications.js.map +1 -0
- package/dist/operator-data-query.d.ts +117 -0
- package/dist/operator-data-query.d.ts.map +1 -0
- package/dist/operator-data-query.js +431 -0
- package/dist/operator-data-query.js.map +1 -0
- package/dist/operator-mcp.d.ts +63 -0
- package/dist/operator-mcp.d.ts.map +1 -0
- package/dist/operator-mcp.js +2828 -0
- package/dist/operator-mcp.js.map +1 -0
- package/dist/outbound-https.d.ts +24 -0
- package/dist/outbound-https.d.ts.map +1 -0
- package/dist/outbound-https.js +172 -0
- package/dist/outbound-https.js.map +1 -0
- package/dist/pitr.d.ts +60 -0
- package/dist/pitr.d.ts.map +1 -0
- package/dist/pitr.js +94 -0
- package/dist/pitr.js.map +1 -0
- package/dist/platform.d.ts +199 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +196 -0
- package/dist/platform.js.map +1 -0
- package/dist/policies.d.ts +115 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +158 -0
- package/dist/policies.js.map +1 -0
- package/dist/principal.d.ts +143 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +174 -0
- package/dist/principal.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +578 -0
- package/dist/realtime.js.map +1 -0
- package/dist/retrieval.d.ts +263 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +602 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-error-client.d.ts +10 -0
- package/dist/runtime-error-client.d.ts.map +1 -0
- package/dist/runtime-error-client.js +70 -0
- package/dist/runtime-error-client.js.map +1 -0
- package/dist/runtime-errors.d.ts +180 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +572 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/schema.d.ts +434 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +232 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +216 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +785 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +191 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +478 -0
- package/dist/settings.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +30 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +469 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +82 -0
- package/dist/storage.js.map +1 -0
- package/dist/subject-domains.d.ts +15 -0
- package/dist/subject-domains.d.ts.map +1 -0
- package/dist/subject-domains.js +38 -0
- package/dist/subject-domains.js.map +1 -0
- package/dist/support.d.ts +132 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +483 -0
- package/dist/support.js.map +1 -0
- package/dist/system-tables.d.ts +12 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +225 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/test.d.ts +57 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +454 -0
- package/dist/test.js.map +1 -0
- package/dist/vite.d.ts +82 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +82 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpush-crypto.d.ts +39 -0
- package/dist/webpush-crypto.d.ts.map +1 -0
- package/dist/webpush-crypto.js +213 -0
- package/dist/webpush-crypto.js.map +1 -0
- package/dist/webpush-private.d.ts +11 -0
- package/dist/webpush-private.d.ts.map +1 -0
- package/dist/webpush-private.js +11 -0
- package/dist/webpush-private.js.map +1 -0
- package/dist/webpush.d.ts +86 -0
- package/dist/webpush.d.ts.map +1 -0
- package/dist/webpush.js +375 -0
- package/dist/webpush.js.map +1 -0
- package/dist/worker.d.ts +185 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +13021 -0
- package/dist/worker.js.map +1 -0
- package/package.json +161 -8
- package/README.md +0 -3
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
// admin-src/BackupPanel.tsx
|
|
2
|
+
import React2, { useEffect, useMemo, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// admin-src/lib.ts
|
|
5
|
+
import { collectTagSuggestions, formatDate, formatDateTime, isNullValue, isPrimitiveArray, jsonColumnMode, parseStoredDate, parseTypedDate, toIsoDate } from "@raybase/datasheet-grid";
|
|
6
|
+
var boot = JSON.parse(document.getElementById("raybase-boot")?.textContent ?? "{}");
|
|
7
|
+
async function api(url, options = {}) {
|
|
8
|
+
const res = await fetch(url, {
|
|
9
|
+
...options,
|
|
10
|
+
headers: { "Content-Type": "application/json", ...options.headers || {} }
|
|
11
|
+
});
|
|
12
|
+
const data = await res.json().catch(() => ({}));
|
|
13
|
+
if (!res.ok) throw new Error(data.error || "Request failed");
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
function rowKey(row) {
|
|
17
|
+
return typeof row.id === "string" ? row.id : "";
|
|
18
|
+
}
|
|
19
|
+
function equalRows(a, b) {
|
|
20
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
21
|
+
}
|
|
22
|
+
function equalRowSets(a, b) {
|
|
23
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// admin-src/ui.tsx
|
|
27
|
+
import React from "react";
|
|
28
|
+
function cx(...classes) {
|
|
29
|
+
return classes.filter(Boolean).join(" ");
|
|
30
|
+
}
|
|
31
|
+
var buttonBase = "inline-flex h-9 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-lg px-3.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/70 disabled:pointer-events-none disabled:opacity-40";
|
|
32
|
+
var primaryButton = `${buttonBase} bg-zinc-950 text-white hover:bg-zinc-800`;
|
|
33
|
+
var secondaryButton = `${buttonBase} border border-zinc-200 bg-white text-zinc-900 hover:border-zinc-300 hover:bg-zinc-50`;
|
|
34
|
+
var dangerButton = `${buttonBase} border border-zinc-300 bg-white text-zinc-900 hover:border-zinc-900 hover:bg-zinc-900 hover:text-white [&>svg]:text-zinc-500 [&>svg]:transition-colors [&:hover>svg]:text-white`;
|
|
35
|
+
var destructiveButton = `${buttonBase} bg-red-600 text-white hover:bg-red-700`;
|
|
36
|
+
var fieldBase = "h-9 rounded-lg border border-zinc-200 bg-white px-3 text-sm text-zinc-900 transition-colors placeholder:text-zinc-400 hover:border-zinc-300 focus:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-400/70";
|
|
37
|
+
var fieldClass = `${fieldBase} w-full`;
|
|
38
|
+
var selectBase = "rb-chevron h-9 appearance-none rounded-lg border border-zinc-200 bg-white pl-3 pr-9 text-sm text-zinc-900 transition-colors hover:border-zinc-300 focus:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-400/70";
|
|
39
|
+
var selectClass = `${selectBase} w-full`;
|
|
40
|
+
var labelClass = "grid gap-1.5 text-[13px] font-medium text-zinc-600";
|
|
41
|
+
var toolbarClass = "mb-6 flex flex-wrap items-center justify-between gap-3";
|
|
42
|
+
var actionRowClass = "flex flex-wrap items-center gap-2";
|
|
43
|
+
var cardClass = "rounded-xl border border-zinc-200 bg-white";
|
|
44
|
+
var tableWrapClass = `mt-4 overflow-auto ${cardClass}`;
|
|
45
|
+
var thClass = "sticky top-0 z-10 border-b border-zinc-200 bg-zinc-50/80 px-4 py-2.5 text-left text-[11px] font-medium uppercase tracking-wider text-zinc-500 backdrop-blur";
|
|
46
|
+
var tdClass = "border-b border-zinc-100 px-4 py-2.5 text-left align-middle text-sm text-zinc-900";
|
|
47
|
+
var chipShadow = "shadow-[0_1px_2px_0_rgb(9_9_11/0.05)]";
|
|
48
|
+
var iconButton = "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/70 disabled:pointer-events-none disabled:opacity-30";
|
|
49
|
+
var stroke = {
|
|
50
|
+
fill: "none",
|
|
51
|
+
stroke: "currentColor",
|
|
52
|
+
strokeWidth: 1.8,
|
|
53
|
+
strokeLinecap: "round",
|
|
54
|
+
strokeLinejoin: "round",
|
|
55
|
+
"aria-hidden": true
|
|
56
|
+
};
|
|
57
|
+
var icons = {
|
|
58
|
+
data: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("ellipse", { cx: "12", cy: "5", rx: "9", ry: "3" }), /* @__PURE__ */ React.createElement("path", { d: "M3 5v14a9 3 0 0 0 18 0V5" }), /* @__PURE__ */ React.createElement("path", { d: "M3 12a9 3 0 0 0 18 0" })),
|
|
59
|
+
file: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), /* @__PURE__ */ React.createElement("path", { d: "M14 2v6h6" }), /* @__PURE__ */ React.createElement("path", { d: "M8 13h8" }), /* @__PURE__ */ React.createElement("path", { d: "M8 17h5" })),
|
|
60
|
+
schema: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }), /* @__PURE__ */ React.createElement("path", { d: "M3 9h18" }), /* @__PURE__ */ React.createElement("path", { d: "M8 9v11" }), /* @__PURE__ */ React.createElement("path", { d: "M13 9v11" }), /* @__PURE__ */ React.createElement("path", { d: "M17 13h4" }), /* @__PURE__ */ React.createElement("path", { d: "M17 17h4" })),
|
|
61
|
+
users: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }), /* @__PURE__ */ React.createElement("circle", { cx: "9", cy: "7", r: "4" }), /* @__PURE__ */ React.createElement("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }), /* @__PURE__ */ React.createElement("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })),
|
|
62
|
+
app: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" }), /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "3" })),
|
|
63
|
+
bell: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9" }), /* @__PURE__ */ React.createElement("path", { d: "M10 21h4" })),
|
|
64
|
+
signOut: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }), /* @__PURE__ */ React.createElement("polyline", { points: "16 17 21 12 16 7" }), /* @__PURE__ */ React.createElement("line", { x1: "21", x2: "9", y1: "12", y2: "12" })),
|
|
65
|
+
sun: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "4" }), /* @__PURE__ */ React.createElement("path", { d: "M12 2v2" }), /* @__PURE__ */ React.createElement("path", { d: "M12 20v2" }), /* @__PURE__ */ React.createElement("path", { d: "m4.93 4.93 1.41 1.41" }), /* @__PURE__ */ React.createElement("path", { d: "m17.66 17.66 1.41 1.41" }), /* @__PURE__ */ React.createElement("path", { d: "M2 12h2" }), /* @__PURE__ */ React.createElement("path", { d: "M20 12h2" }), /* @__PURE__ */ React.createElement("path", { d: "m6.34 17.66-1.41 1.41" }), /* @__PURE__ */ React.createElement("path", { d: "m19.07 4.93-1.41 1.41" })),
|
|
66
|
+
moon: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M12 3a6 6 0 0 0 9 7.7A9 9 0 1 1 12 3z" })),
|
|
67
|
+
trash: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M3 6h18" }), /* @__PURE__ */ React.createElement("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }), /* @__PURE__ */ React.createElement("line", { x1: "10", x2: "10", y1: "11", y2: "17" }), /* @__PURE__ */ React.createElement("line", { x1: "14", x2: "14", y1: "11", y2: "17" })),
|
|
68
|
+
check: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M21.801 10A10 10 0 1 1 17 3.335" }), /* @__PURE__ */ React.createElement("path", { d: "m9 11 3 3L22 4" })),
|
|
69
|
+
checkmark: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke, strokeWidth: 2.2 }, /* @__PURE__ */ React.createElement("path", { d: "M20 6 9 17l-5-5" })),
|
|
70
|
+
alert: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "10" }), /* @__PURE__ */ React.createElement("line", { x1: "12", x2: "12", y1: "8", y2: "12" }), /* @__PURE__ */ React.createElement("line", { x1: "12", x2: "12.01", y1: "16", y2: "16" })),
|
|
71
|
+
plus: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M5 12h14" }), /* @__PURE__ */ React.createElement("path", { d: "M12 5v14" })),
|
|
72
|
+
search: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("circle", { cx: "11", cy: "11", r: "8" }), /* @__PURE__ */ React.createElement("path", { d: "m21 21-4.3-4.3" })),
|
|
73
|
+
filter: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M22 3H2l8 9.46V19l4 2v-8.54L22 3z" })),
|
|
74
|
+
close: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M18 6 6 18" }), /* @__PURE__ */ React.createElement("path", { d: "m6 6 12 12" })),
|
|
75
|
+
closeSmall: /* @__PURE__ */ React.createElement("svg", { width: "11", height: "11", viewBox: "0 0 24 24", ...stroke, strokeWidth: 2.4 }, /* @__PURE__ */ React.createElement("path", { d: "M18 6 6 18" }), /* @__PURE__ */ React.createElement("path", { d: "m6 6 12 12" })),
|
|
76
|
+
chevronLeft: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "m15 18-6-6 6-6" })),
|
|
77
|
+
chevronRight: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "m9 18 6-6-6-6" })),
|
|
78
|
+
chevronUp: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "m18 15-6-6-6 6" })),
|
|
79
|
+
chevronDown: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "m6 9 6 6 6-6" })),
|
|
80
|
+
expand: /* @__PURE__ */ React.createElement("svg", { width: "13", height: "13", viewBox: "0 0 24 24", ...stroke, strokeWidth: 2 }, /* @__PURE__ */ React.createElement("path", { d: "M15 3h6v6" }), /* @__PURE__ */ React.createElement("path", { d: "M9 21H3v-6" }), /* @__PURE__ */ React.createElement("path", { d: "M21 3l-7 7" }), /* @__PURE__ */ React.createElement("path", { d: "M3 21l7-7" })),
|
|
81
|
+
edit: /* @__PURE__ */ React.createElement("svg", { width: "13", height: "13", viewBox: "0 0 24 24", ...stroke, strokeWidth: 2 }, /* @__PURE__ */ React.createElement("path", { d: "M12 20h9" }), /* @__PURE__ */ React.createElement("path", { d: "M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" })),
|
|
82
|
+
key: /* @__PURE__ */ React.createElement("svg", { width: "11", height: "11", viewBox: "0 0 24 24", ...stroke, strokeWidth: 2 }, /* @__PURE__ */ React.createElement("path", { d: "M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" }), /* @__PURE__ */ React.createElement("circle", { cx: "16.5", cy: "7.5", r: ".5", fill: "currentColor" })),
|
|
83
|
+
link: /* @__PURE__ */ React.createElement("svg", { width: "11", height: "11", viewBox: "0 0 24 24", ...stroke, strokeWidth: 2 }, /* @__PURE__ */ React.createElement("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), /* @__PURE__ */ React.createElement("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" })),
|
|
84
|
+
refresh: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8" }), /* @__PURE__ */ React.createElement("path", { d: "M21 3v5h-5" })),
|
|
85
|
+
table: /* @__PURE__ */ React.createElement("svg", { width: "15", height: "15", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18" })),
|
|
86
|
+
calendar: /* @__PURE__ */ React.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }), /* @__PURE__ */ React.createElement("path", { d: "M16 2v4" }), /* @__PURE__ */ React.createElement("path", { d: "M8 2v4" }), /* @__PURE__ */ React.createElement("path", { d: "M3 10h18" })),
|
|
87
|
+
history: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M3 12a9 9 0 1 0 3-6.7" }), /* @__PURE__ */ React.createElement("path", { d: "M3 4v5h5" }), /* @__PURE__ */ React.createElement("path", { d: "M12 7v5l3 2" })),
|
|
88
|
+
org: /* @__PURE__ */ React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", ...stroke }, /* @__PURE__ */ React.createElement("path", { d: "M3 21h18" }), /* @__PURE__ */ React.createElement("path", { d: "M5 21V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v16" }), /* @__PURE__ */ React.createElement("path", { d: "M17 9h2a2 2 0 0 1 2 2v10" }), /* @__PURE__ */ React.createElement("path", { d: "M9 7h3" }), /* @__PURE__ */ React.createElement("path", { d: "M9 11h3" }), /* @__PURE__ */ React.createElement("path", { d: "M9 15h3" }))
|
|
89
|
+
};
|
|
90
|
+
function TypeGlyph({ type, size = 14 }) {
|
|
91
|
+
const shared = {
|
|
92
|
+
width: size,
|
|
93
|
+
height: size,
|
|
94
|
+
viewBox: "0 0 24 24",
|
|
95
|
+
...stroke
|
|
96
|
+
};
|
|
97
|
+
const normalized = type.toUpperCase();
|
|
98
|
+
if (normalized === "INTEGER" || normalized === "NUMERIC") {
|
|
99
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("line", { x1: "4", y1: "9", x2: "20", y2: "9" }), /* @__PURE__ */ React.createElement("line", { x1: "4", y1: "15", x2: "20", y2: "15" }), /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "3", x2: "8", y2: "21" }), /* @__PURE__ */ React.createElement("line", { x1: "16", y1: "3", x2: "14", y2: "21" }));
|
|
100
|
+
}
|
|
101
|
+
if (normalized === "REAL") {
|
|
102
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("line", { x1: "19", y1: "5", x2: "5", y2: "19" }), /* @__PURE__ */ React.createElement("circle", { cx: "6.5", cy: "6.5", r: "2.5" }), /* @__PURE__ */ React.createElement("circle", { cx: "17.5", cy: "17.5", r: "2.5" }));
|
|
103
|
+
}
|
|
104
|
+
if (normalized === "BLOB") {
|
|
105
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("rect", { x: "14", y: "14", width: "4", height: "6", rx: "2" }), /* @__PURE__ */ React.createElement("rect", { x: "6", y: "4", width: "4", height: "6", rx: "2" }), /* @__PURE__ */ React.createElement("path", { d: "M6 20h4" }), /* @__PURE__ */ React.createElement("path", { d: "M14 10h4" }), /* @__PURE__ */ React.createElement("path", { d: "M6 14h2v6" }), /* @__PURE__ */ React.createElement("path", { d: "M14 4h2v6" }));
|
|
106
|
+
}
|
|
107
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("polyline", { points: "4 7 4 4 20 4 20 7" }), /* @__PURE__ */ React.createElement("line", { x1: "9", y1: "20", x2: "15", y2: "20" }), /* @__PURE__ */ React.createElement("line", { x1: "12", y1: "4", x2: "12", y2: "20" }));
|
|
108
|
+
}
|
|
109
|
+
function KindGlyph({ kind, hasEnum, size = 13 }) {
|
|
110
|
+
const shared = { width: size, height: size, viewBox: "0 0 24 24", ...stroke, strokeWidth: 2 };
|
|
111
|
+
if (hasEnum) {
|
|
112
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "9" }), /* @__PURE__ */ React.createElement("path", { d: "m9 12 2 2 4-4" }));
|
|
113
|
+
}
|
|
114
|
+
switch (kind) {
|
|
115
|
+
case "id":
|
|
116
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("path", { d: "M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" }), /* @__PURE__ */ React.createElement("circle", { cx: "16.5", cy: "7.5", r: ".5", fill: "currentColor" }));
|
|
117
|
+
case "integer":
|
|
118
|
+
case "real":
|
|
119
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("line", { x1: "4", y1: "9", x2: "20", y2: "9" }), /* @__PURE__ */ React.createElement("line", { x1: "4", y1: "15", x2: "20", y2: "15" }), /* @__PURE__ */ React.createElement("line", { x1: "10", y1: "3", x2: "8", y2: "21" }), /* @__PURE__ */ React.createElement("line", { x1: "16", y1: "3", x2: "14", y2: "21" }));
|
|
120
|
+
case "boolean":
|
|
121
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("rect", { x: "3", y: "5", width: "18", height: "14", rx: "7" }), /* @__PURE__ */ React.createElement("circle", { cx: "16", cy: "12", r: "3" }));
|
|
122
|
+
case "date":
|
|
123
|
+
case "datetime":
|
|
124
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }), /* @__PURE__ */ React.createElement("path", { d: "M16 2v4" }), /* @__PURE__ */ React.createElement("path", { d: "M8 2v4" }), /* @__PURE__ */ React.createElement("path", { d: "M3 10h18" }));
|
|
125
|
+
case "json":
|
|
126
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("path", { d: "M9 7H7a2 2 0 0 0-2 2v1.5a1.5 1.5 0 0 1-1.5 1.5A1.5 1.5 0 0 1 5 13.5V15a2 2 0 0 0 2 2h2" }), /* @__PURE__ */ React.createElement("path", { d: "M15 7h2a2 2 0 0 1 2 2v1.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0-1.5 1.5V15a2 2 0 0 1-2 2h-2" }));
|
|
127
|
+
case "ref":
|
|
128
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("path", { d: "M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" }), /* @__PURE__ */ React.createElement("path", { d: "M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" }));
|
|
129
|
+
case "secret":
|
|
130
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("rect", { x: "4", y: "11", width: "16", height: "10", rx: "2" }), /* @__PURE__ */ React.createElement("path", { d: "M8 11V7a4 4 0 0 1 8 0v4" }));
|
|
131
|
+
case "longText":
|
|
132
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("path", { d: "M4 6h16" }), /* @__PURE__ */ React.createElement("path", { d: "M4 12h16" }), /* @__PURE__ */ React.createElement("path", { d: "M4 18h10" }));
|
|
133
|
+
default:
|
|
134
|
+
return /* @__PURE__ */ React.createElement("svg", { ...shared }, /* @__PURE__ */ React.createElement("path", { d: "M4 20 12 4l8 16" }), /* @__PURE__ */ React.createElement("path", { d: "M7 14h10" }));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// admin-src/BackupPanel.tsx
|
|
139
|
+
function sizeLabel(bytes) {
|
|
140
|
+
if (bytes === null || bytes === void 0) return "Unknown";
|
|
141
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
142
|
+
const units = ["KB", "MB", "GB"];
|
|
143
|
+
let value = bytes / 1024;
|
|
144
|
+
let unit = units[0];
|
|
145
|
+
for (let index = 1; index < units.length && value >= 1024; index += 1) {
|
|
146
|
+
value /= 1024;
|
|
147
|
+
unit = units[index];
|
|
148
|
+
}
|
|
149
|
+
return `${new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value)} ${unit}`;
|
|
150
|
+
}
|
|
151
|
+
function backupQuery(target, organizationId, extra = {}) {
|
|
152
|
+
const params = new URLSearchParams(extra);
|
|
153
|
+
if (target) params.set("target", target);
|
|
154
|
+
if (target === "app-data" && organizationId) params.set("organizationId", organizationId);
|
|
155
|
+
return params.toString();
|
|
156
|
+
}
|
|
157
|
+
function confirmRestore(label, expected) {
|
|
158
|
+
return window.prompt(`${label}
|
|
159
|
+
|
|
160
|
+
Type ${expected} to confirm.`)?.trim().toLowerCase() === expected;
|
|
161
|
+
}
|
|
162
|
+
function BackupPanel({
|
|
163
|
+
notify,
|
|
164
|
+
environment,
|
|
165
|
+
target,
|
|
166
|
+
organizationId,
|
|
167
|
+
organizationName,
|
|
168
|
+
description,
|
|
169
|
+
archiveMode = "plaintext-sql"
|
|
170
|
+
}) {
|
|
171
|
+
const [status, setStatus] = useState(null);
|
|
172
|
+
const [list, setList] = useState({ entries: [] });
|
|
173
|
+
const [busy, setBusy] = useState(null);
|
|
174
|
+
const [uploadFile, setUploadFile] = useState(null);
|
|
175
|
+
const query = useMemo(() => backupQuery(target, organizationId), [target, organizationId]);
|
|
176
|
+
const querySuffix = query ? `?${query}` : "";
|
|
177
|
+
const targetLabel = target === "control-plane" ? "Control plane" : target === "app-data" ? "App data" : null;
|
|
178
|
+
const subject = target === "app-data" && organizationName ? organizationName : targetLabel?.toLowerCase() ?? "the database";
|
|
179
|
+
const confirmation = target === "control-plane" ? "restore control plane" : "restore";
|
|
180
|
+
const encryptedArchives = archiveMode === "encrypted-v2";
|
|
181
|
+
const requestTarget = () => ({
|
|
182
|
+
...target ? { target } : {},
|
|
183
|
+
...target === "app-data" && organizationId ? { organizationId } : {}
|
|
184
|
+
});
|
|
185
|
+
const encryptedUploadUnavailable = Boolean(
|
|
186
|
+
encryptedArchives && uploadFile && !uploadFile.name.toLowerCase().endsWith(".sql") && (status?.restore?.pitrAvailable === false || status?.restore?.maintenance?.active)
|
|
187
|
+
);
|
|
188
|
+
const load = async () => {
|
|
189
|
+
const [nextStatus, nextList] = await Promise.all([
|
|
190
|
+
api(`/api/admin/backups/status${querySuffix}`),
|
|
191
|
+
api(`/api/admin/backups${querySuffix}`).catch((error) => ({ entries: [], warning: error.message }))
|
|
192
|
+
]);
|
|
193
|
+
setStatus(nextStatus);
|
|
194
|
+
setList(nextList);
|
|
195
|
+
};
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
void load().catch((error) => notify(error.message, "error"));
|
|
198
|
+
}, [query]);
|
|
199
|
+
const createBackup = async () => {
|
|
200
|
+
setBusy("create");
|
|
201
|
+
try {
|
|
202
|
+
await api("/api/admin/backups", { method: "POST", body: JSON.stringify(requestTarget()) });
|
|
203
|
+
notify(targetLabel ? `${targetLabel} backup created.` : "Backup created.");
|
|
204
|
+
await load();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
notify(error instanceof Error ? error.message : "Backup failed", "error");
|
|
207
|
+
} finally {
|
|
208
|
+
setBusy(null);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const restoreFromR2 = async (entry) => {
|
|
212
|
+
const warning = target ? `Restore ${entry.key}? This replaces ${subject} from the selected snapshot.` : `Restore ${entry.key}? Users, sessions, audit, app rows, and system tables move together.`;
|
|
213
|
+
if (!confirmRestore(warning, confirmation)) return;
|
|
214
|
+
setBusy(entry.key);
|
|
215
|
+
try {
|
|
216
|
+
await api("/api/admin/backups/restore", {
|
|
217
|
+
method: "POST",
|
|
218
|
+
body: JSON.stringify({ ...requestTarget(), key: entry.key, confirm: confirmation })
|
|
219
|
+
});
|
|
220
|
+
notify(
|
|
221
|
+
target === "control-plane" ? "Control-plane restore complete. You may need to sign in again." : target === "app-data" ? "App data restore complete." : "Restore complete. You may need to sign in again if your session changed."
|
|
222
|
+
);
|
|
223
|
+
await load();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
notify(error instanceof Error ? error.message : "Restore failed", "error");
|
|
226
|
+
} finally {
|
|
227
|
+
setBusy(null);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const restoreUpload = async () => {
|
|
231
|
+
if (!uploadFile) return;
|
|
232
|
+
const warning = target ? `Restore ${uploadFile.name} for ${subject}? This replaces the selected database state.` : `Restore ${uploadFile.name}? Users, sessions, audit, app rows, and system tables move together.`;
|
|
233
|
+
if (!confirmRestore(warning, confirmation)) return;
|
|
234
|
+
setBusy("upload");
|
|
235
|
+
try {
|
|
236
|
+
const restoreQuery = backupQuery(target, organizationId, { confirm: confirmation });
|
|
237
|
+
const response = await fetch(`/api/admin/backups/restore-upload?${restoreQuery}`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: {
|
|
240
|
+
"Content-Type": encryptedArchives && !uploadFile.name.toLowerCase().endsWith(".sql") ? "application/vnd.raybase.backup" : "application/sql"
|
|
241
|
+
},
|
|
242
|
+
body: uploadFile
|
|
243
|
+
});
|
|
244
|
+
const result = await response.json().catch(() => ({}));
|
|
245
|
+
if (!response.ok) throw new Error(result.error || "We couldn't restore that backup.");
|
|
246
|
+
notify(
|
|
247
|
+
target === "control-plane" ? "Uploaded control-plane backup restored. You may need to sign in again." : target === "app-data" ? "Uploaded app data backup restored." : "Uploaded backup restored. You may need to sign in again if your session changed."
|
|
248
|
+
);
|
|
249
|
+
setUploadFile(null);
|
|
250
|
+
await load();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
notify(error instanceof Error ? error.message : "Restore failed", "error");
|
|
253
|
+
} finally {
|
|
254
|
+
setBusy(null);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
return /* @__PURE__ */ React2.createElement("div", { className: cx(cardClass, "max-w-2xl overflow-hidden") }, /* @__PURE__ */ React2.createElement("div", { className: "border-b border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React2.createElement("h2", { className: "m-0 flex items-center gap-2 text-sm font-semibold" }, /* @__PURE__ */ React2.createElement("span", { className: "text-zinc-500" }, target === "control-plane" ? icons.app : icons.data), targetLabel ? `${targetLabel}${encryptedArchives ? " encrypted" : ""} backups` : encryptedArchives ? "Encrypted R2 backups" : "R2 SQL backups"), /* @__PURE__ */ React2.createElement("p", { className: "m-0 mt-1 text-sm text-zinc-500" }, description || (encryptedArchives ? "Envelope-encrypted SQLite snapshots stored in the configured backups bucket." : "Plaintext SQL snapshots stored in the configured backups bucket.")), environment === "local" ? /* @__PURE__ */ React2.createElement("p", { className: "m-0 mt-3 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs leading-5 text-zinc-600" }, "Automated backups run in production when enabled.") : null), status ? /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement("dl", { className: "m-0 divide-y divide-zinc-100" }, [
|
|
258
|
+
["Status", status.enabled ? "Enabled" : "Disabled"],
|
|
259
|
+
["R2 binding", status.bindingPresent ? "Present" : "Missing"],
|
|
260
|
+
...encryptedArchives ? [
|
|
261
|
+
["Encryption", status.encryptionReady === false ? "Not ready" : status.encryptionReady ? "Ready" : "Unknown"],
|
|
262
|
+
["Provider", status.provider?.id || "Not reported"],
|
|
263
|
+
["Key version", status.provider?.keyId || (status.provider?.mode === "wrapping" ? "Provider-managed" : "Not reported")],
|
|
264
|
+
["Consistency", status.consistencyPolicy === "write-gated" ? "Write-gated" : status.consistencyPolicy === "optimistic" ? "Optimistic retries" : "Not reported"],
|
|
265
|
+
["PITR restore safety", status.restore?.pitrAvailable === false ? "Unavailable" : status.restore?.pitrAvailable ? "Available" : "Not reported"],
|
|
266
|
+
["Restore maintenance", status.restore?.maintenance?.active ? `Active (${status.restore.maintenance.phase || "unknown"})` : "Inactive"]
|
|
267
|
+
] : [],
|
|
268
|
+
...encryptedArchives && status.provider?.passwordParticipants ? [[
|
|
269
|
+
"Password participants",
|
|
270
|
+
status.provider.passwordParticipants.declared ? `${status.provider.passwordParticipants.ready ?? 0}/${status.provider.passwordParticipants.required ?? 0} ready` : "Not declared"
|
|
271
|
+
]] : [],
|
|
272
|
+
["Retention", `${status.retention} per target`],
|
|
273
|
+
["Latest", status.latestBackup ? formatDateTime(status.latestBackup.date) : "None"]
|
|
274
|
+
].map(([label, value]) => /* @__PURE__ */ React2.createElement("div", { key: label, className: "grid grid-cols-[160px_minmax(0,1fr)] items-center gap-4 px-5 py-3.5 text-sm" }, /* @__PURE__ */ React2.createElement("dt", { className: "font-medium text-zinc-500" }, label), /* @__PURE__ */ React2.createElement("dd", { className: "m-0 text-zinc-900" }, value)))), encryptedArchives && status.provider?.passwordCoupled ? /* @__PURE__ */ React2.createElement("div", { className: "border-t border-amber-200 bg-amber-50 px-5 py-4 text-xs leading-relaxed text-amber-900" }, "This provider depends on password-secret participants. Passwordless users may be ineligible, and a password reset or lost password can permanently remove access to older backups.") : null, encryptedArchives && (status.restore?.pitrAvailable === false || status.restore?.maintenance?.active) ? /* @__PURE__ */ React2.createElement("div", { className: "border-t border-amber-200 bg-amber-50 px-5 py-4 text-xs leading-relaxed text-amber-900" }, status.restore.maintenance?.active ? `Restore maintenance is active (${status.restore.maintenance.phase || "unknown"}). Encrypted restores remain blocked until rollback recovery completes.` : status.restore.pitrUnavailableReason || "Encrypted restore requires production PITR recovery support.") : null, /* @__PURE__ */ React2.createElement("div", { className: "border-t border-zinc-100 px-5 py-4 text-xs leading-relaxed text-zinc-500" }, status.caveats.join(" "), status.warning ? /* @__PURE__ */ React2.createElement("div", { className: "mt-1 text-amber-700" }, status.warning) : null), /* @__PURE__ */ React2.createElement("div", { className: "border-t border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React2.createElement("div", { className: "flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React2.createElement("button", { className: secondaryButton, disabled: !status.bindingPresent || !status.enabled || encryptedArchives && status.encryptionReady === false || busy === "create", onClick: () => void createBackup() }, icons.data, "Create backup now"), /* @__PURE__ */ React2.createElement("button", { className: secondaryButton, onClick: () => void load().catch((error) => notify(error.message, "error")) }, icons.refresh, "Refresh"))), /* @__PURE__ */ React2.createElement("div", { className: "border-t border-zinc-100" }, list.entries.length ? /* @__PURE__ */ React2.createElement("div", { className: "divide-y divide-zinc-100" }, list.entries.map((entry) => /* @__PURE__ */ React2.createElement("div", { key: entry.key, className: "grid gap-3 px-5 py-3.5 text-sm md:grid-cols-[minmax(0,1fr)_auto] md:items-center" }, /* @__PURE__ */ React2.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React2.createElement("div", { className: "font-medium text-zinc-900" }, formatDateTime(entry.date)), /* @__PURE__ */ React2.createElement("div", { className: "truncate font-mono text-[11px] text-zinc-500", title: entry.key }, entry.key), /* @__PURE__ */ React2.createElement("div", { className: "mt-1 text-xs text-zinc-400" }, sizeLabel(entry.bytes), " \xB7 ", entry.trigger, " \xB7 ", encryptedArchives ? entry.encrypted === false ? "Legacy plaintext SQL" : entry.encrypted ? "Encrypted format 2" : "Legacy entry" : "Plaintext SQL"), entry.providerId ? /* @__PURE__ */ React2.createElement("div", { className: "mt-1 truncate font-mono text-[11px] text-zinc-400", title: `${entry.providerId}/${entry.kekKeyId || "unknown"}` }, entry.providerId, " \xB7 ", entry.kekKeyId || "provider-managed key") : null, entry.restoreUnavailableReason ? /* @__PURE__ */ React2.createElement("div", { className: cx("mt-1 text-xs", entry.restoreAvailable === false ? "text-amber-700" : "text-zinc-500") }, entry.restoreUnavailableReason) : null), /* @__PURE__ */ React2.createElement("div", { className: "flex flex-wrap gap-2" }, /* @__PURE__ */ React2.createElement("a", { className: secondaryButton, href: `/api/admin/backups/download?${backupQuery(target, organizationId, { key: entry.key })}` }, "Download"), /* @__PURE__ */ React2.createElement("button", { className: dangerButton, disabled: busy === entry.key || entry.restoreAvailable === false, onClick: () => void restoreFromR2(entry) }, "Restore"))))) : /* @__PURE__ */ React2.createElement("div", { className: "px-5 py-4 text-sm text-zinc-500" }, list.warning || (encryptedArchives ? "No backups yet. Create one after encryption is ready." : "No backups yet."))), /* @__PURE__ */ React2.createElement("div", { className: "border-t border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React2.createElement("label", { className: labelClass }, "Restore uploaded backup", /* @__PURE__ */ React2.createElement(
|
|
275
|
+
"input",
|
|
276
|
+
{
|
|
277
|
+
type: "file",
|
|
278
|
+
accept: encryptedArchives ? ".rbb,.sql,application/vnd.raybase.backup,application/sql" : ".sql,application/sql",
|
|
279
|
+
className: cx(fieldBase, "w-full max-w-md"),
|
|
280
|
+
onChange: (event) => {
|
|
281
|
+
const file = event.target.files?.[0];
|
|
282
|
+
if (file && !encryptedArchives && !file.name.toLowerCase().endsWith(".sql")) {
|
|
283
|
+
setUploadFile(null);
|
|
284
|
+
event.currentTarget.value = "";
|
|
285
|
+
notify("This backup service accepts plaintext .sql files only.", "error");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
setUploadFile(file ?? null);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
)), uploadFile ? /* @__PURE__ */ React2.createElement("div", { className: "mt-3 rounded-md border border-zinc-200 bg-zinc-50 px-3 py-2 text-xs text-zinc-600" }, /* @__PURE__ */ React2.createElement("div", { className: "font-medium text-zinc-900" }, uploadFile.name), /* @__PURE__ */ React2.createElement("div", { className: "mt-1" }, sizeLabel(uploadFile.size), " \xB7 ", encryptedArchives ? uploadFile.name.toLowerCase().endsWith(".sql") ? "Legacy plaintext SQL" : "Encrypted format 2" : "Plaintext SQL")) : null, /* @__PURE__ */ React2.createElement("p", { className: "m-0 mt-3 text-xs leading-relaxed text-zinc-500" }, encryptedArchives ? "Encrypted .rbb archives are verified before database changes begin. Legacy .sql restores remain available with compatibility size and statement limits." : "Upload a plaintext format-1 .sql backup for restore."), /* @__PURE__ */ React2.createElement("div", { className: "mt-3 flex justify-end" }, /* @__PURE__ */ React2.createElement("button", { className: dangerButton, disabled: !uploadFile || encryptedUploadUnavailable || busy === "upload", onClick: () => void restoreUpload() }, "Restore uploaded backup")))) : /* @__PURE__ */ React2.createElement("div", { className: "space-y-3 px-5 py-4" }, /* @__PURE__ */ React2.createElement("div", { className: "h-4 w-2/3 animate-pulse rounded bg-zinc-100" }), /* @__PURE__ */ React2.createElement("div", { className: "h-4 w-1/2 animate-pulse rounded bg-zinc-100" })));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// admin-src/DataView.tsx
|
|
295
|
+
import React4, { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
|
|
296
|
+
import { buildGridColumns, collectTagSuggestions as collectTagSuggestions2, compileFilters, FilterBar, isNullValue as isNullValue2, jsonColumnMode as jsonColumnMode2, RecordPanel, toIsoDate as toIsoDate2 } from "@raybase/datasheet-grid";
|
|
297
|
+
import { DynamicDataSheetGrid as DynamicDataSheetGridBase, renderContextMenuItem } from "@raybase/datasheet-grid/engine";
|
|
298
|
+
|
|
299
|
+
// admin-src/imports/CsvImportDialog.tsx
|
|
300
|
+
import React3, { useEffect as useEffect2, useMemo as useMemo2, useRef, useState as useState2 } from "react";
|
|
301
|
+
import { Importer, ImporterField } from "react-csv-importer";
|
|
302
|
+
|
|
303
|
+
// admin-src/imports/importApi.ts
|
|
304
|
+
var maxImportRows = 2e3;
|
|
305
|
+
function importPath(path, adminOrgId) {
|
|
306
|
+
if (!adminOrgId) return path;
|
|
307
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
308
|
+
return `${path}${separator}adminOrgId=${encodeURIComponent(adminOrgId)}`;
|
|
309
|
+
}
|
|
310
|
+
async function sha256File(file) {
|
|
311
|
+
if (!crypto.subtle) return null;
|
|
312
|
+
const digest = await crypto.subtle.digest("SHA-256", await file.arrayBuffer());
|
|
313
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
314
|
+
}
|
|
315
|
+
async function previewImport(request, adminOrgId) {
|
|
316
|
+
return api(importPath("/api/admin/imports/preview", adminOrgId), {
|
|
317
|
+
method: "POST",
|
|
318
|
+
body: JSON.stringify(request)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async function commitImport(runId, rows, adminOrgId) {
|
|
322
|
+
return api(importPath(`/api/admin/imports/${encodeURIComponent(runId)}/commit`, adminOrgId), {
|
|
323
|
+
method: "POST",
|
|
324
|
+
body: JSON.stringify({ rows })
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function revertImport(runId, adminOrgId) {
|
|
328
|
+
return api(importPath(`/api/admin/imports/${encodeURIComponent(runId)}/revert`, adminOrgId), {
|
|
329
|
+
method: "POST",
|
|
330
|
+
body: "{}"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async function listImportHistory(table, adminOrgId) {
|
|
334
|
+
const params = new URLSearchParams({ table, limit: "10" });
|
|
335
|
+
if (adminOrgId) params.set("adminOrgId", adminOrgId);
|
|
336
|
+
const result = await api(`/api/admin/imports?${params}`);
|
|
337
|
+
return result.runs;
|
|
338
|
+
}
|
|
339
|
+
async function getImportDetail(runId, adminOrgId) {
|
|
340
|
+
const result = await api(importPath(`/api/admin/imports/${encodeURIComponent(runId)}`, adminOrgId));
|
|
341
|
+
return result.run;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// admin-src/imports/CsvImportDialog.tsx
|
|
345
|
+
var CsvImporter = Importer;
|
|
346
|
+
var CsvImporterField = ImporterField;
|
|
347
|
+
function importableColumns(table) {
|
|
348
|
+
return Object.entries(table.columns).filter(([, column]) => !column.primaryKey && !column.auto);
|
|
349
|
+
}
|
|
350
|
+
function columnHelp(key, column, references) {
|
|
351
|
+
const parts = [column.kind];
|
|
352
|
+
if (column.enumValues?.length) parts.push(`one of ${column.enumValues.join(", ")}`);
|
|
353
|
+
if (column.references) {
|
|
354
|
+
const loaded = references[key]?.length;
|
|
355
|
+
parts.push(`references ${column.references}${loaded ? `, ${loaded} loaded` : ""}`);
|
|
356
|
+
}
|
|
357
|
+
return parts.join(" \xB7 ");
|
|
358
|
+
}
|
|
359
|
+
function countLabel(value, noun) {
|
|
360
|
+
return `${value.toLocaleString()} ${noun}${value === 1 ? "" : "s"}`;
|
|
361
|
+
}
|
|
362
|
+
function fileMapping(info) {
|
|
363
|
+
const mapping = {};
|
|
364
|
+
info.columnFields.forEach((field, index) => {
|
|
365
|
+
if (!field) return;
|
|
366
|
+
const column = info.preview.columns[index];
|
|
367
|
+
mapping[column?.header || `Column ${index + 1}`] = field;
|
|
368
|
+
});
|
|
369
|
+
return mapping;
|
|
370
|
+
}
|
|
371
|
+
function statusClass(status) {
|
|
372
|
+
if (status === "succeeded" || status === "reverted") return "border-emerald-200 bg-emerald-50 text-emerald-700";
|
|
373
|
+
if (status === "failed" || status === "partially_failed") return "border-red-200 bg-red-50 text-red-700";
|
|
374
|
+
if (status === "running") return "border-blue-200 bg-blue-50 text-blue-700";
|
|
375
|
+
return "border-zinc-200 bg-zinc-50 text-zinc-600";
|
|
376
|
+
}
|
|
377
|
+
function StatusPill({ status }) {
|
|
378
|
+
return /* @__PURE__ */ React3.createElement("span", { className: cx("inline-flex rounded-full border px-2 py-0.5 text-[11px] font-semibold", statusClass(status)) }, status.replace("_", " "));
|
|
379
|
+
}
|
|
380
|
+
function ErrorList({ errors }) {
|
|
381
|
+
if (!errors.length) return /* @__PURE__ */ React3.createElement("div", { className: "rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm font-medium text-emerald-700" }, "No validation errors.");
|
|
382
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "max-h-56 overflow-auto rounded-lg border border-red-200 bg-red-50/60" }, errors.map((error, index) => /* @__PURE__ */ React3.createElement("div", { key: `${error.rowNumber}-${error.column ?? "row"}-${index}`, className: "border-b border-red-100 px-3 py-2 text-sm last:border-b-0" }, /* @__PURE__ */ React3.createElement("div", { className: "font-medium text-red-800" }, "Row ", error.rowNumber, error.column ? ` \xB7 ${error.column}` : ""), /* @__PURE__ */ React3.createElement("div", { className: "mt-0.5 text-red-700" }, error.message))));
|
|
383
|
+
}
|
|
384
|
+
function DetailRows({ detail }) {
|
|
385
|
+
if (!detail) return null;
|
|
386
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "mt-4 overflow-hidden rounded-lg border border-zinc-200" }, /* @__PURE__ */ React3.createElement("div", { className: "border-b border-zinc-100 px-3 py-2 text-sm font-semibold" }, "Run rows"), /* @__PURE__ */ React3.createElement("div", { className: "max-h-64 overflow-auto" }, /* @__PURE__ */ React3.createElement("table", { className: "w-full min-w-[680px] border-collapse" }, /* @__PURE__ */ React3.createElement("thead", null, /* @__PURE__ */ React3.createElement("tr", null, /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Row"), /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Status"), /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Target"), /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Error"))), /* @__PURE__ */ React3.createElement("tbody", null, detail.rows.map((row) => /* @__PURE__ */ React3.createElement("tr", { key: row.id }, /* @__PURE__ */ React3.createElement("td", { className: tdClass }, row.rowNumber), /* @__PURE__ */ React3.createElement("td", { className: tdClass }, /* @__PURE__ */ React3.createElement(StatusPill, { status: row.status })), /* @__PURE__ */ React3.createElement("td", { className: cx(tdClass, "font-mono text-[11px] text-zinc-500") }, row.targetId ?? "-"), /* @__PURE__ */ React3.createElement("td", { className: cx(tdClass, "text-zinc-600") }, row.error ? JSON.stringify(row.error) : "-")))))));
|
|
387
|
+
}
|
|
388
|
+
function CsvImportDialog({ tableName, table, references, allowSystemTables, adminOrgId, toast, onClose, onCommitted }) {
|
|
389
|
+
const rowsRef = useRef([]);
|
|
390
|
+
const [stage, setStage] = useState2("map");
|
|
391
|
+
const [parsedRowCount, setParsedRowCount] = useState2(0);
|
|
392
|
+
const [preview, setPreview] = useState2(null);
|
|
393
|
+
const [commitResult, setCommitResult] = useState2(null);
|
|
394
|
+
const [history, setHistory] = useState2([]);
|
|
395
|
+
const [detail, setDetail] = useState2(null);
|
|
396
|
+
const [busyRunId, setBusyRunId] = useState2(null);
|
|
397
|
+
const [error, setError] = useState2(null);
|
|
398
|
+
const columns = useMemo2(() => importableColumns(table), [table]);
|
|
399
|
+
const loadHistory = async () => {
|
|
400
|
+
setHistory(await listImportHistory(tableName, adminOrgId));
|
|
401
|
+
};
|
|
402
|
+
useEffect2(() => {
|
|
403
|
+
const onKey = (event) => {
|
|
404
|
+
if (event.key === "Escape" && stage !== "committing") onClose();
|
|
405
|
+
};
|
|
406
|
+
document.addEventListener("keydown", onKey);
|
|
407
|
+
void loadHistory().catch((historyError) => toast(historyError.message, "error"));
|
|
408
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
409
|
+
}, [adminOrgId, stage, tableName]);
|
|
410
|
+
const runPreview = async (info) => {
|
|
411
|
+
setStage("previewing");
|
|
412
|
+
setError(null);
|
|
413
|
+
const rows = rowsRef.current;
|
|
414
|
+
const sourceSha256 = await sha256File(info.file).catch(() => null);
|
|
415
|
+
const response = await previewImport(
|
|
416
|
+
{
|
|
417
|
+
table: tableName,
|
|
418
|
+
filename: info.file.name,
|
|
419
|
+
sourceSizeBytes: info.file.size,
|
|
420
|
+
sourceSha256,
|
|
421
|
+
mapping: fileMapping(info),
|
|
422
|
+
rows,
|
|
423
|
+
options: { mode: "create", dryRun: true, allowSystemTables }
|
|
424
|
+
},
|
|
425
|
+
adminOrgId
|
|
426
|
+
);
|
|
427
|
+
setPreview(response);
|
|
428
|
+
setStage("preview");
|
|
429
|
+
await loadHistory();
|
|
430
|
+
};
|
|
431
|
+
const handleCommit = async () => {
|
|
432
|
+
if (!preview) return;
|
|
433
|
+
setStage("committing");
|
|
434
|
+
setError(null);
|
|
435
|
+
try {
|
|
436
|
+
const result = await commitImport(preview.runId, rowsRef.current, adminOrgId);
|
|
437
|
+
setCommitResult(result);
|
|
438
|
+
setStage("complete");
|
|
439
|
+
await loadHistory();
|
|
440
|
+
await onCommitted(result.run);
|
|
441
|
+
} catch (commitError) {
|
|
442
|
+
setError(commitError instanceof Error ? commitError.message : String(commitError));
|
|
443
|
+
setStage("preview");
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const handleRevert = async (run) => {
|
|
447
|
+
if (!window.confirm(`Revert import ${run.id}? This deletes rows created by the import when they still exist.`)) return;
|
|
448
|
+
setBusyRunId(run.id);
|
|
449
|
+
try {
|
|
450
|
+
const result = await revertImport(run.id, adminOrgId);
|
|
451
|
+
await loadHistory();
|
|
452
|
+
await onCommitted(result.run);
|
|
453
|
+
toast(`Reverted ${countLabel(result.revertedRows, "row")}${result.failedRows ? `; ${result.failedRows} failed` : ""}`);
|
|
454
|
+
} catch (revertError) {
|
|
455
|
+
toast(revertError instanceof Error ? revertError.message : String(revertError), "error");
|
|
456
|
+
} finally {
|
|
457
|
+
setBusyRunId(null);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
const showDetail = async (runId) => {
|
|
461
|
+
setBusyRunId(runId);
|
|
462
|
+
try {
|
|
463
|
+
setDetail(await getImportDetail(runId, adminOrgId));
|
|
464
|
+
} catch (detailError) {
|
|
465
|
+
toast(detailError instanceof Error ? detailError.message : String(detailError), "error");
|
|
466
|
+
} finally {
|
|
467
|
+
setBusyRunId(null);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const resetImporter = () => {
|
|
471
|
+
rowsRef.current = [];
|
|
472
|
+
setParsedRowCount(0);
|
|
473
|
+
setPreview(null);
|
|
474
|
+
setCommitResult(null);
|
|
475
|
+
setDetail(null);
|
|
476
|
+
setError(null);
|
|
477
|
+
setStage("map");
|
|
478
|
+
};
|
|
479
|
+
const canCommit = Boolean(preview && preview.counts.validRows > 0 && preview.counts.invalidRows === 0 && stage === "preview");
|
|
480
|
+
return /* @__PURE__ */ React3.createElement("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "csv-import-title" }, /* @__PURE__ */ React3.createElement("div", { className: "absolute inset-0 bg-zinc-950/30", onClick: stage === "committing" ? void 0 : onClose }), /* @__PURE__ */ React3.createElement("div", { className: "rb-import-dialog rb-pop-in relative flex max-h-[92vh] w-full max-w-5xl flex-col overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-[0_24px_48px_rgb(9_9_11/0.18)]" }, /* @__PURE__ */ React3.createElement("div", { className: "flex items-start justify-between gap-4 border-b border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React3.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React3.createElement("h2", { id: "csv-import-title", className: "m-0 text-[15px] font-semibold text-zinc-900" }, "Import CSV"), /* @__PURE__ */ React3.createElement("div", { className: "mt-1 flex flex-wrap items-center gap-2 text-sm text-zinc-500" }, /* @__PURE__ */ React3.createElement("span", { className: "font-medium text-zinc-700" }, table.label), /* @__PURE__ */ React3.createElement("span", { className: "font-mono text-[12px]" }, table.name), /* @__PURE__ */ React3.createElement("span", null, countLabel(maxImportRows, "row"), " max"))), /* @__PURE__ */ React3.createElement("button", { className: "inline-flex h-8 w-8 items-center justify-center rounded-lg text-zinc-400 hover:bg-zinc-100 hover:text-zinc-900", onClick: onClose, title: "Close" }, icons.close)), /* @__PURE__ */ React3.createElement("div", { className: "border-b border-zinc-100 px-5 py-3" }, /* @__PURE__ */ React3.createElement("div", { className: "flex flex-wrap gap-2 text-xs font-semibold" }, ["Upload", "Map columns", "Preview validation", "Confirm"].map((label, index) => {
|
|
481
|
+
const active = stage === "map" && index < 2 || (stage === "previewing" || stage === "preview") && index === 2 || (stage === "committing" || stage === "complete") && index === 3;
|
|
482
|
+
return /* @__PURE__ */ React3.createElement("span", { key: label, className: cx("rounded-full border px-2.5 py-1", active ? "border-zinc-900 bg-zinc-900 text-white" : "border-zinc-200 bg-white text-zinc-500") }, label);
|
|
483
|
+
}))), /* @__PURE__ */ React3.createElement("div", { className: "min-h-0 flex-1 overflow-auto px-5 py-4" }, stage === "map" ? /* @__PURE__ */ React3.createElement("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_300px]" }, /* @__PURE__ */ React3.createElement(
|
|
484
|
+
CsvImporter,
|
|
485
|
+
{
|
|
486
|
+
dataHandler: async (rows) => {
|
|
487
|
+
rowsRef.current = [...rowsRef.current, ...rows];
|
|
488
|
+
setParsedRowCount(rowsRef.current.length);
|
|
489
|
+
if (rowsRef.current.length > maxImportRows) {
|
|
490
|
+
throw new Error(`CSV imports are limited to ${countLabel(maxImportRows, "row")} in v1`);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
defaultNoHeader: false,
|
|
494
|
+
restartable: false,
|
|
495
|
+
chunkSize: 500,
|
|
496
|
+
skipEmptyLines: true,
|
|
497
|
+
onStart: () => {
|
|
498
|
+
rowsRef.current = [];
|
|
499
|
+
setParsedRowCount(0);
|
|
500
|
+
setPreview(null);
|
|
501
|
+
setCommitResult(null);
|
|
502
|
+
setError(null);
|
|
503
|
+
},
|
|
504
|
+
onComplete: (info) => {
|
|
505
|
+
void runPreview(info).catch((previewError) => {
|
|
506
|
+
setError(previewError instanceof Error ? previewError.message : String(previewError));
|
|
507
|
+
setStage("map");
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
columns.map(([key, column]) => /* @__PURE__ */ React3.createElement(CsvImporterField, { key, name: key, label: column.label || key, optional: !column.required }))
|
|
512
|
+
), /* @__PURE__ */ React3.createElement("aside", { className: "min-w-0" }, /* @__PURE__ */ React3.createElement("div", { className: "rounded-lg border border-zinc-200" }, /* @__PURE__ */ React3.createElement("div", { className: "border-b border-zinc-100 px-3 py-2 text-sm font-semibold" }, "Importable fields"), /* @__PURE__ */ React3.createElement("div", { className: "max-h-[420px] divide-y divide-zinc-100 overflow-auto" }, columns.map(([key, column]) => /* @__PURE__ */ React3.createElement("div", { key, className: "px-3 py-2" }, /* @__PURE__ */ React3.createElement("div", { className: "flex items-center justify-between gap-2" }, /* @__PURE__ */ React3.createElement("div", { className: "truncate text-sm font-medium text-zinc-900" }, column.label || key), column.required ? /* @__PURE__ */ React3.createElement("span", { className: "rounded bg-zinc-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase text-zinc-500" }, "required") : null), /* @__PURE__ */ React3.createElement("div", { className: "mt-0.5 text-xs text-zinc-500" }, columnHelp(key, column, references)))))), parsedRowCount ? /* @__PURE__ */ React3.createElement("div", { className: "mt-3 text-sm text-zinc-500" }, countLabel(parsedRowCount, "row"), " parsed") : null, error ? /* @__PURE__ */ React3.createElement("div", { className: "mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm font-medium text-red-700" }, error) : null)) : null, stage === "previewing" ? /* @__PURE__ */ React3.createElement("div", { className: "py-16 text-center text-sm font-medium text-zinc-500" }, "Validating rows...") : null, (stage === "preview" || stage === "committing" || stage === "complete") && preview ? /* @__PURE__ */ React3.createElement("div", { className: "grid gap-4" }, /* @__PURE__ */ React3.createElement("div", { className: "grid gap-3 sm:grid-cols-4" }, /* @__PURE__ */ React3.createElement("div", { className: cx(cardClass, "px-4 py-3") }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs font-medium text-zinc-500" }, "Total"), /* @__PURE__ */ React3.createElement("div", { className: "mt-1 text-xl font-semibold tabular-nums" }, preview.counts.totalRows)), /* @__PURE__ */ React3.createElement("div", { className: cx(cardClass, "px-4 py-3") }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs font-medium text-zinc-500" }, "Valid"), /* @__PURE__ */ React3.createElement("div", { className: "mt-1 text-xl font-semibold tabular-nums text-emerald-700" }, preview.counts.validRows)), /* @__PURE__ */ React3.createElement("div", { className: cx(cardClass, "px-4 py-3") }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs font-medium text-zinc-500" }, "Invalid"), /* @__PURE__ */ React3.createElement("div", { className: "mt-1 text-xl font-semibold tabular-nums text-red-700" }, preview.counts.invalidRows)), /* @__PURE__ */ React3.createElement("div", { className: cx(cardClass, "px-4 py-3") }, /* @__PURE__ */ React3.createElement("div", { className: "text-xs font-medium text-zinc-500" }, "Inserted"), /* @__PURE__ */ React3.createElement("div", { className: "mt-1 text-xl font-semibold tabular-nums" }, commitResult?.counts.insertedRows ?? 0))), /* @__PURE__ */ React3.createElement(ErrorList, { errors: preview.errors }), error ? /* @__PURE__ */ React3.createElement("div", { className: "rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm font-medium text-red-700" }, error) : null, commitResult ? /* @__PURE__ */ React3.createElement("div", { className: "rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-3 text-sm text-emerald-800" }, /* @__PURE__ */ React3.createElement("div", { className: "font-semibold" }, "Import complete"), /* @__PURE__ */ React3.createElement("div", { className: "mt-1" }, "Created ", countLabel(commitResult.counts.insertedRows, "row"), " in ", table.label, ". Run ", /* @__PURE__ */ React3.createElement("span", { className: "font-mono" }, commitResult.run.id), ".")) : null) : null, history.length ? /* @__PURE__ */ React3.createElement("div", { className: "mt-5" }, /* @__PURE__ */ React3.createElement("div", { className: "mb-2 flex items-center justify-between gap-2" }, /* @__PURE__ */ React3.createElement("h3", { className: "m-0 text-sm font-semibold" }, "Recent imports"), /* @__PURE__ */ React3.createElement("button", { className: secondaryButton, onClick: () => void loadHistory().catch((historyError) => toast(historyError.message, "error")) }, icons.refresh, "Refresh")), /* @__PURE__ */ React3.createElement("div", { className: "overflow-hidden rounded-lg border border-zinc-200" }, /* @__PURE__ */ React3.createElement("table", { className: "w-full min-w-[760px] border-collapse" }, /* @__PURE__ */ React3.createElement("thead", null, /* @__PURE__ */ React3.createElement("tr", null, /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Run"), /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Status"), /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Rows"), /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Actor"), /* @__PURE__ */ React3.createElement("th", { className: thClass }, "Created"), /* @__PURE__ */ React3.createElement("th", { className: thClass }))), /* @__PURE__ */ React3.createElement("tbody", null, history.map((run) => {
|
|
513
|
+
const canRevert = ["succeeded", "partially_failed"].includes(run.status) && run.insertedRows > run.revertedRows;
|
|
514
|
+
return /* @__PURE__ */ React3.createElement("tr", { key: run.id }, /* @__PURE__ */ React3.createElement("td", { className: cx(tdClass, "font-mono text-[11px]") }, run.id), /* @__PURE__ */ React3.createElement("td", { className: tdClass }, /* @__PURE__ */ React3.createElement(StatusPill, { status: run.status })), /* @__PURE__ */ React3.createElement("td", { className: tdClass }, run.insertedRows, "/", run.totalRows), /* @__PURE__ */ React3.createElement("td", { className: tdClass }, run.actorEmail ?? run.actorId ?? "-"), /* @__PURE__ */ React3.createElement("td", { className: tdClass }, formatDateTime(run.createdAt)), /* @__PURE__ */ React3.createElement("td", { className: tdClass }, /* @__PURE__ */ React3.createElement("div", { className: "flex justify-end gap-2" }, /* @__PURE__ */ React3.createElement("button", { className: secondaryButton, disabled: busyRunId === run.id, onClick: () => void showDetail(run.id) }, "Details"), /* @__PURE__ */ React3.createElement("button", { className: destructiveButton, disabled: !canRevert || busyRunId === run.id, title: canRevert ? "Revert imported rows" : "This run cannot be reverted", onClick: () => void handleRevert(run) }, "Revert"))));
|
|
515
|
+
})))), /* @__PURE__ */ React3.createElement(DetailRows, { detail })) : null), /* @__PURE__ */ React3.createElement("div", { className: "flex flex-wrap items-center justify-between gap-2 border-t border-zinc-100 bg-zinc-50/60 px-5 py-4" }, /* @__PURE__ */ React3.createElement("button", { className: secondaryButton, disabled: stage === "committing", onClick: resetImporter }, "Start over"), /* @__PURE__ */ React3.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React3.createElement("button", { className: secondaryButton, disabled: stage === "committing", onClick: onClose }, "Close"), stage === "preview" ? /* @__PURE__ */ React3.createElement("button", { className: primaryButton, disabled: !canCommit, title: preview?.counts.invalidRows ? "Fix invalid rows before committing" : void 0, onClick: () => void handleCommit() }, icons.checkmark, "Commit import") : null, stage === "committing" ? /* @__PURE__ */ React3.createElement("button", { className: primaryButton, disabled: true }, "Committing...") : null, stage === "complete" ? /* @__PURE__ */ React3.createElement("button", { className: primaryButton, onClick: onClose }, "Done") : null))));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// admin-src/DataView.tsx
|
|
519
|
+
var columnWidthsStoreKey = "raybase-admin-column-widths";
|
|
520
|
+
var rowHeightStoreKey = "raybase-admin-row-height";
|
|
521
|
+
var DynamicDataSheetGrid = DynamicDataSheetGridBase;
|
|
522
|
+
function readStoredWidths(tableName) {
|
|
523
|
+
try {
|
|
524
|
+
const store = JSON.parse(localStorage.getItem(columnWidthsStoreKey) ?? "{}");
|
|
525
|
+
const widths = store?.[tableName];
|
|
526
|
+
return widths && typeof widths === "object" ? widths : {};
|
|
527
|
+
} catch {
|
|
528
|
+
return {};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function writeStoredWidths(tableName, widths) {
|
|
532
|
+
try {
|
|
533
|
+
const parsed = JSON.parse(localStorage.getItem(columnWidthsStoreKey) ?? "{}");
|
|
534
|
+
const store = parsed && typeof parsed === "object" ? parsed : {};
|
|
535
|
+
if (Object.keys(widths).length) store[tableName] = widths;
|
|
536
|
+
else delete store[tableName];
|
|
537
|
+
localStorage.setItem(columnWidthsStoreKey, JSON.stringify(store));
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function columnAllowsClear(column) {
|
|
542
|
+
return !column.required && !column.primaryKey && !column.auto;
|
|
543
|
+
}
|
|
544
|
+
function recordTitle(row, table) {
|
|
545
|
+
for (const key of ["name", "title", "email", "label"]) {
|
|
546
|
+
const value = row[key];
|
|
547
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
548
|
+
}
|
|
549
|
+
for (const [key, column] of Object.entries(table.columns)) {
|
|
550
|
+
if (column.primaryKey || column.kind === "id") continue;
|
|
551
|
+
const value = row[key];
|
|
552
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
553
|
+
}
|
|
554
|
+
return rowKey(row) || "New record";
|
|
555
|
+
}
|
|
556
|
+
function UnsavedChangesDialog({ onCancel, onDiscard }) {
|
|
557
|
+
useEffect3(() => {
|
|
558
|
+
const onKey = (event) => {
|
|
559
|
+
if (event.key === "Escape") onCancel();
|
|
560
|
+
};
|
|
561
|
+
document.addEventListener("keydown", onKey);
|
|
562
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
563
|
+
}, [onCancel]);
|
|
564
|
+
return /* @__PURE__ */ React4.createElement("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "unsaved-changes-title" }, /* @__PURE__ */ React4.createElement("div", { className: "absolute inset-0 bg-zinc-950/30", onClick: onCancel }), /* @__PURE__ */ React4.createElement("div", { className: "relative w-full max-w-md overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-[0_24px_48px_rgb(9_9_11/0.18)]" }, /* @__PURE__ */ React4.createElement("div", { className: "border-b border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React4.createElement("h2", { id: "unsaved-changes-title", className: "m-0 text-[15px] font-semibold text-zinc-900" }, "Discard unsaved changes?")), /* @__PURE__ */ React4.createElement("div", { className: "px-5 py-4 text-sm leading-relaxed text-zinc-600" }, "Your edits in the data grid have not been saved."), /* @__PURE__ */ React4.createElement("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-100 bg-zinc-50/60 px-5 py-4" }, /* @__PURE__ */ React4.createElement("button", { className: secondaryButton, onClick: onCancel }, "Stay"), /* @__PURE__ */ React4.createElement("button", { className: destructiveButton, onClick: onDiscard }, "Discard changes"))));
|
|
565
|
+
}
|
|
566
|
+
var rowHeightOptions = [
|
|
567
|
+
{ id: "short", label: "Short", height: 32, bars: 4 },
|
|
568
|
+
{ id: "medium", label: "Medium", height: 40, bars: 3 },
|
|
569
|
+
{ id: "tall", label: "Tall", height: 56, bars: 2 }
|
|
570
|
+
];
|
|
571
|
+
function readStoredRowHeight() {
|
|
572
|
+
try {
|
|
573
|
+
const stored = localStorage.getItem(rowHeightStoreKey);
|
|
574
|
+
return rowHeightOptions.some((option) => option.id === stored) ? stored : "medium";
|
|
575
|
+
} catch {
|
|
576
|
+
return "medium";
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function writeStoredRowHeight(value) {
|
|
580
|
+
try {
|
|
581
|
+
localStorage.setItem(rowHeightStoreKey, value);
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function RowHeightToggle({ value, onChange }) {
|
|
586
|
+
return /* @__PURE__ */ React4.createElement("div", { className: "inline-flex h-9 items-center gap-0.5 rounded-lg border border-zinc-200 bg-white p-0.5", role: "radiogroup", "aria-label": "Row height" }, rowHeightOptions.map((option) => /* @__PURE__ */ React4.createElement(
|
|
587
|
+
"button",
|
|
588
|
+
{
|
|
589
|
+
key: option.id,
|
|
590
|
+
type: "button",
|
|
591
|
+
role: "radio",
|
|
592
|
+
"aria-checked": option.id === value,
|
|
593
|
+
title: `${option.label} rows`,
|
|
594
|
+
className: cx(
|
|
595
|
+
"flex h-7 w-8 items-center justify-center rounded-md transition-colors",
|
|
596
|
+
option.id === value ? "bg-zinc-100 text-zinc-900" : "text-zinc-400 hover:text-zinc-700"
|
|
597
|
+
),
|
|
598
|
+
onClick: () => onChange(option.id)
|
|
599
|
+
},
|
|
600
|
+
/* @__PURE__ */ React4.createElement("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": true }, Array.from({ length: option.bars }, (_, index) => {
|
|
601
|
+
const gap = 18 / (option.bars - 1);
|
|
602
|
+
const y = 3 + index * gap;
|
|
603
|
+
return /* @__PURE__ */ React4.createElement("line", { key: index, x1: "4", y1: y, x2: "20", y2: y });
|
|
604
|
+
}))
|
|
605
|
+
)));
|
|
606
|
+
}
|
|
607
|
+
function GutterCell({ rowIndex, columnData }) {
|
|
608
|
+
return /* @__PURE__ */ React4.createElement(
|
|
609
|
+
"div",
|
|
610
|
+
{
|
|
611
|
+
className: "flex h-full w-full cursor-pointer items-center gap-0.5 px-1",
|
|
612
|
+
title: "Edit record",
|
|
613
|
+
onMouseDown: (event) => {
|
|
614
|
+
event.preventDefault();
|
|
615
|
+
event.stopPropagation();
|
|
616
|
+
},
|
|
617
|
+
onClick: (event) => {
|
|
618
|
+
event.stopPropagation();
|
|
619
|
+
columnData.onEdit(rowIndex);
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
/* @__PURE__ */ React4.createElement("span", { className: "rb-row-edit-icon flex h-5 w-5 shrink-0 items-center justify-center text-zinc-500 transition-opacity", "aria-hidden": "true" }, icons.edit),
|
|
623
|
+
/* @__PURE__ */ React4.createElement("span", { className: "rb-row-number min-w-0 flex-1 text-center text-[11px] tabular-nums text-zinc-400" }, rowIndex + 1)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
function renderRecordContextMenuItem(item) {
|
|
627
|
+
if (item.type === "DELETE_ROW") return /* @__PURE__ */ React4.createElement(React4.Fragment, null, "Delete record");
|
|
628
|
+
if (item.type === "DELETE_ROWS") {
|
|
629
|
+
return /* @__PURE__ */ React4.createElement(React4.Fragment, null, "Delete records ", /* @__PURE__ */ React4.createElement("b", null, item.fromRow), " to ", /* @__PURE__ */ React4.createElement("b", null, item.toRow));
|
|
630
|
+
}
|
|
631
|
+
if (item.type === "DUPLICATE_ROW") return /* @__PURE__ */ React4.createElement(React4.Fragment, null, "Duplicate record");
|
|
632
|
+
if (item.type === "DUPLICATE_ROWS") {
|
|
633
|
+
return /* @__PURE__ */ React4.createElement(React4.Fragment, null, "Duplicate records ", /* @__PURE__ */ React4.createElement("b", null, item.fromRow), " to ", /* @__PURE__ */ React4.createElement("b", null, item.toRow));
|
|
634
|
+
}
|
|
635
|
+
if (item.type === "INSERT_ROW_BELLOW") return /* @__PURE__ */ React4.createElement(React4.Fragment, null, "Insert new record");
|
|
636
|
+
return renderContextMenuItem(item);
|
|
637
|
+
}
|
|
638
|
+
function defaultRow(table) {
|
|
639
|
+
const next = {};
|
|
640
|
+
for (const [key, column] of Object.entries(table.columns)) {
|
|
641
|
+
if (column.primaryKey || column.auto) continue;
|
|
642
|
+
next[key] = defaultValue(column);
|
|
643
|
+
}
|
|
644
|
+
return next;
|
|
645
|
+
}
|
|
646
|
+
function defaultValue(column) {
|
|
647
|
+
if (column.enumValues?.length) return null;
|
|
648
|
+
switch (column.kind) {
|
|
649
|
+
case "boolean":
|
|
650
|
+
return false;
|
|
651
|
+
case "integer":
|
|
652
|
+
case "real":
|
|
653
|
+
return column.required ? 0 : null;
|
|
654
|
+
case "date":
|
|
655
|
+
return column.required ? toIsoDate2(/* @__PURE__ */ new Date()) : null;
|
|
656
|
+
case "datetime":
|
|
657
|
+
return column.required ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
658
|
+
case "text":
|
|
659
|
+
case "longText":
|
|
660
|
+
return column.required ? "" : null;
|
|
661
|
+
default:
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function duplicateRowData(table, row) {
|
|
666
|
+
const next = {};
|
|
667
|
+
for (const [key, column] of Object.entries(table.columns)) {
|
|
668
|
+
if (column.primaryKey || column.auto) continue;
|
|
669
|
+
next[key] = column.kind === "secret" ? null : row[key] ?? null;
|
|
670
|
+
}
|
|
671
|
+
return next;
|
|
672
|
+
}
|
|
673
|
+
function filenameFromDisposition(value) {
|
|
674
|
+
if (!value) return null;
|
|
675
|
+
const match = value.match(/filename\*=UTF-8''([^;]+)|filename="?([^"]+)"?/i);
|
|
676
|
+
const raw = match?.[1] ?? match?.[2];
|
|
677
|
+
return raw ? decodeURIComponent(raw) : null;
|
|
678
|
+
}
|
|
679
|
+
function DataView({
|
|
680
|
+
toast,
|
|
681
|
+
routeTableName,
|
|
682
|
+
adminOrgId,
|
|
683
|
+
onCurrentTableChange,
|
|
684
|
+
onDirtyChange
|
|
685
|
+
}) {
|
|
686
|
+
const tableNames = useMemo3(() => Object.keys(boot.schema), []);
|
|
687
|
+
const appTableNames = useMemo3(() => tableNames.filter((name) => !boot.schema[name].system), [tableNames]);
|
|
688
|
+
const [showSystemTables, setShowSystemTables] = useState3(Boolean(routeTableName && boot.schema[routeTableName]?.system));
|
|
689
|
+
const [currentTable, setCurrentTable] = useState3(routeTableName && boot.schema[routeTableName] ? routeTableName : appTableNames[0] ?? tableNames[0] ?? "");
|
|
690
|
+
const [rows, setRows] = useState3([]);
|
|
691
|
+
const [originalRows, setOriginalRows] = useState3([]);
|
|
692
|
+
const [sortBy, setSortBy] = useState3("");
|
|
693
|
+
const [sortDirection, setSortDirection] = useState3("asc");
|
|
694
|
+
const [filters, setFilters] = useState3([]);
|
|
695
|
+
const [searchText, setSearchText] = useState3("");
|
|
696
|
+
const [references, setReferences] = useState3({});
|
|
697
|
+
const [allowSystemTableEditing, setAllowSystemTableEditing] = useState3(false);
|
|
698
|
+
const [pendingTable, setPendingTable] = useState3(null);
|
|
699
|
+
const [pendingShowSystemTables, setPendingShowSystemTables] = useState3(null);
|
|
700
|
+
const [rowHeightId, setRowHeightId] = useState3(() => readStoredRowHeight());
|
|
701
|
+
const [expandedIndex, setExpandedIndex] = useState3(null);
|
|
702
|
+
const [showImportDialog, setShowImportDialog] = useState3(false);
|
|
703
|
+
const [gridHeight] = useState3(() => Math.max(420, window.innerHeight - 380));
|
|
704
|
+
const [columnWidths, setColumnWidths] = useState3(() => readStoredWidths(currentTable));
|
|
705
|
+
const columnWidthsRef = useRef2(columnWidths);
|
|
706
|
+
columnWidthsRef.current = columnWidths;
|
|
707
|
+
const table = boot.schema[currentTable];
|
|
708
|
+
const visibleTableNames = showSystemTables ? tableNames : appTableNames;
|
|
709
|
+
const systemTableCount = tableNames.length - appTableNames.length;
|
|
710
|
+
const currentTableIsSystem = Boolean(table?.system);
|
|
711
|
+
const currentTableHasId = Boolean(table?.columns.id);
|
|
712
|
+
const currentTableReadOnly = currentTableIsSystem && (!allowSystemTableEditing || !currentTableHasId);
|
|
713
|
+
const dirty = useMemo3(() => !equalRowSets(rows, originalRows), [rows, originalRows]);
|
|
714
|
+
useEffect3(() => {
|
|
715
|
+
onDirtyChange(dirty);
|
|
716
|
+
return () => onDirtyChange(false);
|
|
717
|
+
}, [dirty, onDirtyChange]);
|
|
718
|
+
useEffect3(() => {
|
|
719
|
+
if (!dirty) return;
|
|
720
|
+
const onBeforeUnload = (event) => {
|
|
721
|
+
event.preventDefault();
|
|
722
|
+
event.returnValue = "";
|
|
723
|
+
};
|
|
724
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
725
|
+
return () => window.removeEventListener("beforeunload", onBeforeUnload);
|
|
726
|
+
}, [dirty]);
|
|
727
|
+
useEffect3(() => {
|
|
728
|
+
if (!showSystemTables && currentTableIsSystem) {
|
|
729
|
+
const fallback = appTableNames[0] ?? "";
|
|
730
|
+
setCurrentTable(fallback);
|
|
731
|
+
if (fallback) onCurrentTableChange(fallback);
|
|
732
|
+
}
|
|
733
|
+
}, [showSystemTables, currentTableIsSystem, appTableNames, onCurrentTableChange]);
|
|
734
|
+
useEffect3(() => {
|
|
735
|
+
setAllowSystemTableEditing(false);
|
|
736
|
+
setExpandedIndex(null);
|
|
737
|
+
setSortBy("");
|
|
738
|
+
setFilters([]);
|
|
739
|
+
setSearchText("");
|
|
740
|
+
setColumnWidths(readStoredWidths(currentTable));
|
|
741
|
+
}, [currentTable]);
|
|
742
|
+
const resizeHandlers = useMemo3(
|
|
743
|
+
() => ({
|
|
744
|
+
onResize: (key, width) => {
|
|
745
|
+
columnWidthsRef.current = { ...columnWidthsRef.current, [key]: width };
|
|
746
|
+
setColumnWidths(columnWidthsRef.current);
|
|
747
|
+
},
|
|
748
|
+
onResizeEnd: () => writeStoredWidths(currentTable, columnWidthsRef.current),
|
|
749
|
+
onResizeReset: (key) => {
|
|
750
|
+
const next = { ...columnWidthsRef.current };
|
|
751
|
+
delete next[key];
|
|
752
|
+
setColumnWidths(next);
|
|
753
|
+
writeStoredWidths(currentTable, next);
|
|
754
|
+
}
|
|
755
|
+
}),
|
|
756
|
+
[currentTable]
|
|
757
|
+
);
|
|
758
|
+
const sortHandlers = useMemo3(
|
|
759
|
+
() => ({
|
|
760
|
+
activeKey: sortBy,
|
|
761
|
+
direction: sortDirection
|
|
762
|
+
}),
|
|
763
|
+
[sortBy, sortDirection]
|
|
764
|
+
);
|
|
765
|
+
useEffect3(() => {
|
|
766
|
+
if (routeTableName && boot.schema[routeTableName] && routeTableName !== currentTable) {
|
|
767
|
+
setCurrentTable(routeTableName);
|
|
768
|
+
if (boot.schema[routeTableName].system) setShowSystemTables(true);
|
|
769
|
+
}
|
|
770
|
+
}, [currentTable, routeTableName]);
|
|
771
|
+
const discardLocalChanges = () => {
|
|
772
|
+
setRows(originalRows);
|
|
773
|
+
setExpandedIndex(null);
|
|
774
|
+
};
|
|
775
|
+
const requestTableChange = (name) => {
|
|
776
|
+
if (name === currentTable) return;
|
|
777
|
+
if (dirty) {
|
|
778
|
+
setPendingTable(name);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
setCurrentTable(name);
|
|
782
|
+
onCurrentTableChange(name);
|
|
783
|
+
};
|
|
784
|
+
const requestShowSystemTables = (checked) => {
|
|
785
|
+
if (!checked && currentTableIsSystem && dirty) {
|
|
786
|
+
setPendingShowSystemTables(checked);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
setShowSystemTables(checked);
|
|
790
|
+
};
|
|
791
|
+
const requestRowHeight = (next) => {
|
|
792
|
+
setRowHeightId(next);
|
|
793
|
+
writeStoredRowHeight(next);
|
|
794
|
+
};
|
|
795
|
+
useEffect3(() => {
|
|
796
|
+
if (!table) return;
|
|
797
|
+
const loadReferences = async () => {
|
|
798
|
+
const entries = await Promise.all(
|
|
799
|
+
Object.entries(table.columns).filter(([, column]) => column.references).map(async ([key]) => {
|
|
800
|
+
const params = adminOrgId ? `?adminOrgId=${encodeURIComponent(adminOrgId)}` : "";
|
|
801
|
+
return [key, (await api(`/api/admin/references/${currentTable}/${key}${params}`)).options];
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
setReferences(Object.fromEntries(entries));
|
|
805
|
+
};
|
|
806
|
+
void loadReferences().catch((error) => toast(error.message, "error"));
|
|
807
|
+
}, [adminOrgId, currentTable, table, toast]);
|
|
808
|
+
const filterQuery = useMemo3(() => {
|
|
809
|
+
if (!table) return "";
|
|
810
|
+
const compiled = compileFilters(table, filters);
|
|
811
|
+
return Object.keys(compiled).length ? JSON.stringify(compiled) : "";
|
|
812
|
+
}, [table, filters]);
|
|
813
|
+
const searchQuery = searchText.trim();
|
|
814
|
+
const isFiltered = Boolean(filterQuery || searchQuery);
|
|
815
|
+
const load = async () => {
|
|
816
|
+
if (!currentTable) return;
|
|
817
|
+
const params = new URLSearchParams({ admin: "1", limit: "100" });
|
|
818
|
+
if (adminOrgId) params.set("adminOrgId", adminOrgId);
|
|
819
|
+
if (sortBy) {
|
|
820
|
+
params.set("sortBy", sortBy);
|
|
821
|
+
params.set("sortDirection", sortDirection);
|
|
822
|
+
}
|
|
823
|
+
if (filterQuery) params.set("filter", filterQuery);
|
|
824
|
+
if (searchQuery) params.set("search", searchQuery);
|
|
825
|
+
const data = await api(`/api/data/${currentTable}?${params}`);
|
|
826
|
+
setRows(data.rows);
|
|
827
|
+
setOriginalRows(data.rows);
|
|
828
|
+
};
|
|
829
|
+
useEffect3(() => {
|
|
830
|
+
const handle = window.setTimeout(() => {
|
|
831
|
+
void load().catch((error) => toast(error.message, "error"));
|
|
832
|
+
}, 200);
|
|
833
|
+
return () => window.clearTimeout(handle);
|
|
834
|
+
}, [adminOrgId, currentTable, sortBy, sortDirection, filterQuery, searchQuery]);
|
|
835
|
+
const jsonModes = useMemo3(() => {
|
|
836
|
+
const modes = {};
|
|
837
|
+
for (const [key, column] of Object.entries(table?.columns ?? {})) {
|
|
838
|
+
if (column.kind === "json") modes[key] = jsonColumnMode2(originalRows, key);
|
|
839
|
+
}
|
|
840
|
+
return modes;
|
|
841
|
+
}, [table, originalRows]);
|
|
842
|
+
const tagSuggestions = useMemo3(() => {
|
|
843
|
+
const suggestions = {};
|
|
844
|
+
for (const [key, mode] of Object.entries(jsonModes)) {
|
|
845
|
+
if (mode === "tags") suggestions[key] = collectTagSuggestions2(originalRows, key);
|
|
846
|
+
}
|
|
847
|
+
return suggestions;
|
|
848
|
+
}, [jsonModes, originalRows]);
|
|
849
|
+
const dirtyCellKeys = useMemo3(() => {
|
|
850
|
+
const keysByRow = /* @__PURE__ */ new Map();
|
|
851
|
+
const originalById = new Map(originalRows.map((row) => [rowKey(row), row]));
|
|
852
|
+
const columnKeys = Object.keys(table?.columns ?? {});
|
|
853
|
+
rows.forEach((row, rowIndex) => {
|
|
854
|
+
const id = rowKey(row);
|
|
855
|
+
const original = id ? originalById.get(id) : void 0;
|
|
856
|
+
for (const key of columnKeys) {
|
|
857
|
+
if (JSON.stringify(row[key] ?? null) === JSON.stringify(original?.[key] ?? null)) continue;
|
|
858
|
+
const keys = keysByRow.get(rowIndex) ?? /* @__PURE__ */ new Set();
|
|
859
|
+
keys.add(key);
|
|
860
|
+
keysByRow.set(rowIndex, keys);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
return keysByRow;
|
|
864
|
+
}, [rows, originalRows, table]);
|
|
865
|
+
const columns = useMemo3(() => {
|
|
866
|
+
if (!table) return [];
|
|
867
|
+
return buildGridColumns({
|
|
868
|
+
table,
|
|
869
|
+
rows: originalRows,
|
|
870
|
+
references,
|
|
871
|
+
readOnly: currentTableReadOnly,
|
|
872
|
+
jsonModes,
|
|
873
|
+
tagSuggestions,
|
|
874
|
+
dirtyCellKeys,
|
|
875
|
+
sort: sortHandlers,
|
|
876
|
+
columnWidths,
|
|
877
|
+
resize: resizeHandlers
|
|
878
|
+
});
|
|
879
|
+
}, [table, originalRows, references, currentTableReadOnly, jsonModes, tagSuggestions, dirtyCellKeys, sortHandlers, columnWidths, resizeHandlers]);
|
|
880
|
+
const gutterColumn = useMemo3(
|
|
881
|
+
() => ({
|
|
882
|
+
basis: 56,
|
|
883
|
+
component: GutterCell,
|
|
884
|
+
columnData: { onEdit: (rowIndex) => setExpandedIndex(rowIndex) }
|
|
885
|
+
}),
|
|
886
|
+
[]
|
|
887
|
+
);
|
|
888
|
+
const changeCounts = useMemo3(() => {
|
|
889
|
+
const originalById = new Map(originalRows.map((row) => [rowKey(row), row]));
|
|
890
|
+
const currentIds = new Set(rows.map(rowKey).filter(Boolean));
|
|
891
|
+
let created = 0;
|
|
892
|
+
let updated = 0;
|
|
893
|
+
for (const row of rows) {
|
|
894
|
+
const id = rowKey(row);
|
|
895
|
+
if (!id) created += 1;
|
|
896
|
+
else if (!equalRows(row, originalById.get(id) ?? {})) updated += 1;
|
|
897
|
+
}
|
|
898
|
+
let deleted = 0;
|
|
899
|
+
for (const row of originalRows) {
|
|
900
|
+
const id = rowKey(row);
|
|
901
|
+
if (id && !currentIds.has(id)) deleted += 1;
|
|
902
|
+
}
|
|
903
|
+
return { created, updated, deleted, total: created + updated + deleted };
|
|
904
|
+
}, [rows, originalRows]);
|
|
905
|
+
const editableKeys = useMemo3(
|
|
906
|
+
() => Object.entries(table?.columns ?? {}).filter(([, column]) => !column.primaryKey && !column.auto).map(([key]) => key),
|
|
907
|
+
[table]
|
|
908
|
+
);
|
|
909
|
+
const createPayload = (row) => Object.fromEntries(editableKeys.map((key) => [key, row[key] ?? null]));
|
|
910
|
+
const updatePayload = (row, original) => Object.fromEntries(
|
|
911
|
+
editableKeys.filter((key) => JSON.stringify(row[key] ?? null) !== JSON.stringify(original[key] ?? null)).map((key) => [key, row[key] ?? null])
|
|
912
|
+
);
|
|
913
|
+
const save = async () => {
|
|
914
|
+
if (currentTableReadOnly) return;
|
|
915
|
+
const originalById = new Map(originalRows.map((row) => [rowKey(row), row]));
|
|
916
|
+
const currentIds = new Set(rows.map(rowKey).filter(Boolean));
|
|
917
|
+
for (const row of rows) {
|
|
918
|
+
const id = rowKey(row);
|
|
919
|
+
if (!id) {
|
|
920
|
+
const params = new URLSearchParams({ admin: "1" });
|
|
921
|
+
if (adminOrgId) params.set("adminOrgId", adminOrgId);
|
|
922
|
+
await api(`/api/data/${currentTable}?${params}`, { method: "POST", body: JSON.stringify(createPayload(row)) });
|
|
923
|
+
} else if (!equalRows(row, originalById.get(id) ?? {})) {
|
|
924
|
+
const payload = updatePayload(row, originalById.get(id) ?? {});
|
|
925
|
+
if (!Object.keys(payload).length) continue;
|
|
926
|
+
const params = new URLSearchParams({ admin: "1" });
|
|
927
|
+
if (adminOrgId) params.set("adminOrgId", adminOrgId);
|
|
928
|
+
await api(`/api/data/${currentTable}/${id}?${params}`, { method: "PATCH", body: JSON.stringify(payload) });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
for (const row of originalRows) {
|
|
932
|
+
const id = rowKey(row);
|
|
933
|
+
if (id && !currentIds.has(id)) {
|
|
934
|
+
const params = new URLSearchParams({ admin: "1" });
|
|
935
|
+
if (adminOrgId) params.set("adminOrgId", adminOrgId);
|
|
936
|
+
await api(`/api/data/${currentTable}/${id}?${params}`, { method: "DELETE", body: "{}" });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
setExpandedIndex(null);
|
|
940
|
+
await load();
|
|
941
|
+
toast("Saved");
|
|
942
|
+
};
|
|
943
|
+
const exportCsv = async () => {
|
|
944
|
+
if (!table || dirty) return;
|
|
945
|
+
const params = new URLSearchParams();
|
|
946
|
+
if (adminOrgId) params.set("adminOrgId", adminOrgId);
|
|
947
|
+
if (sortBy) {
|
|
948
|
+
params.set("sortBy", sortBy);
|
|
949
|
+
params.set("sortDirection", sortDirection);
|
|
950
|
+
}
|
|
951
|
+
if (filterQuery) params.set("filter", filterQuery);
|
|
952
|
+
if (searchQuery) params.set("search", searchQuery);
|
|
953
|
+
const query = params.toString();
|
|
954
|
+
const res = await fetch(`/api/admin/export/${encodeURIComponent(currentTable)}/csv${query ? `?${query}` : ""}`);
|
|
955
|
+
if (!res.ok) {
|
|
956
|
+
const data = await res.json().catch(() => ({}));
|
|
957
|
+
throw new Error(typeof data.error === "string" ? data.error : "CSV export failed");
|
|
958
|
+
}
|
|
959
|
+
const blob = await res.blob();
|
|
960
|
+
const link = document.createElement("a");
|
|
961
|
+
link.href = URL.createObjectURL(blob);
|
|
962
|
+
link.download = filenameFromDisposition(res.headers.get("content-disposition")) ?? `${currentTable}.csv`;
|
|
963
|
+
link.click();
|
|
964
|
+
URL.revokeObjectURL(link.href);
|
|
965
|
+
toast(isFiltered ? "Filtered CSV exported" : "CSV exported");
|
|
966
|
+
};
|
|
967
|
+
const onImportCommitted = async (run) => {
|
|
968
|
+
setExpandedIndex(null);
|
|
969
|
+
await load();
|
|
970
|
+
toast(run.status === "reverted" ? "Import reverted" : `Imported ${run.insertedRows} ${run.insertedRows === 1 ? "row" : "rows"}`);
|
|
971
|
+
};
|
|
972
|
+
const rowHeight = rowHeightOptions.find((option) => option.id === rowHeightId)?.height ?? 40;
|
|
973
|
+
const expandedRow = expandedIndex !== null ? rows[expandedIndex] : void 0;
|
|
974
|
+
const exportDisabledReason = !table ? "Choose a table first" : dirty ? "Save or discard grid edits before exporting" : "";
|
|
975
|
+
const importDisabledReason = !table ? "Choose a table first" : dirty ? "Save or discard grid edits before importing" : currentTableReadOnly ? currentTableIsSystem ? "Enable system-table editing first" : "This table is read-only" : "";
|
|
976
|
+
const applyColumnSort = (columnKey, direction) => {
|
|
977
|
+
if (dirty) return;
|
|
978
|
+
setSortBy(columnKey);
|
|
979
|
+
setSortDirection(direction);
|
|
980
|
+
};
|
|
981
|
+
const GridContextMenu = ({ clientX, clientY, cursorIndex: rawCursorIndex, items, close }) => {
|
|
982
|
+
const menuRef = useRef2(null);
|
|
983
|
+
const cursorIndex = rawCursorIndex;
|
|
984
|
+
const columnKeys = table ? Object.keys(table.columns) : [];
|
|
985
|
+
const cursorColumnKey = cursorIndex && cursorIndex.col >= 0 ? columnKeys[cursorIndex.col] : void 0;
|
|
986
|
+
const cursorColumn = cursorColumnKey && table ? table.columns[cursorColumnKey] : void 0;
|
|
987
|
+
const sortColumnKey = cursorIndex?.row === -1 ? cursorColumnKey : void 0;
|
|
988
|
+
const sortColumn = sortColumnKey && table && table.columns[sortColumnKey]?.kind !== "secret" ? table.columns[sortColumnKey] : void 0;
|
|
989
|
+
const cursorRowIndex = cursorIndex && cursorIndex.row >= 0 && cursorIndex.row < rows.length ? cursorIndex.row : null;
|
|
990
|
+
const isDataCell = Boolean(cursorRowIndex !== null && cursorColumnKey && cursorColumn);
|
|
991
|
+
const cellValue = isDataCell && cursorRowIndex !== null && cursorColumnKey ? rows[cursorRowIndex]?.[cursorColumnKey] : void 0;
|
|
992
|
+
const clearValueDisabledReason = !isDataCell ? "Choose a cell to clear" : currentTableReadOnly ? "This table is read-only" : !cursorColumn || !columnAllowsClear(cursorColumn) ? "This field cannot be cleared" : isNullValue2(cellValue) ? "Value is already empty" : "";
|
|
993
|
+
const canClearValue = isDataCell && cursorColumnKey && cursorColumn && !currentTableReadOnly && columnAllowsClear(cursorColumn) && !isNullValue2(cellValue);
|
|
994
|
+
useEffect3(() => {
|
|
995
|
+
const onMouseDown = (event) => {
|
|
996
|
+
if (!menuRef.current?.contains(event.target)) close();
|
|
997
|
+
};
|
|
998
|
+
document.addEventListener("mousedown", onMouseDown);
|
|
999
|
+
return () => document.removeEventListener("mousedown", onMouseDown);
|
|
1000
|
+
}, [close]);
|
|
1001
|
+
const sort = (direction) => {
|
|
1002
|
+
if (!sortColumnKey || dirty) return;
|
|
1003
|
+
applyColumnSort(sortColumnKey, direction);
|
|
1004
|
+
close();
|
|
1005
|
+
};
|
|
1006
|
+
const editRecord = () => {
|
|
1007
|
+
if (cursorRowIndex === null) return;
|
|
1008
|
+
setExpandedIndex(cursorRowIndex);
|
|
1009
|
+
close();
|
|
1010
|
+
};
|
|
1011
|
+
const clearValue = () => {
|
|
1012
|
+
if (!canClearValue || cursorRowIndex === null || !cursorColumnKey) return;
|
|
1013
|
+
setRows((current) => current.map((row, index) => index === cursorRowIndex ? { ...row, [cursorColumnKey]: null } : row));
|
|
1014
|
+
close();
|
|
1015
|
+
};
|
|
1016
|
+
return /* @__PURE__ */ React4.createElement("div", { className: "dsg-context-menu", style: { left: `${clientX}px`, top: `${clientY}px` }, ref: menuRef }, items.map((item, index) => /* @__PURE__ */ React4.createElement("div", { key: `${item.type}-${index}`, onClick: item.action, className: "dsg-context-menu-item" }, renderRecordContextMenuItem(item))), isDataCell ? /* @__PURE__ */ React4.createElement("button", { type: "button", className: "dsg-context-menu-item rb-context-menu-button", onMouseDown: (event) => event.preventDefault(), onClick: editRecord }, "Edit record") : null, isDataCell ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement("div", { className: "rb-context-menu-separator" }), /* @__PURE__ */ React4.createElement(
|
|
1017
|
+
"button",
|
|
1018
|
+
{
|
|
1019
|
+
type: "button",
|
|
1020
|
+
className: "dsg-context-menu-item rb-context-menu-button",
|
|
1021
|
+
disabled: !canClearValue,
|
|
1022
|
+
title: clearValueDisabledReason || void 0,
|
|
1023
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
1024
|
+
onClick: clearValue
|
|
1025
|
+
},
|
|
1026
|
+
"Clear value"
|
|
1027
|
+
)) : null, sortColumnKey && sortColumn ? /* @__PURE__ */ React4.createElement(React4.Fragment, null, items.length ? /* @__PURE__ */ React4.createElement("div", { className: "rb-context-menu-separator" }) : null, /* @__PURE__ */ React4.createElement("div", { className: "rb-context-menu-label" }, sortColumn.label), /* @__PURE__ */ React4.createElement("button", { type: "button", className: "dsg-context-menu-item rb-context-menu-button", disabled: dirty, onMouseDown: (event) => event.preventDefault(), onClick: () => sort("asc") }, "Sort ascending", sortBy === sortColumnKey && sortDirection === "asc" ? " (active)" : ""), /* @__PURE__ */ React4.createElement("button", { type: "button", className: "dsg-context-menu-item rb-context-menu-button", disabled: dirty, onMouseDown: (event) => event.preventDefault(), onClick: () => sort("desc") }, "Sort descending", sortBy === sortColumnKey && sortDirection === "desc" ? " (active)" : ""), sortBy === sortColumnKey ? /* @__PURE__ */ React4.createElement(
|
|
1028
|
+
"button",
|
|
1029
|
+
{
|
|
1030
|
+
type: "button",
|
|
1031
|
+
className: "dsg-context-menu-item rb-context-menu-button",
|
|
1032
|
+
disabled: dirty,
|
|
1033
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
1034
|
+
onClick: () => {
|
|
1035
|
+
if (dirty) return;
|
|
1036
|
+
setSortBy("");
|
|
1037
|
+
close();
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
"Clear sort"
|
|
1041
|
+
) : null) : null);
|
|
1042
|
+
};
|
|
1043
|
+
return /* @__PURE__ */ React4.createElement("section", null, /* @__PURE__ */ React4.createElement("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React4.createElement("div", null, /* @__PURE__ */ React4.createElement("h1", { className: "m-0 text-xl font-semibold leading-tight tracking-tight" }, "Data"), /* @__PURE__ */ React4.createElement("p", { className: "m-0 mt-1 text-sm text-zinc-500" }, "Browse and edit your application tables.")), /* @__PURE__ */ React4.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React4.createElement("button", { className: secondaryButton, disabled: Boolean(importDisabledReason), title: importDisabledReason || "Import CSV", onClick: () => setShowImportDialog(true) }, icons.file, "Import CSV"), /* @__PURE__ */ React4.createElement(
|
|
1044
|
+
"button",
|
|
1045
|
+
{
|
|
1046
|
+
className: secondaryButton,
|
|
1047
|
+
disabled: Boolean(exportDisabledReason),
|
|
1048
|
+
title: exportDisabledReason || (isFiltered ? "Export filtered CSV" : "Export CSV"),
|
|
1049
|
+
onClick: () => void exportCsv().catch((error) => toast(error.message, "error"))
|
|
1050
|
+
},
|
|
1051
|
+
icons.data,
|
|
1052
|
+
isFiltered ? "Export filtered CSV" : "Export CSV"
|
|
1053
|
+
), dirty ? /* @__PURE__ */ React4.createElement("button", { className: secondaryButton, onClick: discardLocalChanges }, "Discard") : null, /* @__PURE__ */ React4.createElement(
|
|
1054
|
+
"button",
|
|
1055
|
+
{
|
|
1056
|
+
className: primaryButton,
|
|
1057
|
+
disabled: currentTableReadOnly || !dirty,
|
|
1058
|
+
title: currentTableReadOnly ? "Enable system table editing first" : !dirty ? "No changes to save" : void 0,
|
|
1059
|
+
onClick: () => void save().catch((error) => toast(error.message, "error"))
|
|
1060
|
+
},
|
|
1061
|
+
changeCounts.total ? `Save ${changeCounts.total} ${changeCounts.total === 1 ? "change" : "changes"}` : "Save changes"
|
|
1062
|
+
))), /* @__PURE__ */ React4.createElement("div", { className: "mb-4 grid gap-2 md:grid-cols-[minmax(0,1fr)_auto] md:items-start" }, /* @__PURE__ */ React4.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React4.createElement("div", { className: "inline-flex max-w-full rounded-lg bg-zinc-100 p-1" }, /* @__PURE__ */ React4.createElement("div", { className: "flex max-w-full flex-wrap gap-1" }, visibleTableNames.map((name) => /* @__PURE__ */ React4.createElement(
|
|
1063
|
+
"button",
|
|
1064
|
+
{
|
|
1065
|
+
key: name,
|
|
1066
|
+
className: cx(
|
|
1067
|
+
"h-8 rounded-md px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400/70",
|
|
1068
|
+
name === currentTable ? `bg-white text-zinc-900 ring-1 ring-zinc-200 ${chipShadow}` : "text-zinc-500 hover:text-zinc-900"
|
|
1069
|
+
),
|
|
1070
|
+
onClick: () => requestTableChange(name)
|
|
1071
|
+
},
|
|
1072
|
+
/* @__PURE__ */ React4.createElement("span", { className: "inline-flex items-center gap-1.5" }, boot.schema[name].label, boot.schema[name].system ? /* @__PURE__ */ React4.createElement("span", { className: "rounded bg-zinc-200 px-1 py-0.5 text-[10px] font-semibold uppercase text-zinc-500" }, "system") : null)
|
|
1073
|
+
))))), /* @__PURE__ */ React4.createElement("div", { className: "grid shrink-0 justify-self-start gap-2 md:justify-self-end" }, systemTableCount ? /* @__PURE__ */ React4.createElement("label", { className: "inline-flex h-9 items-center gap-2 rounded-lg border border-zinc-200 bg-white px-3 text-sm font-medium text-zinc-600" }, /* @__PURE__ */ React4.createElement("input", { type: "checkbox", checked: showSystemTables, onChange: (event) => requestShowSystemTables(event.target.checked) }), "System tables") : null)), currentTableIsSystem ? /* @__PURE__ */ React4.createElement("div", { className: cx(cardClass, "mb-4 flex flex-wrap items-center justify-between gap-3 px-4 py-3 text-sm") }, /* @__PURE__ */ React4.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React4.createElement("div", { className: "font-medium text-zinc-900" }, currentTableHasId ? "System table editing is off." : "This system table stays read-only."), /* @__PURE__ */ React4.createElement("div", { className: "mt-0.5 text-zinc-500" }, currentTableHasId ? "Enable the danger setting only for maintenance changes." : "This grid can only save system tables with an id column.")), currentTableHasId ? /* @__PURE__ */ React4.createElement("label", { className: "inline-flex h-9 shrink-0 items-center gap-2 rounded-lg border border-red-200 bg-white px-3 font-medium text-red-700" }, /* @__PURE__ */ React4.createElement("input", { type: "checkbox", checked: allowSystemTableEditing, onChange: (event) => setAllowSystemTableEditing(event.target.checked) }), "Edit system tables") : null) : null, /* @__PURE__ */ React4.createElement("div", { className: "mb-4 flex flex-wrap items-center gap-2" }, table ? /* @__PURE__ */ React4.createElement(
|
|
1074
|
+
FilterBar,
|
|
1075
|
+
{
|
|
1076
|
+
table,
|
|
1077
|
+
references,
|
|
1078
|
+
jsonModes,
|
|
1079
|
+
tagSuggestions,
|
|
1080
|
+
filters,
|
|
1081
|
+
onFiltersChange: setFilters,
|
|
1082
|
+
search: searchText,
|
|
1083
|
+
onSearchChange: setSearchText,
|
|
1084
|
+
disabled: dirty
|
|
1085
|
+
}
|
|
1086
|
+
) : null, /* @__PURE__ */ React4.createElement("div", { className: "ml-auto" }, /* @__PURE__ */ React4.createElement(RowHeightToggle, { value: rowHeightId, onChange: requestRowHeight }))), /* @__PURE__ */ React4.createElement("div", { className: cx(cardClass, "relative overflow-hidden", !rows.length && isFiltered && !dirty && "min-h-64") }, !rows.length && isFiltered && !dirty ? /* @__PURE__ */ React4.createElement("div", { className: "pointer-events-none absolute inset-x-0 top-24 z-10 flex justify-center" }, /* @__PURE__ */ React4.createElement("div", { className: "pointer-events-auto flex flex-col items-center gap-2 px-6 py-4 text-center" }, /* @__PURE__ */ React4.createElement("div", { className: "text-sm font-medium text-zinc-900" }, "No records match"), /* @__PURE__ */ React4.createElement("div", { className: "text-[13px] text-zinc-500" }, "Try adjusting the filters or search."), /* @__PURE__ */ React4.createElement(
|
|
1087
|
+
"button",
|
|
1088
|
+
{
|
|
1089
|
+
type: "button",
|
|
1090
|
+
className: "mt-1 inline-flex h-8 items-center rounded-lg border border-zinc-200 bg-white px-3 text-sm font-medium text-zinc-700 transition-colors hover:border-zinc-300 hover:bg-zinc-50",
|
|
1091
|
+
onClick: () => {
|
|
1092
|
+
setFilters([]);
|
|
1093
|
+
setSearchText("");
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
"Clear filters"
|
|
1097
|
+
))) : null, /* @__PURE__ */ React4.createElement(
|
|
1098
|
+
DynamicDataSheetGrid,
|
|
1099
|
+
{
|
|
1100
|
+
key: currentTable,
|
|
1101
|
+
value: rows,
|
|
1102
|
+
onChange: setRows,
|
|
1103
|
+
columns,
|
|
1104
|
+
gutterColumn,
|
|
1105
|
+
height: gridHeight,
|
|
1106
|
+
rowHeight,
|
|
1107
|
+
createRow: () => table ? defaultRow(table) : {},
|
|
1108
|
+
duplicateRow: ({ rowData }) => table ? duplicateRowData(table, rowData) : { ...rowData },
|
|
1109
|
+
lockRows: currentTableReadOnly,
|
|
1110
|
+
contextMenuComponent: GridContextMenu,
|
|
1111
|
+
addRowsComponent: currentTableReadOnly ? false : ({ addRows }) => /* @__PURE__ */ React4.createElement("div", { className: "flex w-full items-center justify-between bg-white px-2 py-1.5" }, /* @__PURE__ */ React4.createElement(
|
|
1112
|
+
"button",
|
|
1113
|
+
{
|
|
1114
|
+
type: "button",
|
|
1115
|
+
className: "inline-flex h-8 items-center gap-2 rounded-md px-2.5 text-sm font-medium text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-900",
|
|
1116
|
+
onClick: () => addRows(1)
|
|
1117
|
+
},
|
|
1118
|
+
icons.plus,
|
|
1119
|
+
"New record"
|
|
1120
|
+
), /* @__PURE__ */ React4.createElement("span", { className: "px-2 text-xs tabular-nums text-zinc-400" }, rows.length, " ", rows.length === 1 ? "record" : "records", isFiltered ? " \xB7 filtered" : ""))
|
|
1121
|
+
}
|
|
1122
|
+
)), expandedRow && table ? /* @__PURE__ */ React4.createElement(
|
|
1123
|
+
RecordPanel,
|
|
1124
|
+
{
|
|
1125
|
+
table,
|
|
1126
|
+
row: expandedRow,
|
|
1127
|
+
rowIndex: expandedIndex ?? 0,
|
|
1128
|
+
total: rows.length,
|
|
1129
|
+
references,
|
|
1130
|
+
jsonModes,
|
|
1131
|
+
tagSuggestions,
|
|
1132
|
+
readOnly: currentTableReadOnly,
|
|
1133
|
+
getRowId: rowKey,
|
|
1134
|
+
getRecordTitle: recordTitle,
|
|
1135
|
+
onChange: (next) => setRows((current) => current.map((row, index) => index === expandedIndex ? next : row)),
|
|
1136
|
+
onNavigate: (delta) => setExpandedIndex((current) => Math.max(0, Math.min(rows.length - 1, (current ?? 0) + delta))),
|
|
1137
|
+
onDelete: () => {
|
|
1138
|
+
setRows((current) => current.filter((_, index) => index !== expandedIndex));
|
|
1139
|
+
setExpandedIndex(null);
|
|
1140
|
+
},
|
|
1141
|
+
onClose: () => setExpandedIndex(null)
|
|
1142
|
+
}
|
|
1143
|
+
) : null, pendingTable ? /* @__PURE__ */ React4.createElement(
|
|
1144
|
+
UnsavedChangesDialog,
|
|
1145
|
+
{
|
|
1146
|
+
onCancel: () => setPendingTable(null),
|
|
1147
|
+
onDiscard: () => {
|
|
1148
|
+
const next = pendingTable;
|
|
1149
|
+
setPendingTable(null);
|
|
1150
|
+
discardLocalChanges();
|
|
1151
|
+
setCurrentTable(next);
|
|
1152
|
+
onCurrentTableChange(next);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
) : null, pendingShowSystemTables !== null ? /* @__PURE__ */ React4.createElement(
|
|
1156
|
+
UnsavedChangesDialog,
|
|
1157
|
+
{
|
|
1158
|
+
onCancel: () => setPendingShowSystemTables(null),
|
|
1159
|
+
onDiscard: () => {
|
|
1160
|
+
const next = pendingShowSystemTables;
|
|
1161
|
+
setPendingShowSystemTables(null);
|
|
1162
|
+
discardLocalChanges();
|
|
1163
|
+
setShowSystemTables(next);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
) : null, showImportDialog && table ? /* @__PURE__ */ React4.createElement(
|
|
1167
|
+
CsvImportDialog,
|
|
1168
|
+
{
|
|
1169
|
+
tableName: currentTable,
|
|
1170
|
+
table,
|
|
1171
|
+
references,
|
|
1172
|
+
allowSystemTables: currentTableIsSystem && allowSystemTableEditing,
|
|
1173
|
+
adminOrgId,
|
|
1174
|
+
toast,
|
|
1175
|
+
onClose: () => setShowImportDialog(false),
|
|
1176
|
+
onCommitted: onImportCommitted
|
|
1177
|
+
}
|
|
1178
|
+
) : null);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// admin-src/NotificationsView.tsx
|
|
1182
|
+
import React5, { useEffect as useEffect4, useState as useState4 } from "react";
|
|
1183
|
+
var deliveryStatusChoices = ["pending", "processing", "delivered", "failed", "permanent_failure", "skipped_local"];
|
|
1184
|
+
var inboundStatusChoices = ["verified", "processed", "handler_failed", "verification_failed", "parse_failed"];
|
|
1185
|
+
function adminUrl(path, adminOrgId, params = {}) {
|
|
1186
|
+
const search = new URLSearchParams();
|
|
1187
|
+
if (adminOrgId) search.set("adminOrgId", adminOrgId);
|
|
1188
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1189
|
+
if (value) search.set(key, value);
|
|
1190
|
+
}
|
|
1191
|
+
const query = search.toString();
|
|
1192
|
+
return query ? `${path}?${query}` : path;
|
|
1193
|
+
}
|
|
1194
|
+
function asText(value, fallback = "-") {
|
|
1195
|
+
if (value === null || value === void 0 || value === "") return fallback;
|
|
1196
|
+
return String(value);
|
|
1197
|
+
}
|
|
1198
|
+
function asCount(value) {
|
|
1199
|
+
return Number(value ?? 0) || 0;
|
|
1200
|
+
}
|
|
1201
|
+
function unique(values) {
|
|
1202
|
+
return Array.from(new Set(values.filter((value) => Boolean(value)))).sort();
|
|
1203
|
+
}
|
|
1204
|
+
function compactId(value) {
|
|
1205
|
+
const text = asText(value);
|
|
1206
|
+
return text.length > 22 ? `${text.slice(0, 10)}...${text.slice(-8)}` : text;
|
|
1207
|
+
}
|
|
1208
|
+
function redactForDisplay(value, key = "", depth = 0) {
|
|
1209
|
+
if (depth > 5) return "[truncated]";
|
|
1210
|
+
const lower = key.toLowerCase();
|
|
1211
|
+
if (/secret|token|authorization|signature|password|cookie|authheader/.test(lower)) return "[redacted]";
|
|
1212
|
+
if (/webhook|url|href/.test(lower) && typeof value === "string" && /^https?:/i.test(value)) return "[redacted-url]";
|
|
1213
|
+
if (/recipients|recipientlist|channeltargets/.test(lower) && Array.isArray(value) && value.length > 3) return `[${value.length} entries redacted]`;
|
|
1214
|
+
if (typeof value === "string") {
|
|
1215
|
+
const trimmed = value.trim();
|
|
1216
|
+
if ((trimmed.startsWith("{") || trimmed.startsWith("[")) && trimmed.length < 2e4) {
|
|
1217
|
+
try {
|
|
1218
|
+
return redactForDisplay(JSON.parse(trimmed), key, depth + 1);
|
|
1219
|
+
} catch {
|
|
1220
|
+
return value.length > 1200 ? `${value.slice(0, 1200)}...` : value;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return value.length > 1200 ? `${value.slice(0, 1200)}...` : value;
|
|
1224
|
+
}
|
|
1225
|
+
if (Array.isArray(value)) return value.slice(0, 20).map((item) => redactForDisplay(item, key, depth + 1));
|
|
1226
|
+
if (value && typeof value === "object") {
|
|
1227
|
+
return Object.fromEntries(Object.entries(value).map(([childKey, childValue]) => [childKey, redactForDisplay(childValue, childKey, depth + 1)]));
|
|
1228
|
+
}
|
|
1229
|
+
return value;
|
|
1230
|
+
}
|
|
1231
|
+
function JsonPreview({ value, empty = "-" }) {
|
|
1232
|
+
if (value === null || value === void 0 || value === "") return /* @__PURE__ */ React5.createElement("span", { className: "text-zinc-400" }, empty);
|
|
1233
|
+
const redacted = redactForDisplay(value);
|
|
1234
|
+
const text = typeof redacted === "string" ? redacted : JSON.stringify(redacted, null, 2);
|
|
1235
|
+
return /* @__PURE__ */ React5.createElement("pre", { className: "m-0 max-h-64 overflow-auto whitespace-pre-wrap rounded-lg bg-zinc-950 p-3 font-mono text-xs leading-relaxed text-zinc-100" }, text);
|
|
1236
|
+
}
|
|
1237
|
+
function StatusPill2({ status }) {
|
|
1238
|
+
const tone = status === "delivered" || status === "processed" ? "border-emerald-200 bg-emerald-50 text-emerald-700" : status === "failed" || status === "permanent_failure" || status === "handler_failed" || status === "verification_failed" || status === "parse_failed" ? "border-red-200 bg-red-50 text-red-700" : status === "pending" || status === "processing" || status === "verified" ? "border-blue-200 bg-blue-50 text-blue-700" : "border-zinc-200 bg-zinc-50 text-zinc-600";
|
|
1239
|
+
return /* @__PURE__ */ React5.createElement("span", { className: cx("inline-flex rounded-md border px-1.5 py-0.5 font-mono text-[11px]", tone) }, status);
|
|
1240
|
+
}
|
|
1241
|
+
function CountCard({ label, detail, count }) {
|
|
1242
|
+
return /* @__PURE__ */ React5.createElement("div", { className: cx(cardClass, "min-w-0 p-4") }, /* @__PURE__ */ React5.createElement("div", { className: "truncate text-xs font-semibold uppercase tracking-wider text-zinc-500" }, label), /* @__PURE__ */ React5.createElement("div", { className: "mt-2 text-2xl font-semibold tabular-nums text-zinc-950" }, count), detail ? /* @__PURE__ */ React5.createElement("div", { className: "mt-1 truncate text-xs text-zinc-500" }, detail) : null);
|
|
1243
|
+
}
|
|
1244
|
+
function FieldValue({ label, value }) {
|
|
1245
|
+
return /* @__PURE__ */ React5.createElement("div", null, /* @__PURE__ */ React5.createElement("div", { className: "text-xs text-zinc-500" }, label), /* @__PURE__ */ React5.createElement("div", { className: "mt-0.5 break-all text-sm text-zinc-800" }, value));
|
|
1246
|
+
}
|
|
1247
|
+
function NotificationsView({ toast, adminOrgId }) {
|
|
1248
|
+
const [summary, setSummary] = useState4({ adapters: [], deliveryCounts: [], inboundCounts: [] });
|
|
1249
|
+
const [deliveries, setDeliveries] = useState4([]);
|
|
1250
|
+
const [inbound, setInbound] = useState4([]);
|
|
1251
|
+
const [throttles, setThrottles] = useState4([]);
|
|
1252
|
+
const [pushSubscriptions, setPushSubscriptions] = useState4([]);
|
|
1253
|
+
const [deliveryStatus, setDeliveryStatus] = useState4("");
|
|
1254
|
+
const [deliveryChannel, setDeliveryChannel] = useState4("");
|
|
1255
|
+
const [deliveryAdapter, setDeliveryAdapter] = useState4("");
|
|
1256
|
+
const [inboundStatus, setInboundStatus] = useState4("");
|
|
1257
|
+
const [inboundAdapter, setInboundAdapter] = useState4("");
|
|
1258
|
+
const [selectedDelivery, setSelectedDelivery] = useState4(null);
|
|
1259
|
+
const [deliveryDetail, setDeliveryDetail] = useState4(null);
|
|
1260
|
+
const [selectedInbound, setSelectedInbound] = useState4(null);
|
|
1261
|
+
const [inboundDetail, setInboundDetail] = useState4(null);
|
|
1262
|
+
const [supportSubscription, setSupportSubscription] = useState4({ kind: "support.request.created", enabled: false, channels: ["feed"] });
|
|
1263
|
+
const [loading, setLoading] = useState4(true);
|
|
1264
|
+
const [retrying, setRetrying] = useState4(false);
|
|
1265
|
+
const [resettingThrottle, setResettingThrottle] = useState4(null);
|
|
1266
|
+
const [savingSubscription, setSavingSubscription] = useState4(false);
|
|
1267
|
+
const [revokingPush, setRevokingPush] = useState4(null);
|
|
1268
|
+
const load = async () => {
|
|
1269
|
+
setLoading(true);
|
|
1270
|
+
try {
|
|
1271
|
+
const [summaryData, deliveryData, inboundData, throttleData, subscriptionData, pushData] = await Promise.all([
|
|
1272
|
+
api(adminUrl("/api/admin/notifications/summary", adminOrgId)),
|
|
1273
|
+
api(
|
|
1274
|
+
adminUrl("/api/admin/notifications/deliveries", adminOrgId, { status: deliveryStatus, channel: deliveryChannel, adapter: deliveryAdapter })
|
|
1275
|
+
),
|
|
1276
|
+
api(adminUrl("/api/admin/notifications/inbound", adminOrgId, { status: inboundStatus, adapter: inboundAdapter })),
|
|
1277
|
+
api(adminUrl("/api/admin/notifications/throttles", adminOrgId)),
|
|
1278
|
+
api(adminUrl("/api/admin/support/notification-subscription", adminOrgId)),
|
|
1279
|
+
boot.webPushEnabled ? api(adminUrl("/api/admin/notifications/push/subscriptions", adminOrgId)) : Promise.resolve({ counts: [], subscriptions: [] })
|
|
1280
|
+
]);
|
|
1281
|
+
setSummary({
|
|
1282
|
+
adapters: summaryData.adapters ?? [],
|
|
1283
|
+
deliveryCounts: summaryData.deliveryCounts ?? [],
|
|
1284
|
+
inboundCounts: summaryData.inboundCounts ?? []
|
|
1285
|
+
});
|
|
1286
|
+
setDeliveries(deliveryData.deliveries ?? []);
|
|
1287
|
+
setInbound(inboundData.events ?? []);
|
|
1288
|
+
setThrottles(throttleData.throttles ?? []);
|
|
1289
|
+
setPushSubscriptions(pushData.subscriptions ?? []);
|
|
1290
|
+
setSupportSubscription({
|
|
1291
|
+
kind: subscriptionData.kind,
|
|
1292
|
+
enabled: subscriptionData.enabled,
|
|
1293
|
+
channels: subscriptionData.channels?.length ? subscriptionData.channels : ["feed"]
|
|
1294
|
+
});
|
|
1295
|
+
} finally {
|
|
1296
|
+
setLoading(false);
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
useEffect4(() => {
|
|
1300
|
+
void load().catch((error) => toast(error.message, "error"));
|
|
1301
|
+
}, [adminOrgId, deliveryStatus, deliveryChannel, deliveryAdapter, inboundStatus, inboundAdapter]);
|
|
1302
|
+
const selectDelivery = async (row) => {
|
|
1303
|
+
setSelectedDelivery(row);
|
|
1304
|
+
const detail = await api(adminUrl(`/api/admin/notifications/deliveries/${encodeURIComponent(row.id)}`, adminOrgId));
|
|
1305
|
+
setDeliveryDetail(detail);
|
|
1306
|
+
};
|
|
1307
|
+
const selectInbound = async (row) => {
|
|
1308
|
+
setSelectedInbound(row);
|
|
1309
|
+
const detail = await api(adminUrl(`/api/admin/notifications/inbound/${encodeURIComponent(row.id)}`, adminOrgId));
|
|
1310
|
+
setInboundDetail(detail.detail ?? detail);
|
|
1311
|
+
};
|
|
1312
|
+
const retryDelivery = async () => {
|
|
1313
|
+
if (!selectedDelivery) return;
|
|
1314
|
+
setRetrying(true);
|
|
1315
|
+
try {
|
|
1316
|
+
await api(adminUrl(`/api/admin/notifications/deliveries/${encodeURIComponent(selectedDelivery.id)}/retry`, adminOrgId), { method: "POST", body: "{}" });
|
|
1317
|
+
toast("Retry queued");
|
|
1318
|
+
setSelectedDelivery(null);
|
|
1319
|
+
setDeliveryDetail(null);
|
|
1320
|
+
await load();
|
|
1321
|
+
} finally {
|
|
1322
|
+
setRetrying(false);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
const resetThrottle = async (row) => {
|
|
1326
|
+
setResettingThrottle(row.id);
|
|
1327
|
+
try {
|
|
1328
|
+
await api(adminUrl(`/api/admin/notifications/throttles/${encodeURIComponent(row.id)}/reset`, adminOrgId), { method: "POST", body: "{}" });
|
|
1329
|
+
toast("Throttle window reset");
|
|
1330
|
+
await load();
|
|
1331
|
+
} finally {
|
|
1332
|
+
setResettingThrottle(null);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
const updateSupportSubscription = async (enabled) => {
|
|
1336
|
+
setSavingSubscription(true);
|
|
1337
|
+
try {
|
|
1338
|
+
const result = await api(adminUrl("/api/admin/support/notification-subscription", adminOrgId), {
|
|
1339
|
+
method: "PUT",
|
|
1340
|
+
body: JSON.stringify({ enabled, channels: supportSubscription.channels?.length ? supportSubscription.channels : ["feed"] })
|
|
1341
|
+
});
|
|
1342
|
+
setSupportSubscription(result.subscription);
|
|
1343
|
+
toast(enabled ? "Support request notifications enabled" : "Support request notifications disabled");
|
|
1344
|
+
} finally {
|
|
1345
|
+
setSavingSubscription(false);
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
const revokePushSubscription = async (row) => {
|
|
1349
|
+
setRevokingPush(row.id);
|
|
1350
|
+
try {
|
|
1351
|
+
await api(adminUrl(`/api/admin/notifications/push/subscriptions/${encodeURIComponent(row.id)}`, adminOrgId), { method: "DELETE", body: "{}" });
|
|
1352
|
+
toast("Push subscription revoked");
|
|
1353
|
+
await load();
|
|
1354
|
+
} finally {
|
|
1355
|
+
setRevokingPush(null);
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
const deliveryStatusOptions = unique([...deliveryStatusChoices, ...summary.deliveryCounts.map((row) => row.status), ...deliveries.map((row) => asText(row.status, ""))]);
|
|
1359
|
+
const deliveryChannelOptions = unique([...summary.adapters.map((adapter) => adapter.channel), ...summary.deliveryCounts.map((row) => row.channel), ...deliveries.map((row) => asText(row.channel, ""))]);
|
|
1360
|
+
const deliveryAdapterOptions = unique([...summary.adapters.map((adapter) => adapter.adapter), ...summary.deliveryCounts.map((row) => row.adapter), ...deliveries.map((row) => asText(row.adapter, ""))]);
|
|
1361
|
+
const inboundStatusOptions = unique([...inboundStatusChoices, ...summary.inboundCounts.map((row) => row.status), ...inbound.map((row) => asText(row.status, ""))]);
|
|
1362
|
+
const inboundAdapterOptions = unique([...summary.inboundCounts.map((row) => row.adapter), ...inbound.map((row) => asText(row.adapter, ""))]);
|
|
1363
|
+
const detailDelivery = deliveryDetail?.delivery ?? selectedDelivery;
|
|
1364
|
+
const attempts = deliveryDetail?.attempts ?? [];
|
|
1365
|
+
const retryable = detailDelivery ? ["failed", "permanent_failure"].includes(asText(detailDelivery.status, "")) : false;
|
|
1366
|
+
const totalDeliveries = summary.deliveryCounts.reduce((sum, row) => sum + asCount(row.count), 0);
|
|
1367
|
+
const failedDeliveries = summary.deliveryCounts.filter((row) => ["failed", "permanent_failure"].includes(asText(row.status, ""))).reduce((sum, row) => sum + asCount(row.count), 0);
|
|
1368
|
+
const totalInbound = summary.inboundCounts.reduce((sum, row) => sum + asCount(row.count), 0);
|
|
1369
|
+
const totalSuppressed = throttles.reduce((sum, row) => sum + asCount(row.suppressedCount), 0);
|
|
1370
|
+
return /* @__PURE__ */ React5.createElement("section", null, /* @__PURE__ */ React5.createElement("div", { className: "mb-6 flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React5.createElement("div", null, /* @__PURE__ */ React5.createElement("h1", { className: "m-0 text-xl font-semibold leading-tight tracking-tight" }, "Notifications"), /* @__PURE__ */ React5.createElement("p", { className: "m-0 mt-1 text-sm text-zinc-500" }, "Delivery, retry, and inbound webhook evidence.")), /* @__PURE__ */ React5.createElement("button", { className: secondaryButton, onClick: () => void load().catch((error) => toast(error.message, "error")) }, icons.refresh, "Refresh")), /* @__PURE__ */ React5.createElement("div", { className: "mb-6 grid gap-3 sm:grid-cols-2 xl:grid-cols-5" }, /* @__PURE__ */ React5.createElement(CountCard, { label: "Adapters", detail: summary.adapters.length ? "External channels" : "Feed only", count: summary.adapters.length }), /* @__PURE__ */ React5.createElement(CountCard, { label: "Deliveries", detail: "All statuses", count: totalDeliveries }), /* @__PURE__ */ React5.createElement(CountCard, { label: "Failures", detail: "Needs attention", count: failedDeliveries }), /* @__PURE__ */ React5.createElement(CountCard, { label: "Inbound events", detail: "All statuses", count: totalInbound }), /* @__PURE__ */ React5.createElement(CountCard, { label: "Throttled", detail: "Active suppressions", count: totalSuppressed })), /* @__PURE__ */ React5.createElement("div", { className: cx(cardClass, "mb-6 flex flex-wrap items-center justify-between gap-4 p-4") }, /* @__PURE__ */ React5.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React5.createElement("div", { className: "text-sm font-semibold text-zinc-950" }, "Support requests"), /* @__PURE__ */ React5.createElement("div", { className: "mt-1 text-sm text-zinc-500" }, "Notify me when a new support request is created.")), /* @__PURE__ */ React5.createElement(
|
|
1371
|
+
"button",
|
|
1372
|
+
{
|
|
1373
|
+
className: supportSubscription.enabled ? secondaryButton : primaryButton,
|
|
1374
|
+
disabled: savingSubscription,
|
|
1375
|
+
onClick: () => void updateSupportSubscription(!supportSubscription.enabled).catch((error) => toast(error.message, "error"))
|
|
1376
|
+
},
|
|
1377
|
+
icons.bell,
|
|
1378
|
+
supportSubscription.enabled ? "Subscribed" : "Subscribe"
|
|
1379
|
+
)), /* @__PURE__ */ React5.createElement("div", { className: cx(cardClass, "mb-6 overflow-hidden") }, /* @__PURE__ */ React5.createElement("div", { className: "border-b border-zinc-100 px-4 py-3 text-sm font-semibold" }, "Adapters"), summary.adapters.length ? /* @__PURE__ */ React5.createElement("div", { className: "grid gap-3 p-4 md:grid-cols-2 xl:grid-cols-3" }, summary.adapters.map((adapter) => /* @__PURE__ */ React5.createElement("div", { key: `${adapter.channel}:${adapter.adapter}`, className: "rounded-lg border border-zinc-200 p-3" }, /* @__PURE__ */ React5.createElement("div", { className: "flex flex-wrap items-center gap-2" }, /* @__PURE__ */ React5.createElement(StatusPill2, { status: adapter.channel }), /* @__PURE__ */ React5.createElement("span", { className: "font-mono text-xs text-zinc-600" }, adapter.adapter)), adapter.description ? /* @__PURE__ */ React5.createElement("div", { className: "mt-2 text-sm text-zinc-500" }, adapter.description) : null))) : /* @__PURE__ */ React5.createElement("div", { className: "px-4 py-6 text-sm text-zinc-500" }, "No external adapters are registered. Feed notifications remain available.")), boot.webPushEnabled ? /* @__PURE__ */ React5.createElement("div", { className: "mb-8" }, /* @__PURE__ */ React5.createElement("div", { className: "mb-3 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React5.createElement("div", null, /* @__PURE__ */ React5.createElement("h2", { className: "m-0 text-base font-semibold" }, "Push subscriptions"), /* @__PURE__ */ React5.createElement("p", { className: "m-0 mt-1 text-sm text-zinc-500" }, "Capability URLs and encryption keys stay hidden; only origins and endpoint hash prefixes are shown."))), /* @__PURE__ */ React5.createElement("div", { className: tableWrapClass }, /* @__PURE__ */ React5.createElement("table", { className: "w-full min-w-[960px] border-collapse" }, /* @__PURE__ */ React5.createElement("thead", null, /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("th", { className: thClass }, "User / device"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Push service"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Status"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Failures"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Last used"), /* @__PURE__ */ React5.createElement("th", { className: thClass }))), /* @__PURE__ */ React5.createElement("tbody", null, pushSubscriptions.map((row) => /* @__PURE__ */ React5.createElement("tr", { key: row.id, className: "transition-colors hover:bg-zinc-50/60" }, /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement("div", null, row.label || "Unnamed device"), /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-[11px] text-zinc-500" }, compactId(row.userId))), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement("div", { className: "text-sm" }, row.endpointOrigin), /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-[11px] text-zinc-500" }, row.endpointHashPrefix)), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement(StatusPill2, { status: row.status })), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "tabular-nums text-zinc-600") }, row.failureCount), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "whitespace-nowrap text-zinc-500") }, formatDateTime(row.lastUsedAt || row.lastSeenAt)), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "text-right") }, /* @__PURE__ */ React5.createElement("button", { className: dangerButton, disabled: revokingPush === row.id || row.status === "revoked", onClick: () => void revokePushSubscription(row).catch((error) => toast(error.message, "error")) }, icons.trash, "Revoke")))), !pushSubscriptions.length ? /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "py-10 text-center text-zinc-500"), colSpan: 6 }, loading ? "Loading push subscriptions..." : "No push subscriptions.")) : null)))) : null, /* @__PURE__ */ React5.createElement("div", { className: "mb-8" }, /* @__PURE__ */ React5.createElement("div", { className: "mb-3 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React5.createElement("h2", { className: "m-0 text-base font-semibold" }, "Throttles")), /* @__PURE__ */ React5.createElement("div", { className: tableWrapClass }, /* @__PURE__ */ React5.createElement("table", { className: "w-full min-w-[880px] border-collapse" }, /* @__PURE__ */ React5.createElement("thead", null, /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Key"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Recipient"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Reopens"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Suppressed"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Last suppressed"), /* @__PURE__ */ React5.createElement("th", { className: thClass }))), /* @__PURE__ */ React5.createElement("tbody", null, throttles.map((row) => {
|
|
1380
|
+
const lastSuppressed = row.lastSuppressed && typeof row.lastSuppressed === "object" ? row.lastSuppressed : null;
|
|
1381
|
+
return /* @__PURE__ */ React5.createElement("tr", { key: row.id, className: "transition-colors hover:bg-zinc-50/60" }, /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-xs" }, row.throttleKey), /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-[11px] text-zinc-500" }, compactId(row.id))), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "font-mono text-xs text-zinc-600") }, asText(row.maskedRecipientSubjectId ?? row.recipientSubjectId)), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "whitespace-nowrap text-zinc-500") }, formatDateTime(row.windowExpiresAt)), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "tabular-nums text-zinc-600") }, asText(row.suppressedCount, "0")), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement("div", { className: "max-w-[320px] truncate" }, asText(lastSuppressed?.title)), /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-[11px] text-zinc-500" }, formatDateTime(lastSuppressed?.createdAt))), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "text-right") }, /* @__PURE__ */ React5.createElement("button", { className: dangerButton, disabled: resettingThrottle === row.id, onClick: () => void resetThrottle(row).catch((error) => toast(error.message, "error")) }, icons.trash, "Reset")));
|
|
1382
|
+
}), !throttles.length ? /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "py-10 text-center text-zinc-500"), colSpan: 6 }, loading ? "Loading throttle windows..." : "No active notification throttle windows.")) : null)))), /* @__PURE__ */ React5.createElement("div", { className: "mb-8" }, /* @__PURE__ */ React5.createElement("div", { className: "mb-3 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React5.createElement("h2", { className: "m-0 text-base font-semibold" }, "Deliveries"), /* @__PURE__ */ React5.createElement("div", { className: actionRowClass }, /* @__PURE__ */ React5.createElement("select", { className: selectClass, value: deliveryStatus, onChange: (event) => setDeliveryStatus(event.target.value) }, /* @__PURE__ */ React5.createElement("option", { value: "" }, "All statuses"), deliveryStatusOptions.map((status) => /* @__PURE__ */ React5.createElement("option", { key: status, value: status }, status))), /* @__PURE__ */ React5.createElement("select", { className: selectClass, value: deliveryChannel, onChange: (event) => setDeliveryChannel(event.target.value) }, /* @__PURE__ */ React5.createElement("option", { value: "" }, "All channels"), deliveryChannelOptions.map((channel) => /* @__PURE__ */ React5.createElement("option", { key: channel, value: channel }, channel))), /* @__PURE__ */ React5.createElement("select", { className: selectClass, value: deliveryAdapter, onChange: (event) => setDeliveryAdapter(event.target.value) }, /* @__PURE__ */ React5.createElement("option", { value: "" }, "All adapters"), deliveryAdapterOptions.map((adapter) => /* @__PURE__ */ React5.createElement("option", { key: adapter, value: adapter }, adapter))))), /* @__PURE__ */ React5.createElement("div", { className: "grid gap-4 xl:grid-cols-[minmax(0,1fr)_420px]" }, /* @__PURE__ */ React5.createElement("div", { className: tableWrapClass }, /* @__PURE__ */ React5.createElement("table", { className: "w-full min-w-[920px] border-collapse" }, /* @__PURE__ */ React5.createElement("thead", null, /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Delivery"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Status"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Channel"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Adapter"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Attempts"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Updated"))), /* @__PURE__ */ React5.createElement("tbody", null, deliveries.map((row) => /* @__PURE__ */ React5.createElement("tr", { key: row.id, className: cx("cursor-pointer transition-colors hover:bg-zinc-50/60", selectedDelivery?.id === row.id && "bg-zinc-50"), onClick: () => void selectDelivery(row).catch((error) => toast(error.message, "error")) }, /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-xs" }, compactId(row.id)), /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-[11px] text-zinc-500" }, compactId(row.notificationId))), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement(StatusPill2, { status: asText(row.status, "unknown") })), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, asText(row.channel)), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, asText(row.adapter)), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "tabular-nums text-zinc-600") }, asText(row.attempt, "0"), "/", asText(row.maxAttempts, "-")), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "whitespace-nowrap text-zinc-500") }, formatDateTime(row.updatedAt ?? row.createdAt)))), !deliveries.length ? /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "py-10 text-center text-zinc-500"), colSpan: 6 }, loading ? "Loading deliveries..." : "No notification deliveries match the current filters.")) : null))), /* @__PURE__ */ React5.createElement("aside", { className: cx(cardClass, "min-w-0 overflow-hidden") }, detailDelivery ? /* @__PURE__ */ React5.createElement("div", { className: "grid gap-4 p-4" }, /* @__PURE__ */ React5.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React5.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React5.createElement("h3", { className: "m-0 truncate text-base font-semibold" }, asText(detailDelivery.channel), " delivery"), /* @__PURE__ */ React5.createElement("div", { className: "mt-1 font-mono text-[11px] text-zinc-500" }, asText(detailDelivery.id))), /* @__PURE__ */ React5.createElement(StatusPill2, { status: asText(detailDelivery.status, "unknown") })), /* @__PURE__ */ React5.createElement("div", { className: "grid grid-cols-2 gap-3" }, /* @__PURE__ */ React5.createElement(FieldValue, { label: "Adapter", value: asText(detailDelivery.adapter) }), /* @__PURE__ */ React5.createElement(FieldValue, { label: "Recipient", value: compactId(detailDelivery.recipientSubjectId) }), /* @__PURE__ */ React5.createElement(FieldValue, { label: "Created", value: formatDateTime(detailDelivery.createdAt) }), /* @__PURE__ */ React5.createElement(FieldValue, { label: "Last attempt", value: formatDateTime(detailDelivery.lastAttemptAt) })), /* @__PURE__ */ React5.createElement("button", { className: primaryButton, disabled: !retryable || retrying, onClick: () => void retryDelivery().catch((error) => toast(error.message, "error")) }, icons.refresh, "Retry"), /* @__PURE__ */ React5.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React5.createElement("div", { className: "text-xs font-semibold uppercase tracking-wider text-zinc-500" }, "Delivery evidence"), /* @__PURE__ */ React5.createElement(JsonPreview, { value: detailDelivery })), /* @__PURE__ */ React5.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React5.createElement("div", { className: "text-xs font-semibold uppercase tracking-wider text-zinc-500" }, "Attempts"), /* @__PURE__ */ React5.createElement("div", { className: "max-h-72 overflow-auto rounded-lg border border-zinc-200" }, attempts.map((attempt) => /* @__PURE__ */ React5.createElement("div", { key: asText(attempt.id), className: "grid gap-2 border-b border-zinc-100 px-3 py-3 text-sm last:border-b-0" }, /* @__PURE__ */ React5.createElement("div", { className: "flex items-center justify-between gap-2" }, /* @__PURE__ */ React5.createElement(StatusPill2, { status: asText(attempt.status, "unknown") }), /* @__PURE__ */ React5.createElement("span", { className: "font-mono text-[11px] text-zinc-400" }, "#", asText(attempt.attempt))), /* @__PURE__ */ React5.createElement(JsonPreview, { value: attempt }))), !attempts.length ? /* @__PURE__ */ React5.createElement("div", { className: "px-3 py-6 text-center text-sm text-zinc-500" }, "No attempts recorded.") : null))) : /* @__PURE__ */ React5.createElement("div", { className: "p-6 text-sm text-zinc-500" }, "Select a delivery.")))), /* @__PURE__ */ React5.createElement("div", null, /* @__PURE__ */ React5.createElement("div", { className: "mb-3 flex flex-wrap items-end justify-between gap-3" }, /* @__PURE__ */ React5.createElement("h2", { className: "m-0 text-base font-semibold" }, "Inbound Events"), /* @__PURE__ */ React5.createElement("div", { className: actionRowClass }, /* @__PURE__ */ React5.createElement("select", { className: selectClass, value: inboundStatus, onChange: (event) => setInboundStatus(event.target.value) }, /* @__PURE__ */ React5.createElement("option", { value: "" }, "All statuses"), inboundStatusOptions.map((status) => /* @__PURE__ */ React5.createElement("option", { key: status, value: status }, status))), /* @__PURE__ */ React5.createElement("select", { className: selectClass, value: inboundAdapter, onChange: (event) => setInboundAdapter(event.target.value) }, /* @__PURE__ */ React5.createElement("option", { value: "" }, "All adapters"), inboundAdapterOptions.map((adapter) => /* @__PURE__ */ React5.createElement("option", { key: adapter, value: adapter }, adapter))))), /* @__PURE__ */ React5.createElement("div", { className: "grid gap-4 xl:grid-cols-[minmax(0,1fr)_420px]" }, /* @__PURE__ */ React5.createElement("div", { className: tableWrapClass }, /* @__PURE__ */ React5.createElement("table", { className: "w-full min-w-[820px] border-collapse" }, /* @__PURE__ */ React5.createElement("thead", null, /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Event"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Status"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Adapter"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Verified"), /* @__PURE__ */ React5.createElement("th", { className: thClass }, "Received"))), /* @__PURE__ */ React5.createElement("tbody", null, inbound.map((row) => /* @__PURE__ */ React5.createElement("tr", { key: row.id, className: cx("cursor-pointer transition-colors hover:bg-zinc-50/60", selectedInbound?.id === row.id && "bg-zinc-50"), onClick: () => void selectInbound(row).catch((error) => toast(error.message, "error")) }, /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement("div", { className: "font-mono text-xs" }, compactId(row.id)), /* @__PURE__ */ React5.createElement("div", { className: "truncate font-mono text-[11px] text-zinc-500" }, asText(row.providerEventId))), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, /* @__PURE__ */ React5.createElement(StatusPill2, { status: asText(row.status, "unknown") })), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, asText(row.adapter)), /* @__PURE__ */ React5.createElement("td", { className: tdClass }, row.verified ? "Yes" : "No"), /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "whitespace-nowrap text-zinc-500") }, formatDateTime(row.receivedAt)))), !inbound.length ? /* @__PURE__ */ React5.createElement("tr", null, /* @__PURE__ */ React5.createElement("td", { className: cx(tdClass, "py-10 text-center text-zinc-500"), colSpan: 5 }, loading ? "Loading inbound events..." : "No inbound events match the current filters.")) : null))), /* @__PURE__ */ React5.createElement("aside", { className: cx(cardClass, "min-w-0 overflow-hidden") }, inboundDetail ? /* @__PURE__ */ React5.createElement("div", { className: "grid gap-4 p-4" }, /* @__PURE__ */ React5.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React5.createElement("div", { className: "min-w-0" }, /* @__PURE__ */ React5.createElement("h3", { className: "m-0 truncate text-base font-semibold" }, "Inbound event"), /* @__PURE__ */ React5.createElement("div", { className: "mt-1 font-mono text-[11px] text-zinc-500" }, asText(inboundDetail.id))), /* @__PURE__ */ React5.createElement(StatusPill2, { status: asText(inboundDetail.status, "unknown") })), /* @__PURE__ */ React5.createElement("div", { className: "grid grid-cols-2 gap-3" }, /* @__PURE__ */ React5.createElement(FieldValue, { label: "Adapter", value: asText(inboundDetail.adapter) }), /* @__PURE__ */ React5.createElement(FieldValue, { label: "Path", value: asText(inboundDetail.path) }), /* @__PURE__ */ React5.createElement(FieldValue, { label: "Received", value: formatDateTime(inboundDetail.receivedAt) }), /* @__PURE__ */ React5.createElement(FieldValue, { label: "Processed", value: formatDateTime(inboundDetail.processedAt) })), /* @__PURE__ */ React5.createElement("div", { className: "grid gap-2" }, /* @__PURE__ */ React5.createElement("div", { className: "text-xs font-semibold uppercase tracking-wider text-zinc-500" }, "Event evidence"), /* @__PURE__ */ React5.createElement(JsonPreview, { value: inboundDetail }))) : /* @__PURE__ */ React5.createElement("div", { className: "p-6 text-sm text-zinc-500" }, "Select an inbound event.")))));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// admin-src/PitrCard.tsx
|
|
1386
|
+
import React6, { useEffect as useEffect5, useState as useState5 } from "react";
|
|
1387
|
+
function localInputValue(date) {
|
|
1388
|
+
const offsetMs = date.getTimezoneOffset() * 60 * 1e3;
|
|
1389
|
+
return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16);
|
|
1390
|
+
}
|
|
1391
|
+
function localInputEpoch(value) {
|
|
1392
|
+
const epoch = new Date(value).getTime();
|
|
1393
|
+
return Number.isFinite(epoch) ? epoch : null;
|
|
1394
|
+
}
|
|
1395
|
+
function sizeLabel2(bytes) {
|
|
1396
|
+
if (bytes === null || bytes === void 0) return "Unknown";
|
|
1397
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1398
|
+
const units = ["KB", "MB", "GB"];
|
|
1399
|
+
let value = bytes / 1024;
|
|
1400
|
+
let unit = units[0];
|
|
1401
|
+
for (let index = 1; index < units.length && value >= 1024; index += 1) {
|
|
1402
|
+
value /= 1024;
|
|
1403
|
+
unit = units[index];
|
|
1404
|
+
}
|
|
1405
|
+
return `${new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(value)} ${unit}`;
|
|
1406
|
+
}
|
|
1407
|
+
function PitrCard({
|
|
1408
|
+
notify,
|
|
1409
|
+
organizationId,
|
|
1410
|
+
appDataOnly = false
|
|
1411
|
+
}) {
|
|
1412
|
+
const [status, setStatus] = useState5(null);
|
|
1413
|
+
const [restoreTo, setRestoreTo] = useState5(() => localInputValue(new Date(Date.now() - 30 * 60 * 1e3)));
|
|
1414
|
+
const [preview, setPreview] = useState5(null);
|
|
1415
|
+
const load = async () => {
|
|
1416
|
+
const params = organizationId ? `?organizationId=${encodeURIComponent(organizationId)}` : "";
|
|
1417
|
+
const next = await api(`/api/admin/pitr${params}`);
|
|
1418
|
+
setStatus(next);
|
|
1419
|
+
if (!restoreTo && next.now) setRestoreTo(localInputValue(new Date(new Date(next.now).getTime() - 30 * 60 * 1e3)));
|
|
1420
|
+
};
|
|
1421
|
+
useEffect5(() => {
|
|
1422
|
+
void load().catch((error) => notify(error.message, "error"));
|
|
1423
|
+
}, [organizationId]);
|
|
1424
|
+
const disabledReason = boot.local ? "Point-in-time restore is only available in production \u2014 local development has no durable change log." : status && !status.supported ? status.reason : "";
|
|
1425
|
+
const minRestoreEpoch = status ? new Date(status.earliestRestoreTime).getTime() : null;
|
|
1426
|
+
const maxRestoreEpoch = status ? new Date(status.now).getTime() : null;
|
|
1427
|
+
const restoreEpoch = localInputEpoch(restoreTo);
|
|
1428
|
+
const restoreOutOfRange = restoreEpoch === null || typeof minRestoreEpoch === "number" && restoreEpoch < minRestoreEpoch || typeof maxRestoreEpoch === "number" && restoreEpoch > maxRestoreEpoch;
|
|
1429
|
+
const canPreview = Boolean(status && !disabledReason && restoreTo && !restoreOutOfRange);
|
|
1430
|
+
const previewRestore = async () => {
|
|
1431
|
+
const body = {
|
|
1432
|
+
timestamp: new Date(restoreTo).toISOString(),
|
|
1433
|
+
...organizationId || status?.organizationId ? { organizationId: organizationId || status?.organizationId } : {}
|
|
1434
|
+
};
|
|
1435
|
+
const result = await api("/api/admin/pitr/preview", { method: "POST", body: JSON.stringify(body) });
|
|
1436
|
+
setPreview(result);
|
|
1437
|
+
};
|
|
1438
|
+
const undo = () => {
|
|
1439
|
+
if (!status?.lastRestore?.undoBookmark) return;
|
|
1440
|
+
setPreview({
|
|
1441
|
+
bookmark: status.lastRestore.undoBookmark,
|
|
1442
|
+
requestedTime: status.lastRestore.restoredAt,
|
|
1443
|
+
organizationId: status.organizationId,
|
|
1444
|
+
undoOf: status.lastRestore.restoredAt
|
|
1445
|
+
});
|
|
1446
|
+
};
|
|
1447
|
+
const presets = [
|
|
1448
|
+
["5m", 5 * 60 * 1e3],
|
|
1449
|
+
["30m", 30 * 60 * 1e3],
|
|
1450
|
+
["1h", 60 * 60 * 1e3],
|
|
1451
|
+
["6h", 6 * 60 * 60 * 1e3],
|
|
1452
|
+
["1d", 24 * 60 * 60 * 1e3]
|
|
1453
|
+
];
|
|
1454
|
+
return /* @__PURE__ */ React6.createElement("div", { className: cx(cardClass, "max-w-2xl overflow-hidden") }, /* @__PURE__ */ React6.createElement("div", { className: "border-b border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React6.createElement("h2", { className: "m-0 flex items-center gap-2 text-sm font-semibold" }, /* @__PURE__ */ React6.createElement("span", { className: "text-zinc-500" }, icons.history), "Point-in-time restore"), /* @__PURE__ */ React6.createElement("p", { className: "m-0 mt-1 text-sm text-zinc-500" }, appDataOnly ? "Restore application data to how it was at an earlier moment. Users, sessions, and the audit trail are not affected." : "Restore the whole database to an earlier moment, including users, sessions, roles, audit, and app data.")), status ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, disabledReason ? /* @__PURE__ */ React6.createElement("div", { className: "px-5 py-4 text-sm text-zinc-500" }, disabledReason) : /* @__PURE__ */ React6.createElement("dl", { className: "m-0 divide-y divide-zinc-100" }, /* @__PURE__ */ React6.createElement("div", { className: "grid grid-cols-[160px_minmax(0,1fr)] items-center gap-4 px-5 py-3.5 text-sm" }, /* @__PURE__ */ React6.createElement("dt", { className: "font-medium text-zinc-500" }, appDataOnly ? "App data" : "Database"), /* @__PURE__ */ React6.createElement("dd", { className: "m-0 text-zinc-900" }, "Durable Object (SQLite) \xB7 ", sizeLabel2(status.databaseSizeBytes))), /* @__PURE__ */ React6.createElement("div", { className: "grid grid-cols-[160px_minmax(0,1fr)] items-center gap-4 px-5 py-3.5 text-sm" }, /* @__PURE__ */ React6.createElement("dt", { className: "font-medium text-zinc-500" }, "Restorable"), /* @__PURE__ */ React6.createElement("dd", { className: "m-0 text-zinc-900" }, "Any moment in the last 30 days")), /* @__PURE__ */ React6.createElement("div", { className: "grid grid-cols-[160px_minmax(0,1fr)] items-center gap-4 px-5 py-3.5 text-sm" }, /* @__PURE__ */ React6.createElement("dt", { className: "font-medium text-zinc-500" }, "Last restore"), /* @__PURE__ */ React6.createElement("dd", { className: "m-0 flex flex-wrap items-center gap-2 text-zinc-900" }, status.lastRestore ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, formatDateTime(status.lastRestore.restoredTo || status.lastRestore.restoredAt), /* @__PURE__ */ React6.createElement("span", { className: "text-zinc-400" }, "by ", status.lastRestore.actorId), /* @__PURE__ */ React6.createElement("button", { className: secondaryButton, onClick: undo, title: "Return to the state captured immediately before this restore." }, "Undo to pre-restore state")) : "None"))), /* @__PURE__ */ React6.createElement("div", { className: "border-t border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React6.createElement("label", { className: labelClass }, "Restore to", /* @__PURE__ */ React6.createElement(
|
|
1455
|
+
"input",
|
|
1456
|
+
{
|
|
1457
|
+
type: "datetime-local",
|
|
1458
|
+
className: cx(fieldBase, "w-full max-w-xs"),
|
|
1459
|
+
value: restoreTo,
|
|
1460
|
+
min: localInputValue(new Date(status.earliestRestoreTime)),
|
|
1461
|
+
max: localInputValue(new Date(status.now)),
|
|
1462
|
+
disabled: Boolean(disabledReason),
|
|
1463
|
+
onChange: (event) => setRestoreTo(event.target.value)
|
|
1464
|
+
}
|
|
1465
|
+
)), /* @__PURE__ */ React6.createElement("div", { className: "mt-2 flex flex-wrap items-center gap-1.5" }, presets.map(([label, offset]) => /* @__PURE__ */ React6.createElement(
|
|
1466
|
+
"button",
|
|
1467
|
+
{
|
|
1468
|
+
key: label,
|
|
1469
|
+
className: "h-7 rounded-md border border-zinc-200 bg-white px-2 text-xs font-medium text-zinc-600 transition-colors hover:border-zinc-300 hover:bg-zinc-50 disabled:pointer-events-none disabled:opacity-40",
|
|
1470
|
+
disabled: Boolean(disabledReason),
|
|
1471
|
+
onClick: () => setRestoreTo(localInputValue(new Date(new Date(status.now).getTime() - offset)))
|
|
1472
|
+
},
|
|
1473
|
+
label
|
|
1474
|
+
))), /* @__PURE__ */ React6.createElement("div", { className: "mt-2 text-xs text-zinc-400" }, "Local time \xB7 resolves to the nearest stored snapshot"), /* @__PURE__ */ React6.createElement("div", { className: "mt-4 flex flex-wrap items-center justify-between gap-3" }, /* @__PURE__ */ React6.createElement("div", { className: "text-xs text-zinc-500" }, "Review only. Data changes after the confirmation step."), /* @__PURE__ */ React6.createElement("button", { className: secondaryButton, disabled: !canPreview, onClick: () => void previewRestore().catch((error) => notify(error.message, "error")) }, icons.calendar, "Review restore")))) : /* @__PURE__ */ React6.createElement("div", { className: "space-y-3 px-5 py-4" }, /* @__PURE__ */ React6.createElement("div", { className: "h-4 w-2/3 animate-pulse rounded bg-zinc-100" }), /* @__PURE__ */ React6.createElement("div", { className: "h-4 w-1/2 animate-pulse rounded bg-zinc-100" })), preview ? /* @__PURE__ */ React6.createElement(
|
|
1475
|
+
RestoreConfirmDialog,
|
|
1476
|
+
{
|
|
1477
|
+
preview,
|
|
1478
|
+
appDataOnly,
|
|
1479
|
+
onCancel: () => setPreview(null),
|
|
1480
|
+
onRestored: async () => {
|
|
1481
|
+
setPreview(null);
|
|
1482
|
+
notify(
|
|
1483
|
+
preview.undoOf ? "Undo complete \u2014 data is back to the state from before the restore." : "Restore complete \u2014 use Undo to pre-restore state here if you need to roll it back."
|
|
1484
|
+
);
|
|
1485
|
+
await load();
|
|
1486
|
+
},
|
|
1487
|
+
notify
|
|
1488
|
+
}
|
|
1489
|
+
) : null);
|
|
1490
|
+
}
|
|
1491
|
+
function RestoreConfirmDialog({
|
|
1492
|
+
preview,
|
|
1493
|
+
appDataOnly,
|
|
1494
|
+
onCancel,
|
|
1495
|
+
onRestored,
|
|
1496
|
+
notify
|
|
1497
|
+
}) {
|
|
1498
|
+
const [confirmation, setConfirmation] = useState5("");
|
|
1499
|
+
const [saving, setSaving] = useState5(false);
|
|
1500
|
+
useEffect5(() => {
|
|
1501
|
+
const onKey = (event) => {
|
|
1502
|
+
if (event.key === "Escape" && !saving) onCancel();
|
|
1503
|
+
};
|
|
1504
|
+
document.addEventListener("keydown", onKey);
|
|
1505
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1506
|
+
}, [onCancel, saving]);
|
|
1507
|
+
const restore = async () => {
|
|
1508
|
+
setSaving(true);
|
|
1509
|
+
try {
|
|
1510
|
+
await api("/api/admin/pitr/restore", {
|
|
1511
|
+
method: "POST",
|
|
1512
|
+
body: JSON.stringify({
|
|
1513
|
+
bookmark: preview.bookmark,
|
|
1514
|
+
restoredTo: preview.requestedTime,
|
|
1515
|
+
organizationId: preview.organizationId,
|
|
1516
|
+
undoOf: preview.undoOf
|
|
1517
|
+
})
|
|
1518
|
+
});
|
|
1519
|
+
await onRestored();
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
notify(error instanceof Error ? error.message : "Restore failed", "error");
|
|
1522
|
+
} finally {
|
|
1523
|
+
setSaving(false);
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
const canSubmit = confirmation.trim().toLowerCase() === "restore" && !saving;
|
|
1527
|
+
return /* @__PURE__ */ React6.createElement("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", role: "dialog", "aria-modal": "true", "aria-labelledby": "restore-confirm-title" }, /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 bg-zinc-950/30", onClick: saving ? void 0 : onCancel }), /* @__PURE__ */ React6.createElement("div", { className: "relative w-full max-w-md overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-[0_24px_48px_rgb(9_9_11/0.18)]" }, /* @__PURE__ */ React6.createElement("div", { className: "border-b border-zinc-100 px-5 py-4" }, /* @__PURE__ */ React6.createElement("h2", { id: "restore-confirm-title", className: "m-0 text-[15px] font-semibold text-zinc-900" }, preview.undoOf ? `Undo the restore from ${formatDateTime(preview.undoOf)}?` : `Restore data to ${formatDateTime(preview.requestedTime)}?`)), /* @__PURE__ */ React6.createElement("div", { className: "space-y-4 px-5 py-4 text-sm leading-relaxed text-zinc-600" }, /* @__PURE__ */ React6.createElement("p", { className: "m-0 text-zinc-700" }, "No data has changed yet. This action runs only after you type ", /* @__PURE__ */ React6.createElement("span", { className: "font-semibold text-zinc-900" }, "restore"), " and confirm."), /* @__PURE__ */ React6.createElement("ul", { className: "m-0 list-disc space-y-1 pl-5" }, /* @__PURE__ */ React6.createElement("li", null, preview.undoOf ? appDataOnly ? "Application tables return to the state captured immediately before that restore." : "The whole database returns to the state captured immediately before that restore." : appDataOnly ? "Application tables revert to exactly how they were at this moment." : "The whole database reverts to exactly how it was at this moment."), /* @__PURE__ */ React6.createElement("li", null, preview.undoOf ? "Changes made after that pre-restore state are discarded." : "Changes made after this moment are discarded."), appDataOnly ? /* @__PURE__ */ React6.createElement("li", null, "Users, sessions, roles, and the audit trail are not affected.") : /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement("li", null, "Users, sessions, roles, audit, and app rows all move together."), /* @__PURE__ */ React6.createElement("li", null, "Sessions created after the restore point are removed, so affected users must sign in again.")), /* @__PURE__ */ React6.createElement("li", null, "An undo point is saved automatically. After this completes, the Last restore row shows the rollback action."), /* @__PURE__ */ React6.createElement("li", null, "This action is recorded in the audit trail.")), /* @__PURE__ */ React6.createElement("div", { className: "truncate font-mono text-[11px] text-zinc-400", title: preview.bookmark }, preview.bookmark), /* @__PURE__ */ React6.createElement("label", { className: labelClass }, "Type restore to confirm", /* @__PURE__ */ React6.createElement("input", { className: fieldClass, value: confirmation, disabled: saving, onChange: (event) => setConfirmation(event.target.value) }))), /* @__PURE__ */ React6.createElement("div", { className: "flex items-center justify-end gap-2 border-t border-zinc-100 bg-zinc-50/60 px-5 py-4" }, /* @__PURE__ */ React6.createElement("button", { className: secondaryButton, disabled: saving, onClick: onCancel }, "Cancel"), /* @__PURE__ */ React6.createElement("button", { className: destructiveButton, disabled: !canSubmit, onClick: () => void restore() }, saving ? "Restoring\u2026" : appDataOnly ? "Restore data" : "Restore database"))));
|
|
1528
|
+
}
|
|
1529
|
+
export {
|
|
1530
|
+
BackupPanel,
|
|
1531
|
+
DataView,
|
|
1532
|
+
KindGlyph,
|
|
1533
|
+
NotificationsView,
|
|
1534
|
+
PitrCard,
|
|
1535
|
+
TypeGlyph,
|
|
1536
|
+
UnsavedChangesDialog,
|
|
1537
|
+
actionRowClass,
|
|
1538
|
+
buttonBase,
|
|
1539
|
+
cardClass,
|
|
1540
|
+
chipShadow,
|
|
1541
|
+
cx,
|
|
1542
|
+
dangerButton,
|
|
1543
|
+
destructiveButton,
|
|
1544
|
+
fieldBase,
|
|
1545
|
+
fieldClass,
|
|
1546
|
+
iconButton,
|
|
1547
|
+
icons,
|
|
1548
|
+
labelClass,
|
|
1549
|
+
primaryButton,
|
|
1550
|
+
secondaryButton,
|
|
1551
|
+
selectBase,
|
|
1552
|
+
selectClass,
|
|
1553
|
+
tableWrapClass,
|
|
1554
|
+
tdClass,
|
|
1555
|
+
thClass,
|
|
1556
|
+
toolbarClass
|
|
1557
|
+
};
|