insforge 0.3.1
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/.dockerignore +58 -0
- package/.env.example +49 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +79 -0
- package/.github/copilot-instructions.md +147 -0
- package/.github/workflows/build-image.yml +65 -0
- package/.github/workflows/ci-premerge-check.yml +24 -0
- package/.github/workflows/deploy-aws.yml +130 -0
- package/.github/workflows/lint-and-format.yml +33 -0
- package/.prettierignore +65 -0
- package/.prettierrc +9 -0
- package/CHANGELOG.md +3 -0
- package/CONTRIBUTING.md +126 -0
- package/Dockerfile +27 -0
- package/GITHUB_OAUTH_SETUP.md +49 -0
- package/GOOGLE_OAUTH_SETUP.md +148 -0
- package/LICENSE +201 -0
- package/README.md +134 -0
- package/assets/Dark.svg +23 -0
- package/assets/archDiagram.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/mcpInstallv2.png +0 -0
- package/assets/sampleResponse.png +0 -0
- package/assets/signin.png +0 -0
- package/assets/userflow.png +0 -0
- package/backend/migrations/000_create-base-tables.sql +142 -0
- package/backend/migrations/001_create-helper-functions.sql +41 -0
- package/backend/migrations/002_rename-auth-tables.sql +30 -0
- package/backend/migrations/003_create-users-table.sql +56 -0
- package/backend/migrations/004_add-reload-postgrest-func.sql +24 -0
- package/backend/migrations/005_enable-project-admin-modify-users.sql +30 -0
- package/backend/migrations/006_modify-ai-usage-table.sql +25 -0
- package/backend/migrations/007_drop-metadata-table.sql +2 -0
- package/backend/migrations/008_add-system-tables.sql +77 -0
- package/backend/migrations/009_add-function-secrets.sql +24 -0
- package/backend/migrations/010_modify-ai-config-modalities.sql +93 -0
- package/backend/migrations/011_refactor-secrets-table.sql +15 -0
- package/backend/migrations/012_add-storage-uploaded-by.sql +8 -0
- package/backend/package.json +75 -0
- package/backend/src/api/middleware/auth.ts +240 -0
- package/backend/src/api/middleware/error.ts +231 -0
- package/backend/src/api/middleware/upload.ts +59 -0
- package/backend/src/api/routes/agent.ts +29 -0
- package/backend/src/api/routes/ai.ts +472 -0
- package/backend/src/api/routes/auth.oauth.ts +482 -0
- package/backend/src/api/routes/auth.ts +386 -0
- package/backend/src/api/routes/database.advance.ts +275 -0
- package/backend/src/api/routes/database.records.ts +246 -0
- package/backend/src/api/routes/database.tables.ts +161 -0
- package/backend/src/api/routes/docs.ts +66 -0
- package/backend/src/api/routes/functions.ts +183 -0
- package/backend/src/api/routes/logs.ts +150 -0
- package/backend/src/api/routes/metadata.ts +160 -0
- package/backend/src/api/routes/openapi.ts +82 -0
- package/backend/src/api/routes/secrets.ts +199 -0
- package/backend/src/api/routes/storage.ts +547 -0
- package/backend/src/api/routes/usage.ts +96 -0
- package/backend/src/core/ai/chat.ts +207 -0
- package/backend/src/core/ai/client.ts +242 -0
- package/backend/src/core/ai/config.ts +187 -0
- package/backend/src/core/ai/image.ts +156 -0
- package/backend/src/core/ai/model.ts +117 -0
- package/backend/src/core/ai/usage.ts +290 -0
- package/backend/src/core/auth/auth.ts +781 -0
- package/backend/src/core/auth/oauth.ts +398 -0
- package/backend/src/core/database/advance.ts +1074 -0
- package/backend/src/core/database/manager.ts +178 -0
- package/backend/src/core/database/table.ts +772 -0
- package/backend/src/core/documentation/agent.ts +689 -0
- package/backend/src/core/documentation/openapi.ts +856 -0
- package/backend/src/core/functions/functions.ts +310 -0
- package/backend/src/core/logs/analytics.ts +76 -0
- package/backend/src/core/logs/audit.ts +255 -0
- package/backend/src/core/logs/providers/base.provider.ts +83 -0
- package/backend/src/core/logs/providers/cloudwatch.provider.ts +510 -0
- package/backend/src/core/logs/providers/localdb.provider.ts +246 -0
- package/backend/src/core/secrets/encryption.ts +58 -0
- package/backend/src/core/secrets/secrets.ts +410 -0
- package/backend/src/core/socket/socket.ts +388 -0
- package/backend/src/core/socket/types.ts +79 -0
- package/backend/src/core/storage/storage.ts +923 -0
- package/backend/src/server.ts +288 -0
- package/backend/src/types/ai.ts +46 -0
- package/backend/src/types/auth.ts +90 -0
- package/backend/src/types/database.ts +136 -0
- package/backend/src/types/error-constants.ts +86 -0
- package/backend/src/types/logs.ts +47 -0
- package/backend/src/types/profile.ts +55 -0
- package/backend/src/types/storage.ts +23 -0
- package/backend/src/utils/cloud-token.ts +39 -0
- package/backend/src/utils/constants.ts +1 -0
- package/backend/src/utils/environment.ts +35 -0
- package/backend/src/utils/helpers.ts +49 -0
- package/backend/src/utils/logger.ts +13 -0
- package/backend/src/utils/response.ts +62 -0
- package/backend/src/utils/seed.ts +205 -0
- package/backend/src/utils/sql-parser.ts +63 -0
- package/backend/src/utils/uuid.ts +9 -0
- package/backend/src/utils/validations.ts +129 -0
- package/backend/tests/README.md +134 -0
- package/backend/tests/cleanup-all-test-data.sh +231 -0
- package/backend/tests/cloud/test-s3-multitenant.sh +132 -0
- package/backend/tests/local/comprehensive-curl-tests.sh +156 -0
- package/backend/tests/local/test-auth-router.sh +144 -0
- package/backend/tests/local/test-database-router.sh +222 -0
- package/backend/tests/local/test-e2e.sh +241 -0
- package/backend/tests/local/test-fk-errors.sh +97 -0
- package/backend/tests/local/test-id-field.sh +201 -0
- package/backend/tests/local/test-public-bucket.sh +265 -0
- package/backend/tests/local/test-secrets.sh +248 -0
- package/backend/tests/local/test-serverless-functions.sh.disabled +325 -0
- package/backend/tests/local/test-traditional-rest.sh +209 -0
- package/backend/tests/manual/README.md +51 -0
- package/backend/tests/manual/create-large-table-simple.sql +11 -0
- package/backend/tests/manual/seed-large-table.sql +101 -0
- package/backend/tests/manual/setup-large-table-extras.sql +34 -0
- package/backend/tests/manual/test-better-auth.sh +303 -0
- package/backend/tests/manual/test-bulk-upsert.sh +410 -0
- package/backend/tests/manual/test-database-advance.sh +297 -0
- package/backend/tests/manual/test-postgrest-stability.sh +192 -0
- package/backend/tests/manual/test-rawsql-export-import.sh +412 -0
- package/backend/tests/manual/test-universal-storage.sh +264 -0
- package/backend/tests/manual/test-users.sql +18 -0
- package/backend/tests/run-all-tests.sh +140 -0
- package/backend/tests/setup.ts +22 -0
- package/backend/tests/test-config.sh +303 -0
- package/backend/tsconfig.json +23 -0
- package/backend/tsup.config.ts +18 -0
- package/backend/vitest.config.ts +22 -0
- package/docker-compose.prod.yml +145 -0
- package/docker-compose.yml +167 -0
- package/docker-init/db/db-init.sql +125 -0
- package/docker-init/db/jwt.sql +5 -0
- package/docker-init/db/logs.sql +9 -0
- package/docker-init/db/postgresql.conf +17 -0
- package/docs/deprecated/insforge-auth-api.md +215 -0
- package/docs/deprecated/insforge-auth-sdk.md +100 -0
- package/docs/deprecated/insforge-db-api.md +359 -0
- package/docs/deprecated/insforge-db-sdk.md +140 -0
- package/docs/deprecated/insforge-debug-sdk.md +157 -0
- package/docs/deprecated/insforge-debug.md +65 -0
- package/docs/deprecated/insforge-instructions.md +124 -0
- package/docs/deprecated/insforge-project.md +118 -0
- package/docs/deprecated/insforge-storage-api.md +279 -0
- package/docs/deprecated/insforge-storage-sdk.md +159 -0
- package/docs/insforge-instructions-sdk.md +407 -0
- package/eslint.config.js +317 -0
- package/examples/oauth/frontend-oauth-example.html +251 -0
- package/examples/response-examples.md +444 -0
- package/frontend/README.md +112 -0
- package/frontend/components.json +17 -0
- package/frontend/index.html +13 -0
- package/frontend/package.json +63 -0
- package/frontend/public/favicon.ico +0 -0
- package/frontend/src/App.tsx +106 -0
- package/frontend/src/assets/icons/checkbox_checked.svg +6 -0
- package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -0
- package/frontend/src/assets/icons/checked.svg +3 -0
- package/frontend/src/assets/icons/error.svg +3 -0
- package/frontend/src/assets/icons/pencil.svg +4 -0
- package/frontend/src/assets/icons/refresh.svg +4 -0
- package/frontend/src/assets/icons/step_active.svg +3 -0
- package/frontend/src/assets/icons/step_inactive.svg +11 -0
- package/frontend/src/assets/icons/warning.svg +3 -0
- package/frontend/src/assets/logos/amazon.svg +1 -0
- package/frontend/src/assets/logos/claude_code.svg +3 -0
- package/frontend/src/assets/logos/cline.svg +6 -0
- package/frontend/src/assets/logos/cursor.svg +20 -0
- package/frontend/src/assets/logos/discord.svg +9 -0
- package/frontend/src/assets/logos/gemini.svg +19 -0
- package/frontend/src/assets/logos/github.svg +5 -0
- package/frontend/src/assets/logos/google.svg +13 -0
- package/frontend/src/assets/logos/grok.svg +10 -0
- package/frontend/src/assets/logos/insforge_dark.svg +15 -0
- package/frontend/src/assets/logos/insforge_light.svg +15 -0
- package/frontend/src/assets/logos/openai.svg +10 -0
- package/frontend/src/assets/logos/roo_code.svg +9 -0
- package/frontend/src/assets/logos/trae.svg +3 -0
- package/frontend/src/assets/logos/windsurf.svg +10 -0
- package/frontend/src/components/ButtonWithLoading.tsx +27 -0
- package/frontend/src/components/Checkbox.tsx +61 -0
- package/frontend/src/components/CodeBlock.tsx +32 -0
- package/frontend/src/components/ConfirmDialog.tsx +96 -0
- package/frontend/src/components/CopyButton.tsx +69 -0
- package/frontend/src/components/DeleteActionButton.tsx +42 -0
- package/frontend/src/components/EmptyState.tsx +41 -0
- package/frontend/src/components/ErrorState.tsx +35 -0
- package/frontend/src/components/FeatureSidebar.tsx +126 -0
- package/frontend/src/components/FeatureSidebarItem.tsx +101 -0
- package/frontend/src/components/JsonHighlight.tsx +61 -0
- package/frontend/src/components/LoadingState.tsx +16 -0
- package/frontend/src/components/PaginationControls.tsx +54 -0
- package/frontend/src/components/PromptDialog.tsx +68 -0
- package/frontend/src/components/SearchInput.tsx +90 -0
- package/frontend/src/components/SelectionClearButton.tsx +26 -0
- package/frontend/src/components/Stepper.tsx +139 -0
- package/frontend/src/components/ThemeToggle.tsx +58 -0
- package/frontend/src/components/TypeBadge.tsx +20 -0
- package/frontend/src/components/datagrid/DataGrid.tsx +264 -0
- package/frontend/src/components/datagrid/DefaultCellRenderer.tsx +114 -0
- package/frontend/src/components/datagrid/IdCell.tsx +44 -0
- package/frontend/src/components/datagrid/SortableHeader.tsx +74 -0
- package/frontend/src/components/datagrid/cell-editors/BooleanCellEditor.tsx +54 -0
- package/frontend/src/components/datagrid/cell-editors/DateCellEditor.tsx +483 -0
- package/frontend/src/components/datagrid/cell-editors/JsonCellEditor.tsx +362 -0
- package/frontend/src/components/datagrid/cell-editors/TextCellEditor.tsx +38 -0
- package/frontend/src/components/datagrid/cell-editors/index.ts +14 -0
- package/frontend/src/components/datagrid/cell-editors/types.ts +43 -0
- package/frontend/src/components/datagrid/datagridTypes.tsx +72 -0
- package/frontend/src/components/datagrid/index.tsx +20 -0
- package/frontend/src/components/index.ts +39 -0
- package/frontend/src/components/layout/AppHeader.tsx +146 -0
- package/frontend/src/components/layout/AppSidebar.tsx +190 -0
- package/frontend/src/components/layout/CloudLayout.tsx +95 -0
- package/frontend/src/components/layout/Layout.tsx +43 -0
- package/frontend/src/components/radix/Alert.tsx +45 -0
- package/frontend/src/components/radix/AlertDialog.tsx +115 -0
- package/frontend/src/components/radix/Avatar.tsx +45 -0
- package/frontend/src/components/radix/Badge.tsx +33 -0
- package/frontend/src/components/radix/Button.tsx +50 -0
- package/frontend/src/components/radix/Card.tsx +58 -0
- package/frontend/src/components/radix/Dialog.tsx +98 -0
- package/frontend/src/components/radix/DropdownMenu.tsx +185 -0
- package/frontend/src/components/radix/Form.tsx +167 -0
- package/frontend/src/components/radix/Input.tsx +22 -0
- package/frontend/src/components/radix/Label.tsx +19 -0
- package/frontend/src/components/radix/Popover.tsx +29 -0
- package/frontend/src/components/radix/ScrollArea.tsx +44 -0
- package/frontend/src/components/radix/Select.tsx +151 -0
- package/frontend/src/components/radix/Separator.tsx +26 -0
- package/frontend/src/components/radix/Sheet.tsx +119 -0
- package/frontend/src/components/radix/Skeleton.tsx +7 -0
- package/frontend/src/components/radix/Switch.tsx +29 -0
- package/frontend/src/components/radix/Tabs.tsx +50 -0
- package/frontend/src/components/radix/Textarea.tsx +21 -0
- package/frontend/src/components/radix/Tooltip.tsx +28 -0
- package/frontend/src/features/ai/components/AIConfigCard.tsx +154 -0
- package/frontend/src/features/ai/components/AIConfigDialog.tsx +76 -0
- package/frontend/src/features/ai/components/AIConfigForm.tsx +222 -0
- package/frontend/src/features/ai/components/AIEmptyState.tsx +18 -0
- package/frontend/src/features/ai/components/fields/ModalityField.tsx +87 -0
- package/frontend/src/features/ai/components/fields/ModelSelectionField.tsx +134 -0
- package/frontend/src/features/ai/components/fields/SystemPromptField.tsx +33 -0
- package/frontend/src/features/ai/helpers.ts +155 -0
- package/frontend/src/features/ai/hooks/useAIConfigs.ts +221 -0
- package/frontend/src/features/ai/hooks/useAIUsage.ts +77 -0
- package/frontend/src/features/ai/page/AIPage.tsx +178 -0
- package/frontend/src/features/ai/services/ai.service.ts +148 -0
- package/frontend/src/features/auth/components/AddOAuthDialog.tsx +106 -0
- package/frontend/src/features/auth/components/AuthMethodTab.tsx +238 -0
- package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +303 -0
- package/frontend/src/features/auth/components/OAuthEmptyState.tsx +15 -0
- package/frontend/src/features/auth/components/UserFormDialog.tsx +248 -0
- package/frontend/src/features/auth/components/UsersDataGrid.tsx +183 -0
- package/frontend/src/features/auth/components/UsersTab.tsx +114 -0
- package/frontend/src/features/auth/hooks/useOAuthConfig.ts +129 -0
- package/frontend/src/features/auth/hooks/useUsers.ts +57 -0
- package/frontend/src/features/auth/index.ts +9 -0
- package/frontend/src/features/auth/page/AuthenticationPage.tsx +169 -0
- package/frontend/src/features/auth/services/auth.service.ts +112 -0
- package/frontend/src/features/auth/services/oauth.service.ts +49 -0
- package/frontend/src/features/dashboard/page/DashboardPage.tsx +194 -0
- package/frontend/src/features/database/components/ColumnTypeSelect.tsx +64 -0
- package/frontend/src/features/database/components/DatabaseDataGrid.tsx +282 -0
- package/frontend/src/features/database/components/ForeignKeyCell.tsx +187 -0
- package/frontend/src/features/database/components/ForeignKeyPopover.tsx +378 -0
- package/frontend/src/features/database/components/LinkRecordModal.tsx +288 -0
- package/frontend/src/features/database/components/RecordFormDialog.tsx +164 -0
- package/frontend/src/features/database/components/RecordFormField.tsx +568 -0
- package/frontend/src/features/database/components/TableEmptyState.tsx +21 -0
- package/frontend/src/features/database/components/TableForm.tsx +656 -0
- package/frontend/src/features/database/components/TableFormColumn.tsx +137 -0
- package/frontend/src/features/database/components/TableListSkeleton.tsx +9 -0
- package/frontend/src/features/database/components/TableSidebar.tsx +47 -0
- package/frontend/src/features/database/constants.ts +26 -0
- package/frontend/src/features/database/helpers.ts +125 -0
- package/frontend/src/features/database/hooks/UseLinkModal.tsx +78 -0
- package/frontend/src/features/database/index.ts +12 -0
- package/frontend/src/features/database/page/DatabasePage.tsx +626 -0
- package/frontend/src/features/database/schema.ts +25 -0
- package/frontend/src/features/database/services/database.service.ts +216 -0
- package/frontend/src/features/functions/components/FunctionEmptyState.tsx +15 -0
- package/frontend/src/features/functions/components/FunctionRow.tsx +71 -0
- package/frontend/src/features/functions/components/FunctionViewer.tsx +46 -0
- package/frontend/src/features/functions/components/FunctionsContent.tsx +88 -0
- package/frontend/src/features/functions/components/FunctionsSidebar.tsx +56 -0
- package/frontend/src/features/functions/components/SecretEmptyState.tsx +23 -0
- package/frontend/src/features/functions/components/SecretRow.tsx +68 -0
- package/frontend/src/features/functions/components/SecretsContent.tsx +120 -0
- package/frontend/src/features/functions/hooks/useFunctions.ts +106 -0
- package/frontend/src/features/functions/page/FunctionsPage.tsx +28 -0
- package/frontend/src/features/functions/services/functions.service.ts +48 -0
- package/frontend/src/features/login/components/AuthErrorBoundary.tsx +87 -0
- package/frontend/src/features/login/components/PrivateRoute.tsx +24 -0
- package/frontend/src/features/login/page/CloudLoginPage.tsx +93 -0
- package/frontend/src/features/login/page/LoginPage.tsx +174 -0
- package/frontend/src/features/logs/components/AnalyticsLogsTable.tsx +313 -0
- package/frontend/src/features/logs/components/LogsTable.tsx +199 -0
- package/frontend/src/features/logs/hooks/useAuditLogs.ts +39 -0
- package/frontend/src/features/logs/index.ts +5 -0
- package/frontend/src/features/logs/page/AnalyticsLogsPage.tsx +530 -0
- package/frontend/src/features/logs/page/AuditsPage.tsx +192 -0
- package/frontend/src/features/logs/services/log.service.ts +171 -0
- package/frontend/src/features/metadata/hooks/useMetadata.ts +53 -0
- package/frontend/src/features/metadata/index.ts +0 -0
- package/frontend/src/features/metadata/page/MetadataPage.tsx +136 -0
- package/frontend/src/features/metadata/services/metadata.service.ts +17 -0
- package/frontend/src/features/onboard/components/CompletionCard.tsx +41 -0
- package/frontend/src/features/onboard/components/OnboardButton.tsx +84 -0
- package/frontend/src/features/onboard/components/StepContent.tsx +91 -0
- package/frontend/src/features/onboard/components/TestConnectionStep.tsx +53 -0
- package/frontend/src/features/onboard/components/mcp/CursorDeeplinkGenerator.tsx +35 -0
- package/frontend/src/features/onboard/components/mcp/McpInstallation.tsx +144 -0
- package/frontend/src/features/onboard/components/mcp/index.ts +4 -0
- package/frontend/src/features/onboard/components/mcp/mcp-helper.tsx +98 -0
- package/frontend/src/features/onboard/index.ts +3 -0
- package/frontend/src/features/onboard/page/OnBoardPage.tsx +104 -0
- package/frontend/src/features/onboard/types.ts +8 -0
- package/frontend/src/features/secrets/hooks/useSecrets.ts +139 -0
- package/frontend/src/features/secrets/services/secrets.service.ts +57 -0
- package/frontend/src/features/storage/components/BucketEmptyState.tsx +19 -0
- package/frontend/src/features/storage/components/BucketFormDialog.tsx +194 -0
- package/frontend/src/features/storage/components/BucketListSkeleton.tsx +17 -0
- package/frontend/src/features/storage/components/FilePreviewDialog.tsx +287 -0
- package/frontend/src/features/storage/components/StorageDataGrid.tsx +239 -0
- package/frontend/src/features/storage/components/StorageManager.tsx +236 -0
- package/frontend/src/features/storage/components/StorageSidebar.tsx +44 -0
- package/frontend/src/features/storage/components/UploadToast.tsx +46 -0
- package/frontend/src/features/storage/index.ts +3 -0
- package/frontend/src/features/storage/page/StoragePage.tsx +553 -0
- package/frontend/src/features/storage/services/storage.service.ts +144 -0
- package/frontend/src/features/visualizer/components/AuthNode.tsx +107 -0
- package/frontend/src/features/visualizer/components/BucketNode.tsx +34 -0
- package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +359 -0
- package/frontend/src/features/visualizer/components/TableNode.tsx +152 -0
- package/frontend/src/features/visualizer/components/VisualizerSkeleton.tsx +24 -0
- package/frontend/src/features/visualizer/components/index.ts +5 -0
- package/frontend/src/features/visualizer/page/VisualizerPage.tsx +127 -0
- package/frontend/src/index.css +248 -0
- package/frontend/src/lib/api/client.ts +163 -0
- package/frontend/src/lib/contexts/AuthContext.tsx +157 -0
- package/frontend/src/lib/contexts/OnboardStepContext.tsx +68 -0
- package/frontend/src/lib/contexts/SocketContext.tsx +303 -0
- package/frontend/src/lib/contexts/ThemeContext.tsx +125 -0
- package/frontend/src/lib/hooks/useAuth.ts +4 -0
- package/frontend/src/lib/hooks/useConfirm.ts +55 -0
- package/frontend/src/lib/hooks/useInterval.ts +27 -0
- package/frontend/src/lib/hooks/useMediaQuery.ts +59 -0
- package/frontend/src/lib/hooks/useOnboardingCompletion.ts +29 -0
- package/frontend/src/lib/hooks/usePagination.ts +27 -0
- package/frontend/src/lib/hooks/useTimeout.ts +27 -0
- package/frontend/src/lib/hooks/useToast.tsx +229 -0
- package/frontend/src/lib/utils/constants.ts +38 -0
- package/frontend/src/lib/utils/utils.ts +165 -0
- package/frontend/src/lib/utils/validation-schemas.ts +126 -0
- package/frontend/src/main.tsx +16 -0
- package/frontend/src/rdg.css +194 -0
- package/frontend/src/vite-env.d.ts +12 -0
- package/frontend/tailwind.config.js +97 -0
- package/frontend/tsconfig.json +26 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +37 -0
- package/frontend/vitest.config.ts +36 -0
- package/functions/deno.json +25 -0
- package/functions/server.ts +290 -0
- package/functions/worker-template.js +126 -0
- package/openapi/ai.yaml +689 -0
- package/openapi/auth.yaml +563 -0
- package/openapi/functions.yaml +476 -0
- package/openapi/health.yaml +30 -0
- package/openapi/logs.yaml +224 -0
- package/openapi/metadata.yaml +178 -0
- package/openapi/records.yaml +382 -0
- package/openapi/secrets.yaml +371 -0
- package/openapi/storage.yaml +876 -0
- package/openapi/tables.yaml +464 -0
- package/package.json +88 -0
- package/shared-schemas/package.json +31 -0
- package/shared-schemas/src/ai-api.schema.ts +167 -0
- package/shared-schemas/src/ai.schema.ts +54 -0
- package/shared-schemas/src/auth-api.schema.ts +193 -0
- package/shared-schemas/src/auth.schema.ts +94 -0
- package/shared-schemas/src/database-api.schema.ts +259 -0
- package/shared-schemas/src/database.schema.ts +69 -0
- package/shared-schemas/src/functions-api.schema.ts +25 -0
- package/shared-schemas/src/functions.schema.ts +16 -0
- package/shared-schemas/src/index.ts +13 -0
- package/shared-schemas/src/logs-api.schema.ts +49 -0
- package/shared-schemas/src/logs.schema.ts +14 -0
- package/shared-schemas/src/metadata.schema.ts +56 -0
- package/shared-schemas/src/storage-api.schema.ts +65 -0
- package/shared-schemas/src/storage.schema.ts +19 -0
- package/shared-schemas/tsconfig.json +21 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback, type DragEvent } from 'react';
|
|
2
|
+
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
|
|
3
|
+
import { Upload } from 'lucide-react';
|
|
4
|
+
import PencilIcon from '@/assets/icons/pencil.svg?react';
|
|
5
|
+
import RefreshIcon from '@/assets/icons/refresh.svg?react';
|
|
6
|
+
import { storageService } from '@/features/storage/services/storage.service';
|
|
7
|
+
import { Button } from '@/components/radix/Button';
|
|
8
|
+
import { Alert, AlertDescription } from '@/components/radix/Alert';
|
|
9
|
+
import { StorageSidebar } from '@/features/storage/components/StorageSidebar';
|
|
10
|
+
import { StorageManager } from '@/features/storage/components/StorageManager';
|
|
11
|
+
import { BucketFormDialog } from '@/features/storage/components/BucketFormDialog';
|
|
12
|
+
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
|
13
|
+
import { EmptyState } from '@/components/EmptyState';
|
|
14
|
+
import {
|
|
15
|
+
Tooltip,
|
|
16
|
+
TooltipContent,
|
|
17
|
+
TooltipProvider,
|
|
18
|
+
TooltipTrigger,
|
|
19
|
+
} from '@/components/radix/Tooltip';
|
|
20
|
+
import { useConfirm } from '@/lib/hooks/useConfirm';
|
|
21
|
+
import { useToast } from '@/lib/hooks/useToast';
|
|
22
|
+
import { useUploadToast } from '@/features/storage/components/UploadToast';
|
|
23
|
+
import { SearchInput, SelectionClearButton, DeleteActionButton } from '@/components';
|
|
24
|
+
import {
|
|
25
|
+
DataUpdatePayload,
|
|
26
|
+
DataUpdateResourceType,
|
|
27
|
+
ServerEvents,
|
|
28
|
+
SocketMessage,
|
|
29
|
+
useSocket,
|
|
30
|
+
} from '@/lib/contexts/SocketContext';
|
|
31
|
+
|
|
32
|
+
interface BucketFormState {
|
|
33
|
+
mode: 'create' | 'edit';
|
|
34
|
+
name: string | null;
|
|
35
|
+
isPublic: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default function StoragePage() {
|
|
39
|
+
const [selectedBucket, setSelectedBucket] = useState<string | null>(null);
|
|
40
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
41
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
42
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
43
|
+
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
|
44
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
45
|
+
// Bucket form state
|
|
46
|
+
const [bucketFormOpen, setBucketFormOpen] = useState(false);
|
|
47
|
+
const [bucketFormState, setBucketFormState] = useState<BucketFormState>({
|
|
48
|
+
mode: 'create',
|
|
49
|
+
name: null,
|
|
50
|
+
isPublic: false,
|
|
51
|
+
});
|
|
52
|
+
const { confirm, confirmDialogProps } = useConfirm();
|
|
53
|
+
const { showToast } = useToast();
|
|
54
|
+
const { showUploadToast, updateUploadProgress, cancelUpload } = useUploadToast();
|
|
55
|
+
const queryClient = useQueryClient();
|
|
56
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
57
|
+
const uploadAbortControllerRef = useRef<AbortController | null>(null);
|
|
58
|
+
|
|
59
|
+
const { socket, isConnected } = useSocket();
|
|
60
|
+
|
|
61
|
+
// Fetch buckets
|
|
62
|
+
const {
|
|
63
|
+
data: buckets = [],
|
|
64
|
+
isLoading,
|
|
65
|
+
error: bucketsError,
|
|
66
|
+
refetch: refetchBuckets,
|
|
67
|
+
} = useQuery({
|
|
68
|
+
queryKey: ['storage', 'buckets'],
|
|
69
|
+
queryFn: () => storageService.listBuckets(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Fetch bucket statistics
|
|
73
|
+
const { data: bucketStats } = useQuery({
|
|
74
|
+
queryKey: ['storage', 'bucket-stats', buckets],
|
|
75
|
+
queryFn: async () => {
|
|
76
|
+
const stats: Record<
|
|
77
|
+
string,
|
|
78
|
+
{ fileCount: number; totalSize: number; public: boolean; createdAt?: string }
|
|
79
|
+
> = {};
|
|
80
|
+
const currentBuckets = buckets;
|
|
81
|
+
const promises = currentBuckets.map(async (bucket) => {
|
|
82
|
+
try {
|
|
83
|
+
const result = await storageService.listObjects(bucket.name, { limit: 1000 });
|
|
84
|
+
const objects = result.objects;
|
|
85
|
+
const totalSize = objects.reduce((sum, file) => sum + file.size, 0);
|
|
86
|
+
return {
|
|
87
|
+
bucketName: bucket.name,
|
|
88
|
+
stats: {
|
|
89
|
+
fileCount: result.pagination.total,
|
|
90
|
+
totalSize: totalSize,
|
|
91
|
+
public: bucket.public,
|
|
92
|
+
createdAt: bucket.createdAt,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error) {
|
|
97
|
+
console.error(error);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
bucketName: bucket.name,
|
|
102
|
+
stats: {
|
|
103
|
+
fileCount: 0,
|
|
104
|
+
totalSize: 0,
|
|
105
|
+
public: bucket.public,
|
|
106
|
+
createdAt: bucket.createdAt,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const results = await Promise.all(promises);
|
|
112
|
+
results.forEach((result) => {
|
|
113
|
+
if (result) {
|
|
114
|
+
stats[result.bucketName] = result.stats;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return stats;
|
|
118
|
+
},
|
|
119
|
+
enabled: buckets.length > 0,
|
|
120
|
+
staleTime: 30000, // Cache for 30 seconds
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Build bucket info map
|
|
124
|
+
const bucketInfo = React.useMemo(() => {
|
|
125
|
+
return bucketStats || {};
|
|
126
|
+
}, [bucketStats]);
|
|
127
|
+
|
|
128
|
+
// Upload mutation
|
|
129
|
+
const uploadMutation = useMutation({
|
|
130
|
+
mutationFn: async ({
|
|
131
|
+
bucket,
|
|
132
|
+
file,
|
|
133
|
+
fileName,
|
|
134
|
+
}: {
|
|
135
|
+
bucket: string;
|
|
136
|
+
file: File;
|
|
137
|
+
fileName?: string;
|
|
138
|
+
}) => {
|
|
139
|
+
const key = fileName || file.name;
|
|
140
|
+
return await storageService.uploadObject(bucket, key, file);
|
|
141
|
+
},
|
|
142
|
+
onSuccess: () => {
|
|
143
|
+
void queryClient.invalidateQueries({ queryKey: ['storage'] });
|
|
144
|
+
},
|
|
145
|
+
// Remove global onError handler - errors are now handled individually in uploadFiles
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!socket || !isConnected) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const handleDataUpdate = (message: SocketMessage<DataUpdatePayload>) => {
|
|
154
|
+
if (
|
|
155
|
+
message.payload?.resource === DataUpdateResourceType.METADATA ||
|
|
156
|
+
message.payload?.resource === DataUpdateResourceType.STORAGE_SCHEMA
|
|
157
|
+
) {
|
|
158
|
+
// Invalidate all buckets queries
|
|
159
|
+
void queryClient.invalidateQueries({ queryKey: ['storage'] });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
socket.on(ServerEvents.DATA_UPDATE, handleDataUpdate);
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
socket.off(ServerEvents.DATA_UPDATE, handleDataUpdate);
|
|
167
|
+
};
|
|
168
|
+
}, [socket, isConnected, queryClient]);
|
|
169
|
+
|
|
170
|
+
// Auto-select first bucket
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (buckets.length > 0 && !selectedBucket) {
|
|
173
|
+
setSelectedBucket(buckets[0].name);
|
|
174
|
+
}
|
|
175
|
+
}, [buckets, selectedBucket]);
|
|
176
|
+
|
|
177
|
+
const handleRefresh = async () => {
|
|
178
|
+
setIsRefreshing(true);
|
|
179
|
+
try {
|
|
180
|
+
await Promise.all([
|
|
181
|
+
refetchBuckets(),
|
|
182
|
+
queryClient.invalidateQueries({ queryKey: ['storage'] }),
|
|
183
|
+
]);
|
|
184
|
+
} finally {
|
|
185
|
+
setIsRefreshing(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Handle bulk delete files
|
|
190
|
+
const handleBulkDeleteFiles = async (fileKeys: string[]) => {
|
|
191
|
+
if (!selectedBucket || fileKeys.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const shouldDelete = await confirm({
|
|
196
|
+
title: `Delete ${fileKeys.length} ${fileKeys.length === 1 ? 'file' : 'files'}`,
|
|
197
|
+
description: `Are you sure you want to delete ${fileKeys.length} ${fileKeys.length === 1 ? 'file' : 'files'}? This action cannot be undone.`,
|
|
198
|
+
confirmText: 'Delete',
|
|
199
|
+
destructive: true,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (shouldDelete) {
|
|
203
|
+
try {
|
|
204
|
+
await Promise.all(fileKeys.map((key) => storageService.deleteObject(selectedBucket, key)));
|
|
205
|
+
void queryClient.invalidateQueries({ queryKey: ['storage'] });
|
|
206
|
+
setSelectedFiles(new Set());
|
|
207
|
+
showToast(`${fileKeys.length} files deleted successfully`, 'success');
|
|
208
|
+
} catch {
|
|
209
|
+
showToast('Failed to delete some files', 'error');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
void queryClient.invalidateQueries({ queryKey: ['storage'] });
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const uploadFiles = async (files: FileList | File[] | null) => {
|
|
216
|
+
if (!files || files.length === 0 || !selectedBucket) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setIsUploading(true);
|
|
221
|
+
|
|
222
|
+
// Create abort controller for cancellation
|
|
223
|
+
uploadAbortControllerRef.current = new AbortController();
|
|
224
|
+
|
|
225
|
+
// Show upload toast
|
|
226
|
+
const toastId = showUploadToast(files.length, {
|
|
227
|
+
onCancel: () => {
|
|
228
|
+
uploadAbortControllerRef.current?.abort();
|
|
229
|
+
setIsUploading(false);
|
|
230
|
+
if (fileInputRef.current) {
|
|
231
|
+
fileInputRef.current.value = '';
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
let successCount = 0;
|
|
237
|
+
|
|
238
|
+
// Upload files sequentially with individual error handling
|
|
239
|
+
for (let i = 0; i < files.length; i++) {
|
|
240
|
+
if (uploadAbortControllerRef.current?.signal.aborted) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Update progress
|
|
245
|
+
const progress = Math.round(((i + 1) / files.length) * 100);
|
|
246
|
+
updateUploadProgress(toastId, progress);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await uploadMutation.mutateAsync({
|
|
250
|
+
bucket: selectedBucket,
|
|
251
|
+
file: files[i],
|
|
252
|
+
fileName: files[i].name, // Backend will auto-rename if needed
|
|
253
|
+
});
|
|
254
|
+
successCount++;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// Handle individual file upload error
|
|
257
|
+
const fileName = files[i].name;
|
|
258
|
+
|
|
259
|
+
// Show individual file error (but don't stop the overall process)
|
|
260
|
+
const errorMessage = error instanceof Error ? error.message : 'Upload failed';
|
|
261
|
+
showToast(`Failed to upload "${fileName}": ${errorMessage}`, 'error');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
showToast(`${successCount} files uploaded successfully`, 'success');
|
|
265
|
+
|
|
266
|
+
// Complete the upload toast
|
|
267
|
+
cancelUpload(toastId);
|
|
268
|
+
|
|
269
|
+
// Always reset uploading state
|
|
270
|
+
setIsUploading(false);
|
|
271
|
+
uploadAbortControllerRef.current = null;
|
|
272
|
+
|
|
273
|
+
// Reset file input
|
|
274
|
+
if (fileInputRef.current) {
|
|
275
|
+
fileInputRef.current.value = '';
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const handleFileUpload = useCallback(uploadFiles, [
|
|
280
|
+
cancelUpload,
|
|
281
|
+
selectedBucket,
|
|
282
|
+
showToast,
|
|
283
|
+
showUploadToast,
|
|
284
|
+
updateUploadProgress,
|
|
285
|
+
uploadMutation,
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
289
|
+
await handleFileUpload(event.target.files);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const handleDeleteBucket = async (bucketName: string) => {
|
|
293
|
+
const confirmOptions = {
|
|
294
|
+
title: 'Delete Bucket',
|
|
295
|
+
description: `Are you sure you want to delete the bucket "${bucketName}"? This will permanently delete all files in this bucket. This action cannot be undone.`,
|
|
296
|
+
confirmText: 'Delete',
|
|
297
|
+
destructive: true,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const shouldDelete = await confirm(confirmOptions);
|
|
301
|
+
|
|
302
|
+
if (shouldDelete) {
|
|
303
|
+
try {
|
|
304
|
+
await storageService.deleteBucket(bucketName);
|
|
305
|
+
|
|
306
|
+
// Refresh buckets list
|
|
307
|
+
await refetchBuckets();
|
|
308
|
+
showToast('Bucket deleted successfully', 'success');
|
|
309
|
+
|
|
310
|
+
// If the deleted bucket was selected, select the first available bucket
|
|
311
|
+
if (selectedBucket === bucketName) {
|
|
312
|
+
const updatedBuckets =
|
|
313
|
+
queryClient.getQueryData<typeof buckets>(['storage', 'buckets']) || [];
|
|
314
|
+
setSelectedBucket(updatedBuckets[0]?.name || null);
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to delete bucket';
|
|
318
|
+
showToast(errorMessage, 'error');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const handleEditBucket = (bucketName: string) => {
|
|
324
|
+
// Get current bucket's public status
|
|
325
|
+
const info = bucketInfo[bucketName];
|
|
326
|
+
setBucketFormState({
|
|
327
|
+
mode: 'edit',
|
|
328
|
+
name: bucketName,
|
|
329
|
+
isPublic: info?.public ?? false,
|
|
330
|
+
});
|
|
331
|
+
setBucketFormOpen(true);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const handleDragOver = useCallback((event: DragEvent<HTMLDivElement>) => {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
setIsDragging(true);
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
339
|
+
const handleDragLeave = useCallback((_event: DragEvent<HTMLDivElement>) => {
|
|
340
|
+
setIsDragging(false);
|
|
341
|
+
}, []);
|
|
342
|
+
|
|
343
|
+
const handleDrop = useCallback(
|
|
344
|
+
(event: DragEvent<HTMLDivElement>) => {
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
setIsDragging(false);
|
|
347
|
+
|
|
348
|
+
// To support only file uploads (not directories), we filter through
|
|
349
|
+
// dataTransfer.items instead of directly using dataTransfer.files.
|
|
350
|
+
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
|
|
351
|
+
const fileItems: File[] = Array.from(event.dataTransfer.items)
|
|
352
|
+
.filter((item) => item.webkitGetAsEntry()?.isFile)
|
|
353
|
+
.map((item) => item.getAsFile())
|
|
354
|
+
.filter((item) => item !== null);
|
|
355
|
+
|
|
356
|
+
void handleFileUpload(fileItems);
|
|
357
|
+
},
|
|
358
|
+
[handleFileUpload]
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const error = bucketsError;
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div className="flex h-full bg-bg-gray dark:bg-neutral-800">
|
|
365
|
+
{/* Secondary Sidebar - Bucket List */}
|
|
366
|
+
<StorageSidebar
|
|
367
|
+
buckets={Object.keys(bucketInfo)}
|
|
368
|
+
selectedBucket={selectedBucket || undefined}
|
|
369
|
+
onBucketSelect={setSelectedBucket}
|
|
370
|
+
loading={isLoading}
|
|
371
|
+
onNewBucket={() => {
|
|
372
|
+
setBucketFormState({
|
|
373
|
+
mode: 'create',
|
|
374
|
+
name: null,
|
|
375
|
+
isPublic: true,
|
|
376
|
+
});
|
|
377
|
+
setBucketFormOpen(true);
|
|
378
|
+
}}
|
|
379
|
+
onEditBucket={handleEditBucket}
|
|
380
|
+
onDeleteBucket={(bucketName) => void handleDeleteBucket(bucketName)}
|
|
381
|
+
/>
|
|
382
|
+
|
|
383
|
+
{/* Main Content Area */}
|
|
384
|
+
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
|
385
|
+
{selectedBucket && (
|
|
386
|
+
<>
|
|
387
|
+
{/* Sticky Header Section */}
|
|
388
|
+
<div className="sticky top-0 z-30 bg-bg-gray dark:bg-neutral-800">
|
|
389
|
+
<div className="pl-4 pr-1.5 py-1.5 h-12">
|
|
390
|
+
{/* Page Header with Breadcrumb */}
|
|
391
|
+
<div className="flex items-center justify-between">
|
|
392
|
+
<div className="flex items-center gap-3">
|
|
393
|
+
{selectedBucket && (
|
|
394
|
+
<nav className="flex items-center text-base font-semibold">
|
|
395
|
+
<span className="text-black dark:text-white">{selectedBucket}</span>
|
|
396
|
+
</nav>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{/* Separator */}
|
|
400
|
+
<div className="h-6 w-px bg-gray-200 dark:bg-neutral-700" />
|
|
401
|
+
|
|
402
|
+
{/* Action buttons group */}
|
|
403
|
+
<div className="flex items-center gap-1">
|
|
404
|
+
<TooltipProvider>
|
|
405
|
+
{selectedBucket && (
|
|
406
|
+
<Tooltip>
|
|
407
|
+
<TooltipTrigger asChild>
|
|
408
|
+
<Button
|
|
409
|
+
variant="ghost"
|
|
410
|
+
size="icon"
|
|
411
|
+
className="p-1 h-9 w-9"
|
|
412
|
+
onClick={() => handleEditBucket(selectedBucket)}
|
|
413
|
+
>
|
|
414
|
+
<PencilIcon className="h-5 w-5 text-zinc-400 dark:text-neutral-400" />
|
|
415
|
+
</Button>
|
|
416
|
+
</TooltipTrigger>
|
|
417
|
+
<TooltipContent side="bottom" align="center">
|
|
418
|
+
<p>Edit Bucket</p>
|
|
419
|
+
</TooltipContent>
|
|
420
|
+
</Tooltip>
|
|
421
|
+
)}
|
|
422
|
+
<Tooltip>
|
|
423
|
+
<TooltipTrigger asChild>
|
|
424
|
+
<Button
|
|
425
|
+
variant="ghost"
|
|
426
|
+
size="icon"
|
|
427
|
+
className="p-1 h-9 w-9"
|
|
428
|
+
onClick={() => void handleRefresh()}
|
|
429
|
+
disabled={isRefreshing}
|
|
430
|
+
>
|
|
431
|
+
<RefreshIcon className="h-5 w-5 text-zinc-400 dark:text-neutral-400" />
|
|
432
|
+
</Button>
|
|
433
|
+
</TooltipTrigger>
|
|
434
|
+
<TooltipContent side="bottom" align="center">
|
|
435
|
+
<p>{isRefreshing ? 'Refreshing...' : 'Refresh'}</p>
|
|
436
|
+
</TooltipContent>
|
|
437
|
+
</Tooltip>
|
|
438
|
+
</TooltipProvider>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div className="pt-2 px-3 pb-4">
|
|
444
|
+
{/* Search Bar and Actions - only show when bucket is selected */}
|
|
445
|
+
{selectedBucket && (
|
|
446
|
+
<div className="flex items-center justify-between">
|
|
447
|
+
{selectedFiles.size > 0 ? (
|
|
448
|
+
<div className="flex items-center gap-3">
|
|
449
|
+
<SelectionClearButton
|
|
450
|
+
selectedCount={selectedFiles.size}
|
|
451
|
+
itemType="file"
|
|
452
|
+
onClear={() => setSelectedFiles(new Set())}
|
|
453
|
+
/>
|
|
454
|
+
<DeleteActionButton
|
|
455
|
+
selectedCount={selectedFiles.size}
|
|
456
|
+
itemType="file"
|
|
457
|
+
onDelete={() => void handleBulkDeleteFiles(Array.from(selectedFiles))}
|
|
458
|
+
/>
|
|
459
|
+
</div>
|
|
460
|
+
) : (
|
|
461
|
+
<SearchInput
|
|
462
|
+
value={searchQuery}
|
|
463
|
+
onChange={setSearchQuery}
|
|
464
|
+
placeholder="Search Files by Name"
|
|
465
|
+
className="flex-1 max-w-80 dark:bg-neutral-800 dark:text-white dark:placeholder:text-neutral-400 dark:border-neutral-700"
|
|
466
|
+
debounceTime={300}
|
|
467
|
+
/>
|
|
468
|
+
)}
|
|
469
|
+
<div className="flex items-center gap-2 ml-4">
|
|
470
|
+
{selectedFiles.size === 0 && (
|
|
471
|
+
<>
|
|
472
|
+
{/* Upload File Button - moved here when no files selected */}
|
|
473
|
+
<input
|
|
474
|
+
ref={fileInputRef}
|
|
475
|
+
type="file"
|
|
476
|
+
multiple
|
|
477
|
+
onChange={(e) => void handleFileSelect(e)}
|
|
478
|
+
className="hidden"
|
|
479
|
+
accept="*"
|
|
480
|
+
style={{ display: 'none' }}
|
|
481
|
+
/>
|
|
482
|
+
<Button
|
|
483
|
+
className="h-10 px-4 font-medium gap-1.5 dark:bg-emerald-300 dark:text-zinc-950 dark:hover:bg-emerald-400"
|
|
484
|
+
onClick={() => fileInputRef.current?.click()}
|
|
485
|
+
disabled={isUploading}
|
|
486
|
+
>
|
|
487
|
+
<Upload className="w-5 h-5" />
|
|
488
|
+
{isUploading ? 'Uploading...' : 'Upload File'}
|
|
489
|
+
</Button>
|
|
490
|
+
</>
|
|
491
|
+
)}
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
{/* Content (supports drag-and-drop file upload) */}
|
|
499
|
+
<div
|
|
500
|
+
className={
|
|
501
|
+
'relative flex-1 flex flex-col overflow-hidden' + (isDragging ? ' opacity-25' : '')
|
|
502
|
+
}
|
|
503
|
+
onDragOver={handleDragOver}
|
|
504
|
+
onDragLeave={handleDragLeave}
|
|
505
|
+
onDrop={handleDrop}
|
|
506
|
+
>
|
|
507
|
+
{error && (
|
|
508
|
+
<Alert variant="destructive" className="mb-4 mx-8 mt-4">
|
|
509
|
+
<AlertDescription>{String(error)}</AlertDescription>
|
|
510
|
+
</Alert>
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
<StorageManager
|
|
514
|
+
bucketName={selectedBucket}
|
|
515
|
+
fileCount={bucketStats?.[selectedBucket]?.fileCount || 0}
|
|
516
|
+
searchQuery={searchQuery}
|
|
517
|
+
selectedFiles={selectedFiles}
|
|
518
|
+
onSelectedFilesChange={setSelectedFiles}
|
|
519
|
+
isRefreshing={isRefreshing}
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
</>
|
|
523
|
+
)}
|
|
524
|
+
{!selectedBucket && (
|
|
525
|
+
<div className="flex-1 flex items-center justify-center">
|
|
526
|
+
<EmptyState
|
|
527
|
+
title="No Bucket Selected"
|
|
528
|
+
description="Select a bucket from the sidebar to view its files"
|
|
529
|
+
/>
|
|
530
|
+
</div>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
{/* Bucket Form (handles both create and edit) */}
|
|
535
|
+
<BucketFormDialog
|
|
536
|
+
open={bucketFormOpen}
|
|
537
|
+
onOpenChange={setBucketFormOpen}
|
|
538
|
+
mode={bucketFormState.mode}
|
|
539
|
+
initialBucketName={bucketFormState.name || ''}
|
|
540
|
+
initialIsPublic={bucketFormState.isPublic}
|
|
541
|
+
onSuccess={(bucketName) => {
|
|
542
|
+
void refetchBuckets();
|
|
543
|
+
if (bucketFormState.mode === 'create' && bucketName) {
|
|
544
|
+
setSelectedBucket(bucketName);
|
|
545
|
+
}
|
|
546
|
+
}}
|
|
547
|
+
/>
|
|
548
|
+
|
|
549
|
+
{/* Confirm Dialog */}
|
|
550
|
+
<ConfirmDialog {...confirmDialogProps} />
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { apiClient } from '@/lib/api/client';
|
|
2
|
+
import {
|
|
3
|
+
StorageFileSchema,
|
|
4
|
+
StorageBucketSchema,
|
|
5
|
+
ListObjectsResponseSchema,
|
|
6
|
+
} from '@insforge/shared-schemas';
|
|
7
|
+
|
|
8
|
+
export interface ListObjectsParams {
|
|
9
|
+
prefix?: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
offset?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const storageService = {
|
|
15
|
+
// List all buckets
|
|
16
|
+
async listBuckets(): Promise<StorageBucketSchema[]> {
|
|
17
|
+
const response = await apiClient.request('/storage/buckets', {
|
|
18
|
+
headers: apiClient.withAccessToken(),
|
|
19
|
+
});
|
|
20
|
+
// Traditional REST: API returns array directly
|
|
21
|
+
return response;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// List objects in a bucket
|
|
25
|
+
async listObjects(
|
|
26
|
+
bucketName: string,
|
|
27
|
+
params?: ListObjectsParams,
|
|
28
|
+
searchQuery?: string
|
|
29
|
+
): Promise<ListObjectsResponseSchema> {
|
|
30
|
+
const searchParams = new URLSearchParams();
|
|
31
|
+
if (params?.prefix) {
|
|
32
|
+
searchParams.append('prefix', params.prefix);
|
|
33
|
+
}
|
|
34
|
+
if (params?.limit) {
|
|
35
|
+
searchParams.append('limit', params.limit.toString());
|
|
36
|
+
}
|
|
37
|
+
if (params?.offset) {
|
|
38
|
+
searchParams.append('offset', params.offset.toString());
|
|
39
|
+
}
|
|
40
|
+
if (searchQuery && searchQuery.trim()) {
|
|
41
|
+
searchParams.append('search', searchQuery.trim());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = `/storage/buckets/${encodeURIComponent(bucketName)}/objects${searchParams.toString() ? `?${searchParams}` : ''}`;
|
|
45
|
+
const response: {
|
|
46
|
+
data: StorageFileSchema[];
|
|
47
|
+
pagination: { offset: number; limit: number; total: number };
|
|
48
|
+
} = await apiClient.request(url, {
|
|
49
|
+
headers: apiClient.withAccessToken(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
objects: response.data,
|
|
54
|
+
pagination: response.pagination,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Upload an object to bucket
|
|
59
|
+
async uploadObject(
|
|
60
|
+
bucketName: string,
|
|
61
|
+
objectKey: string,
|
|
62
|
+
object: File
|
|
63
|
+
): Promise<StorageFileSchema> {
|
|
64
|
+
const formData = new FormData();
|
|
65
|
+
formData.append('file', object);
|
|
66
|
+
|
|
67
|
+
// Use fetch directly for object uploads to avoid Content-Type header issues
|
|
68
|
+
const response = await fetch(
|
|
69
|
+
`/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`,
|
|
70
|
+
{
|
|
71
|
+
method: 'PUT',
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: `Bearer ${apiClient.getToken()}`,
|
|
74
|
+
},
|
|
75
|
+
body: formData,
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const error = await response.json();
|
|
81
|
+
// Traditional REST error format
|
|
82
|
+
throw new Error(error.message || error.error || 'Upload failed');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await response.json();
|
|
86
|
+
// Traditional REST: response returned directly
|
|
87
|
+
return result;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Get download URL for an object
|
|
91
|
+
getDownloadUrl(bucketName: string, objectKey: string): string {
|
|
92
|
+
return `/api/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// Download an object (returns blob)
|
|
96
|
+
async downloadObject(bucketName: string, objectKey: string): Promise<Blob> {
|
|
97
|
+
const response = await fetch(this.getDownloadUrl(bucketName, objectKey), {
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${apiClient.getToken()}`,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new Error(`Failed to download object: ${response.statusText}`);
|
|
104
|
+
}
|
|
105
|
+
return await response.blob();
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Delete an object
|
|
109
|
+
async deleteObject(bucketName: string, objectKey: string): Promise<void> {
|
|
110
|
+
await apiClient.request(
|
|
111
|
+
`/storage/buckets/${encodeURIComponent(bucketName)}/objects/${encodeURIComponent(objectKey)}`,
|
|
112
|
+
{
|
|
113
|
+
method: 'DELETE',
|
|
114
|
+
headers: apiClient.withAccessToken(),
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
// Create a new bucket
|
|
120
|
+
async createBucket(bucketName: string, isPublic: boolean = true): Promise<void> {
|
|
121
|
+
await apiClient.request('/storage/buckets', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: apiClient.withAccessToken(),
|
|
124
|
+
body: JSON.stringify({ bucketName: bucketName, isPublic: isPublic }),
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Delete entire bucket
|
|
129
|
+
async deleteBucket(bucketName: string): Promise<void> {
|
|
130
|
+
await apiClient.request(`/storage/buckets/${encodeURIComponent(bucketName)}`, {
|
|
131
|
+
method: 'DELETE',
|
|
132
|
+
headers: apiClient.withAccessToken(),
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// Edit bucket (update visibility or other config)
|
|
137
|
+
async editBucket(bucketName: string, config: { isPublic: boolean }): Promise<void> {
|
|
138
|
+
await apiClient.request(`/storage/buckets/${encodeURIComponent(bucketName)}`, {
|
|
139
|
+
method: 'PATCH',
|
|
140
|
+
headers: apiClient.withAccessToken(),
|
|
141
|
+
body: JSON.stringify(config),
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
};
|