@open-mercato/core 0.4.7-develop-0a657b411f → 0.4.7-develop-e249d3e7d0
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/dist/generated/entities/carrier_shipment/index.js +37 -0
- package/dist/generated/entities/carrier_shipment/index.js.map +7 -0
- package/dist/generated/entities/gateway_transaction/index.js +47 -0
- package/dist/generated/entities/gateway_transaction/index.js.map +7 -0
- package/dist/generated/entities/webhook_processed_event/index.js +17 -0
- package/dist/generated/entities/webhook_processed_event/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +10 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +6 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +14 -5
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.meta.js +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.meta.js.map +1 -1
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +37 -12
- package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
- package/dist/modules/directory/api/get/tenants/lookup.js +1 -0
- package/dist/modules/directory/api/get/tenants/lookup.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +38 -11
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/logs/route.js +52 -26
- package/dist/modules/integrations/api/logs/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +37 -7
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/api/umes-read.js +121 -0
- package/dist/modules/integrations/api/umes-read.js.map +7 -0
- package/dist/modules/integrations/backend/integrations/[id]/page.js +715 -183
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +30 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/detail-page-widgets.js +46 -0
- package/dist/modules/integrations/backend/integrations/detail-page-widgets.js.map +7 -0
- package/dist/modules/integrations/backend/integrations/page.js +78 -62
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.meta.js +2 -2
- package/dist/modules/integrations/backend/integrations/page.meta.js.map +1 -1
- package/dist/modules/integrations/setup.js +2 -2
- package/dist/modules/integrations/setup.js.map +2 -2
- package/dist/modules/payment_gateways/acl.js +12 -0
- package/dist/modules/payment_gateways/acl.js.map +7 -0
- package/dist/modules/payment_gateways/api/cancel/route.js +55 -0
- package/dist/modules/payment_gateways/api/cancel/route.js.map +7 -0
- package/dist/modules/payment_gateways/api/capture/route.js +55 -0
- package/dist/modules/payment_gateways/api/capture/route.js.map +7 -0
- package/dist/modules/payment_gateways/api/interceptors.js +24 -0
- package/dist/modules/payment_gateways/api/interceptors.js.map +7 -0
- package/dist/modules/payment_gateways/api/openapi.js +5 -0
- package/dist/modules/payment_gateways/api/openapi.js.map +7 -0
- package/dist/modules/payment_gateways/api/refund/route.js +56 -0
- package/dist/modules/payment_gateways/api/refund/route.js.map +7 -0
- package/dist/modules/payment_gateways/api/sessions/route.js +74 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +7 -0
- package/dist/modules/payment_gateways/api/status/route.js +66 -0
- package/dist/modules/payment_gateways/api/status/route.js.map +7 -0
- package/dist/modules/payment_gateways/api/transactions/[id]/route.js +118 -0
- package/dist/modules/payment_gateways/api/transactions/[id]/route.js.map +7 -0
- package/dist/modules/payment_gateways/api/transactions/route.js +113 -0
- package/dist/modules/payment_gateways/api/transactions/route.js.map +7 -0
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +136 -0
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +7 -0
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +496 -0
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +7 -0
- package/dist/modules/payment_gateways/backend/payment-gateways/page.meta.js +23 -0
- package/dist/modules/payment_gateways/backend/payment-gateways/page.meta.js.map +7 -0
- package/dist/modules/payment_gateways/data/enrichers.js +5 -0
- package/dist/modules/payment_gateways/data/enrichers.js.map +7 -0
- package/dist/modules/payment_gateways/data/entities.js +131 -0
- package/dist/modules/payment_gateways/data/entities.js.map +7 -0
- package/dist/modules/payment_gateways/data/validators.js +57 -0
- package/dist/modules/payment_gateways/data/validators.js.map +7 -0
- package/dist/modules/payment_gateways/di.js +16 -0
- package/dist/modules/payment_gateways/di.js.map +7 -0
- package/dist/modules/payment_gateways/events.js +21 -0
- package/dist/modules/payment_gateways/events.js.map +7 -0
- package/dist/modules/payment_gateways/i18n/en.js +6 -0
- package/dist/modules/payment_gateways/i18n/en.js.map +7 -0
- package/dist/modules/payment_gateways/i18n/pl.js +6 -0
- package/dist/modules/payment_gateways/i18n/pl.js.map +7 -0
- package/dist/modules/payment_gateways/index.js +9 -0
- package/dist/modules/payment_gateways/index.js.map +7 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js +378 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +7 -0
- package/dist/modules/payment_gateways/lib/queue.js +17 -0
- package/dist/modules/payment_gateways/lib/queue.js.map +7 -0
- package/dist/modules/payment_gateways/lib/status-machine.js +29 -0
- package/dist/modules/payment_gateways/lib/status-machine.js.map +7 -0
- package/dist/modules/payment_gateways/lib/webhook-processor.js +88 -0
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +7 -0
- package/dist/modules/payment_gateways/lib/webhook-utils.js +42 -0
- package/dist/modules/payment_gateways/lib/webhook-utils.js.map +7 -0
- package/dist/modules/payment_gateways/migrations/Migration20260305122155.js +19 -0
- package/dist/modules/payment_gateways/migrations/Migration20260305122155.js.map +7 -0
- package/dist/modules/payment_gateways/setup.js +13 -0
- package/dist/modules/payment_gateways/setup.js.map +7 -0
- package/dist/modules/payment_gateways/widgets/injection-table.js +7 -0
- package/dist/modules/payment_gateways/widgets/injection-table.js.map +7 -0
- package/dist/modules/payment_gateways/workers/status-poller.js +44 -0
- package/dist/modules/payment_gateways/workers/status-poller.js.map +7 -0
- package/dist/modules/payment_gateways/workers/webhook-processor.js +20 -0
- package/dist/modules/payment_gateways/workers/webhook-processor.js.map +7 -0
- package/dist/modules/sales/data/enrichers.js +72 -0
- package/dist/modules/sales/data/enrichers.js.map +7 -0
- package/dist/modules/sales/lib/makeSalesLineRoute.js +3 -0
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/sales/widgets/injection/payment-gateway-config-field/widget.js +29 -0
- package/dist/modules/sales/widgets/injection/payment-gateway-config-field/widget.js.map +7 -0
- package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js +23 -0
- package/dist/modules/sales/widgets/injection/payment-gateway-status-column/widget.js.map +7 -0
- package/dist/modules/sales/widgets/injection-table.js +13 -1
- package/dist/modules/sales/widgets/injection-table.js.map +2 -2
- package/dist/modules/shipping_carriers/acl.js +10 -0
- package/dist/modules/shipping_carriers/acl.js.map +7 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js +55 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +7 -0
- package/dist/modules/shipping_carriers/api/interceptors.js +21 -0
- package/dist/modules/shipping_carriers/api/interceptors.js.map +7 -0
- package/dist/modules/shipping_carriers/api/openapi.js +5 -0
- package/dist/modules/shipping_carriers/api/openapi.js.map +7 -0
- package/dist/modules/shipping_carriers/api/rates/route.js +55 -0
- package/dist/modules/shipping_carriers/api/rates/route.js.map +7 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js +61 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +7 -0
- package/dist/modules/shipping_carriers/api/tracking/route.js +58 -0
- package/dist/modules/shipping_carriers/api/tracking/route.js.map +7 -0
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js +119 -0
- package/dist/modules/shipping_carriers/api/webhook/[provider]/route.js.map +7 -0
- package/dist/modules/shipping_carriers/data/enrichers.js +82 -0
- package/dist/modules/shipping_carriers/data/enrichers.js.map +7 -0
- package/dist/modules/shipping_carriers/data/entities.js +80 -0
- package/dist/modules/shipping_carriers/data/entities.js.map +7 -0
- package/dist/modules/shipping_carriers/data/validators.js +49 -0
- package/dist/modules/shipping_carriers/data/validators.js.map +7 -0
- package/dist/modules/shipping_carriers/di.js +15 -0
- package/dist/modules/shipping_carriers/di.js.map +7 -0
- package/dist/modules/shipping_carriers/events.js +19 -0
- package/dist/modules/shipping_carriers/events.js.map +7 -0
- package/dist/modules/shipping_carriers/i18n/en.js +11 -0
- package/dist/modules/shipping_carriers/i18n/en.js.map +7 -0
- package/dist/modules/shipping_carriers/i18n/pl.js +11 -0
- package/dist/modules/shipping_carriers/i18n/pl.js.map +7 -0
- package/dist/modules/shipping_carriers/index.js +9 -0
- package/dist/modules/shipping_carriers/index.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/adapter-registry.js +29 -0
- package/dist/modules/shipping_carriers/lib/adapter-registry.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/adapter.js +1 -0
- package/dist/modules/shipping_carriers/lib/adapter.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/queue.js +17 -0
- package/dist/modules/shipping_carriers/lib/queue.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipping-service.js +155 -0
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/status-sync.js +37 -0
- package/dist/modules/shipping_carriers/lib/status-sync.js.map +7 -0
- package/dist/modules/shipping_carriers/migrations/Migration20260305170000.js +16 -0
- package/dist/modules/shipping_carriers/migrations/Migration20260305170000.js.map +7 -0
- package/dist/modules/shipping_carriers/setup.js +13 -0
- package/dist/modules/shipping_carriers/setup.js.map +7 -0
- package/dist/modules/shipping_carriers/widgets/injection/create-shipment-button/widget.js +25 -0
- package/dist/modules/shipping_carriers/widgets/injection/create-shipment-button/widget.js.map +7 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-column/widget.js +23 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-column/widget.js.map +7 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js +40 -0
- package/dist/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.js.map +7 -0
- package/dist/modules/shipping_carriers/widgets/injection-table.js +24 -0
- package/dist/modules/shipping_carriers/widgets/injection-table.js.map +7 -0
- package/dist/modules/shipping_carriers/workers/status-poller.js +21 -0
- package/dist/modules/shipping_carriers/workers/status-poller.js.map +7 -0
- package/dist/modules/shipping_carriers/workers/webhook-processor.js +54 -0
- package/dist/modules/shipping_carriers/workers/webhook-processor.js.map +7 -0
- package/dist/modules/translations/api/get/locales.js +1 -0
- package/dist/modules/translations/api/get/locales.js.map +2 -2
- package/dist/modules/translations/api/put/locales.js +1 -0
- package/dist/modules/translations/api/put/locales.js.map +2 -2
- package/generated/entities/carrier_shipment/index.ts +17 -0
- package/generated/entities/gateway_transaction/index.ts +22 -0
- package/generated/entities/webhook_processed_event/index.ts +7 -0
- package/generated/entities.ids.generated.ts +10 -1
- package/generated/entity-fields-registry.ts +6 -0
- package/jest.config.cjs +1 -0
- package/package.json +5 -2
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +18 -5
- package/src/modules/data_sync/backend/data-sync/page.meta.ts +2 -2
- package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +50 -12
- package/src/modules/directory/api/get/tenants/lookup.ts +1 -0
- package/src/modules/integrations/AGENTS.md +31 -0
- package/src/modules/integrations/api/[id]/route.ts +38 -11
- package/src/modules/integrations/api/logs/route.ts +53 -27
- package/src/modules/integrations/api/route.ts +31 -1
- package/src/modules/integrations/api/umes-read.ts +177 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +902 -202
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +43 -9
- package/src/modules/integrations/backend/integrations/detail-page-widgets.ts +74 -0
- package/src/modules/integrations/backend/integrations/page.meta.ts +2 -2
- package/src/modules/integrations/backend/integrations/page.tsx +65 -54
- package/src/modules/integrations/i18n/de.json +15 -0
- package/src/modules/integrations/i18n/en.json +15 -0
- package/src/modules/integrations/i18n/es.json +15 -0
- package/src/modules/integrations/i18n/pl.json +15 -0
- package/src/modules/integrations/setup.ts +2 -2
- package/src/modules/payment_gateways/acl.ts +8 -0
- package/src/modules/payment_gateways/api/cancel/route.ts +56 -0
- package/src/modules/payment_gateways/api/capture/route.ts +56 -0
- package/src/modules/payment_gateways/api/interceptors.ts +22 -0
- package/src/modules/payment_gateways/api/openapi.ts +1 -0
- package/src/modules/payment_gateways/api/refund/route.ts +57 -0
- package/src/modules/payment_gateways/api/sessions/route.ts +76 -0
- package/src/modules/payment_gateways/api/status/route.ts +69 -0
- package/src/modules/payment_gateways/api/transactions/[id]/route.ts +123 -0
- package/src/modules/payment_gateways/api/transactions/route.ts +120 -0
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +161 -0
- package/src/modules/payment_gateways/backend/payment-gateways/page.meta.ts +19 -0
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +660 -0
- package/src/modules/payment_gateways/data/enrichers.ts +8 -0
- package/src/modules/payment_gateways/data/entities.ts +106 -0
- package/src/modules/payment_gateways/data/validators.ts +67 -0
- package/src/modules/payment_gateways/di.ts +26 -0
- package/src/modules/payment_gateways/events.ts +17 -0
- package/src/modules/payment_gateways/i18n/de.json +77 -0
- package/src/modules/payment_gateways/i18n/en.json +77 -0
- package/src/modules/payment_gateways/i18n/en.ts +4 -0
- package/src/modules/payment_gateways/i18n/es.json +77 -0
- package/src/modules/payment_gateways/i18n/pl.json +77 -0
- package/src/modules/payment_gateways/i18n/pl.ts +4 -0
- package/src/modules/payment_gateways/index.ts +5 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +486 -0
- package/src/modules/payment_gateways/lib/queue.ts +19 -0
- package/src/modules/payment_gateways/lib/status-machine.ts +28 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +133 -0
- package/src/modules/payment_gateways/lib/webhook-utils.ts +52 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +373 -0
- package/src/modules/payment_gateways/migrations/Migration20260305122155.ts +20 -0
- package/src/modules/payment_gateways/setup.ts +11 -0
- package/src/modules/payment_gateways/widgets/injection-table.ts +9 -0
- package/src/modules/payment_gateways/workers/status-poller.ts +58 -0
- package/src/modules/payment_gateways/workers/webhook-processor.ts +30 -0
- package/src/modules/sales/data/enrichers.ts +120 -0
- package/src/modules/sales/lib/makeSalesLineRoute.ts +3 -0
- package/src/modules/sales/widgets/injection/payment-gateway-config-field/widget.ts +28 -0
- package/src/modules/sales/widgets/injection/payment-gateway-status-column/widget.ts +22 -0
- package/src/modules/sales/widgets/injection-table.ts +12 -0
- package/src/modules/shipping_carriers/acl.ts +6 -0
- package/src/modules/shipping_carriers/api/cancel/route.ts +53 -0
- package/src/modules/shipping_carriers/api/interceptors.ts +19 -0
- package/src/modules/shipping_carriers/api/openapi.ts +1 -0
- package/src/modules/shipping_carriers/api/rates/route.ts +53 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +59 -0
- package/src/modules/shipping_carriers/api/tracking/route.ts +56 -0
- package/src/modules/shipping_carriers/api/webhook/[provider]/route.ts +134 -0
- package/src/modules/shipping_carriers/data/enrichers.ts +89 -0
- package/src/modules/shipping_carriers/data/entities.ts +60 -0
- package/src/modules/shipping_carriers/data/validators.ts +48 -0
- package/src/modules/shipping_carriers/di.ts +20 -0
- package/src/modules/shipping_carriers/events.ts +16 -0
- package/src/modules/shipping_carriers/i18n/de.json +7 -0
- package/src/modules/shipping_carriers/i18n/en.json +7 -0
- package/src/modules/shipping_carriers/i18n/en.ts +7 -0
- package/src/modules/shipping_carriers/i18n/es.json +7 -0
- package/src/modules/shipping_carriers/i18n/pl.json +7 -0
- package/src/modules/shipping_carriers/i18n/pl.ts +7 -0
- package/src/modules/shipping_carriers/index.ts +5 -0
- package/src/modules/shipping_carriers/lib/adapter-registry.ts +33 -0
- package/src/modules/shipping_carriers/lib/adapter.ts +93 -0
- package/src/modules/shipping_carriers/lib/queue.ts +19 -0
- package/src/modules/shipping_carriers/lib/shipping-service.ts +204 -0
- package/src/modules/shipping_carriers/lib/status-sync.ts +38 -0
- package/src/modules/shipping_carriers/migrations/Migration20260305170000.ts +14 -0
- package/src/modules/shipping_carriers/setup.ts +11 -0
- package/src/modules/shipping_carriers/widgets/injection/create-shipment-button/widget.ts +24 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-column/widget.ts +22 -0
- package/src/modules/shipping_carriers/widgets/injection/tracking-status-badge/widget.tsx +44 -0
- package/src/modules/shipping_carriers/widgets/injection-table.ts +22 -0
- package/src/modules/shipping_carriers/workers/status-poller.ts +33 -0
- package/src/modules/shipping_carriers/workers/webhook-processor.ts +79 -0
- package/src/modules/translations/api/get/locales.ts +1 -0
- package/src/modules/translations/api/put/locales.ts +1 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
4
|
+
import { z } from 'zod'
|
|
5
5
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
|
+
import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
7
|
+
import { WebhookSetupGuide } from '@open-mercato/ui/backend/WebhookSetupGuide'
|
|
8
|
+
import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
9
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
10
|
+
import { FormHeader } from '@open-mercato/ui/backend/forms'
|
|
6
11
|
import { Card, CardHeader, CardTitle, CardContent } from '@open-mercato/ui/primitives/card'
|
|
7
12
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
8
13
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
@@ -10,14 +15,24 @@ import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
|
10
15
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
11
16
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
12
17
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
18
|
+
import { JsonDisplay } from '@open-mercato/ui/backend/JsonDisplay'
|
|
13
19
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
20
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
21
|
+
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
15
22
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
16
|
-
import type
|
|
17
|
-
import { LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
18
|
-
import {
|
|
23
|
+
import { LEGACY_INTEGRATION_DETAIL_TABS_SPOT_ID, type CredentialFieldType, type IntegrationCredentialField } from '@open-mercato/shared/modules/integrations/types'
|
|
24
|
+
import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
25
|
+
import { Bell, ChevronDown, ChevronRight, CreditCard, HardDrive, MessageSquare, RefreshCw, Truck, Webhook, Zap } from 'lucide-react'
|
|
26
|
+
import {
|
|
27
|
+
buildIntegrationDetailInjectedTabs,
|
|
28
|
+
filterIntegrationDetailWidgetsByKind,
|
|
29
|
+
resolveIntegrationDetailWidgetSpotId,
|
|
30
|
+
resolveRequestedIntegrationDetailTab,
|
|
31
|
+
} from '../detail-page-widgets'
|
|
19
32
|
|
|
20
33
|
type CredentialField = IntegrationCredentialField
|
|
34
|
+
type BuiltInIntegrationDetailTab = 'credentials' | 'version' | 'health' | 'logs'
|
|
35
|
+
type IntegrationDetailTab = BuiltInIntegrationDetailTab | string
|
|
21
36
|
|
|
22
37
|
const UNSUPPORTED_CREDENTIAL_FIELD_TYPES = new Set<CredentialFieldType>(['oauth', 'ssh_keypair'])
|
|
23
38
|
|
|
@@ -43,6 +58,9 @@ type IntegrationDetail = {
|
|
|
43
58
|
bundleId?: string
|
|
44
59
|
docsUrl?: string
|
|
45
60
|
apiVersions?: ApiVersion[]
|
|
61
|
+
detailPage?: {
|
|
62
|
+
widgetSpotId?: string
|
|
63
|
+
}
|
|
46
64
|
credentials?: { fields: CredentialField[] }
|
|
47
65
|
}
|
|
48
66
|
bundle?: { id: string; title: string; credentials?: { fields: CredentialField[] } }
|
|
@@ -58,10 +76,27 @@ type IntegrationDetail = {
|
|
|
58
76
|
|
|
59
77
|
type LogEntry = {
|
|
60
78
|
id: string
|
|
79
|
+
runId?: string | null
|
|
80
|
+
scopeEntityType?: string | null
|
|
81
|
+
scopeEntityId?: string | null
|
|
61
82
|
level: 'info' | 'warn' | 'error'
|
|
62
83
|
message: string
|
|
63
84
|
createdAt: string
|
|
64
|
-
code?: string
|
|
85
|
+
code?: string | null
|
|
86
|
+
payload?: Record<string, unknown> | null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type IntegrationDetailPageProps = {
|
|
90
|
+
params?: {
|
|
91
|
+
id?: string | string[]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type HealthCheckResponse = {
|
|
96
|
+
status: 'healthy' | 'degraded' | 'unhealthy'
|
|
97
|
+
message: string | null
|
|
98
|
+
details: Record<string, unknown> | null
|
|
99
|
+
checkedAt: string
|
|
65
100
|
}
|
|
66
101
|
|
|
67
102
|
const LOG_LEVEL_STYLES: Record<string, string> = {
|
|
@@ -76,9 +111,152 @@ const HEALTH_STATUS_STYLES: Record<string, string> = {
|
|
|
76
111
|
unhealthy: 'bg-red-100 text-red-800',
|
|
77
112
|
}
|
|
78
113
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
114
|
+
const CATEGORY_ICONS: Record<string, React.ElementType> = {
|
|
115
|
+
payment: CreditCard,
|
|
116
|
+
shipping: Truck,
|
|
117
|
+
data_sync: RefreshCw,
|
|
118
|
+
communication: MessageSquare,
|
|
119
|
+
notification: Bell,
|
|
120
|
+
storage: HardDrive,
|
|
121
|
+
webhook: Webhook,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resolveRouteId(value: string | string[] | undefined): string | undefined {
|
|
125
|
+
if (Array.isArray(value)) return value[0]
|
|
126
|
+
return value
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolvePathnameId(pathname: string): string | undefined {
|
|
130
|
+
const parts = pathname.split('/').filter(Boolean)
|
|
131
|
+
const integrationId = parts.at(-1)
|
|
132
|
+
if (!integrationId || integrationId === 'integrations' || integrationId === 'bundle') return undefined
|
|
133
|
+
return decodeURIComponent(integrationId)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildCredentialFields(credFields: CredentialField[]): CrudField[] {
|
|
137
|
+
return credFields.map((field) => {
|
|
138
|
+
const shared = {
|
|
139
|
+
id: field.key,
|
|
140
|
+
label: field.label,
|
|
141
|
+
description: field.helpDetails ? (
|
|
142
|
+
<div className="space-y-1">
|
|
143
|
+
{field.helpText ? <div>{field.helpText}</div> : null}
|
|
144
|
+
<WebhookSetupGuide guide={field.helpDetails} buttonLabel="Show details" />
|
|
145
|
+
</div>
|
|
146
|
+
) : field.helpText,
|
|
147
|
+
placeholder: field.placeholder,
|
|
148
|
+
required: field.required,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (field.type === 'secret') {
|
|
152
|
+
return {
|
|
153
|
+
...shared,
|
|
154
|
+
type: 'custom' as const,
|
|
155
|
+
component: ({ id, value, setValue, disabled }) => (
|
|
156
|
+
<Input
|
|
157
|
+
id={id}
|
|
158
|
+
type="password"
|
|
159
|
+
placeholder={field.placeholder}
|
|
160
|
+
value={typeof value === 'string' ? value : ''}
|
|
161
|
+
onChange={(event) => setValue(event.target.value)}
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
/>
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (field.type === 'select' && field.options) {
|
|
169
|
+
return {
|
|
170
|
+
...shared,
|
|
171
|
+
type: 'select' as const,
|
|
172
|
+
options: field.options,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (field.type === 'boolean') {
|
|
177
|
+
return {
|
|
178
|
+
...shared,
|
|
179
|
+
type: 'checkbox' as const,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...shared,
|
|
185
|
+
type: 'text' as const,
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isHealthLog(log: LogEntry): boolean {
|
|
191
|
+
return log.message === 'Health check passed' || log.message.startsWith('Health check:')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function extractHealthDetails(payload: Record<string, unknown> | null | undefined): Record<string, unknown> {
|
|
195
|
+
if (!payload) return {}
|
|
196
|
+
return Object.fromEntries(
|
|
197
|
+
Object.entries(payload).filter(([key, value]) => key !== 'status' && key !== 'message' && value !== undefined && value !== null),
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function formatHealthValue(value: unknown): string {
|
|
202
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
|
203
|
+
if (typeof value === 'string') return value
|
|
204
|
+
if (typeof value === 'number') return String(value)
|
|
205
|
+
if (value instanceof Date) return value.toLocaleString()
|
|
206
|
+
return JSON.stringify(value)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatTypeLabel(value: string): string {
|
|
210
|
+
return value.split('_').filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(' ')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isPrimitiveLogValue(value: unknown): value is string | number | boolean | null {
|
|
214
|
+
return value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function formatLogDetailLabel(key: string): string {
|
|
218
|
+
return key
|
|
219
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
220
|
+
.replace(/[_-]+/g, ' ')
|
|
221
|
+
.split(' ')
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.map((part) => part[0]?.toUpperCase() + part.slice(1))
|
|
224
|
+
.join(' ')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatLogPrimitiveValue(value: string | number | boolean | null): string {
|
|
228
|
+
if (value === null) return 'None'
|
|
229
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No'
|
|
230
|
+
return String(value)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function splitLogPayload(payload: Record<string, unknown> | null | undefined) {
|
|
234
|
+
if (!payload) {
|
|
235
|
+
return {
|
|
236
|
+
inlineEntries: [] as Array<[string, string | number | boolean | null]>,
|
|
237
|
+
nestedEntries: [] as Array<[string, unknown]>,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const inlineEntries: Array<[string, string | number | boolean | null]> = []
|
|
242
|
+
const nestedEntries: Array<[string, unknown]> = []
|
|
243
|
+
|
|
244
|
+
Object.entries(payload).forEach(([key, value]) => {
|
|
245
|
+
if (isPrimitiveLogValue(value)) {
|
|
246
|
+
inlineEntries.push([key, value])
|
|
247
|
+
return
|
|
248
|
+
}
|
|
249
|
+
nestedEntries.push([key, value])
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
return { inlineEntries, nestedEntries }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default function IntegrationDetailPage({ params }: IntegrationDetailPageProps) {
|
|
256
|
+
const pathname = usePathname()
|
|
257
|
+
const router = useRouter()
|
|
258
|
+
const searchParams = useSearchParams()
|
|
259
|
+
const integrationId = resolveRouteId(params?.id) ?? resolvePathnameId(pathname)
|
|
82
260
|
const t = useT()
|
|
83
261
|
|
|
84
262
|
const [detail, setDetail] = React.useState<IntegrationDetail | null>(null)
|
|
@@ -86,46 +264,76 @@ export default function IntegrationDetailPage() {
|
|
|
86
264
|
const [error, setError] = React.useState<string | null>(null)
|
|
87
265
|
|
|
88
266
|
const [credValues, setCredValues] = React.useState<Record<string, unknown>>({})
|
|
89
|
-
const [
|
|
267
|
+
const [credentialsFormKey, setCredentialsFormKey] = React.useState(0)
|
|
268
|
+
const [isSavingCredentials, setIsSavingCredentials] = React.useState(false)
|
|
90
269
|
|
|
91
270
|
const [logs, setLogs] = React.useState<LogEntry[]>([])
|
|
92
271
|
const [logLevel, setLogLevel] = React.useState<string>('')
|
|
93
272
|
const [isLoadingLogs, setIsLoadingLogs] = React.useState(false)
|
|
273
|
+
const [expandedLogId, setExpandedLogId] = React.useState<string | null>(null)
|
|
94
274
|
|
|
95
275
|
const [isCheckingHealth, setIsCheckingHealth] = React.useState(false)
|
|
96
276
|
const [isTogglingState, setIsTogglingState] = React.useState(false)
|
|
277
|
+
const [latestHealthResult, setLatestHealthResult] = React.useState<HealthCheckResponse | null>(null)
|
|
278
|
+
const [activeTab, setActiveTab] = React.useState<IntegrationDetailTab>('credentials')
|
|
97
279
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
280
|
+
const credentialsFormId = React.useId()
|
|
281
|
+
|
|
282
|
+
const resolveCurrentIntegrationId = React.useCallback(() => {
|
|
283
|
+
return integrationId ?? (
|
|
284
|
+
typeof window !== 'undefined'
|
|
285
|
+
? resolvePathnameId(window.location.pathname)
|
|
286
|
+
: undefined
|
|
105
287
|
)
|
|
106
|
-
|
|
107
|
-
|
|
288
|
+
}, [integrationId])
|
|
289
|
+
|
|
290
|
+
const loadDetail = React.useCallback(async () => {
|
|
291
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
292
|
+
if (!currentIntegrationId) {
|
|
108
293
|
setIsLoading(false)
|
|
294
|
+
setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
109
295
|
return
|
|
110
296
|
}
|
|
111
|
-
|
|
112
|
-
setIsLoading(
|
|
113
|
-
|
|
297
|
+
setError(null)
|
|
298
|
+
setIsLoading(true)
|
|
299
|
+
try {
|
|
300
|
+
const call = await apiCall<IntegrationDetail>(
|
|
301
|
+
`/api/integrations/${encodeURIComponent(currentIntegrationId)}`,
|
|
302
|
+
undefined,
|
|
303
|
+
{ fallback: null },
|
|
304
|
+
)
|
|
305
|
+
if (!call.ok || !call.result) {
|
|
306
|
+
setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
307
|
+
setIsLoading(false)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
setDetail(call.result)
|
|
311
|
+
setIsLoading(false)
|
|
312
|
+
} catch {
|
|
313
|
+
setError(t('integrations.detail.loadError', 'Failed to load integration'))
|
|
314
|
+
setIsLoading(false)
|
|
315
|
+
}
|
|
316
|
+
}, [resolveCurrentIntegrationId, t])
|
|
114
317
|
|
|
115
318
|
const loadCredentials = React.useCallback(async () => {
|
|
319
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
320
|
+
if (!currentIntegrationId) return
|
|
116
321
|
const call = await apiCall<{ credentials: Record<string, unknown> }>(
|
|
117
|
-
`/api/integrations/${encodeURIComponent(
|
|
322
|
+
`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`,
|
|
118
323
|
undefined,
|
|
119
324
|
{ fallback: null },
|
|
120
325
|
)
|
|
121
326
|
if (call.ok && call.result?.credentials) {
|
|
122
327
|
setCredValues(call.result.credentials)
|
|
328
|
+
setCredentialsFormKey((current) => current + 1)
|
|
123
329
|
}
|
|
124
|
-
}, [
|
|
330
|
+
}, [resolveCurrentIntegrationId])
|
|
125
331
|
|
|
126
332
|
const loadLogs = React.useCallback(async () => {
|
|
333
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
334
|
+
if (!currentIntegrationId) return
|
|
127
335
|
setIsLoadingLogs(true)
|
|
128
|
-
const params = new URLSearchParams({ integrationId, pageSize: '50' })
|
|
336
|
+
const params = new URLSearchParams({ integrationId: currentIntegrationId, pageSize: '50' })
|
|
129
337
|
if (logLevel) params.set('level', logLevel)
|
|
130
338
|
const call = await apiCall<{ items: LogEntry[] }>(
|
|
131
339
|
`/api/integrations/logs?${params.toString()}`,
|
|
@@ -136,222 +344,562 @@ export default function IntegrationDetailPage() {
|
|
|
136
344
|
setLogs(call.result.items)
|
|
137
345
|
}
|
|
138
346
|
setIsLoadingLogs(false)
|
|
139
|
-
}, [
|
|
347
|
+
}, [logLevel, resolveCurrentIntegrationId])
|
|
348
|
+
|
|
349
|
+
const detailWidgetSpotId = React.useMemo(
|
|
350
|
+
() => resolveIntegrationDetailWidgetSpotId(detail?.integration ?? null, LEGACY_INTEGRATION_DETAIL_TABS_SPOT_ID),
|
|
351
|
+
[detail?.integration],
|
|
352
|
+
)
|
|
353
|
+
const mutationContextId = React.useMemo(
|
|
354
|
+
() => `integrations.detail:${integrationId ?? 'unknown'}`,
|
|
355
|
+
[integrationId],
|
|
356
|
+
)
|
|
357
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
358
|
+
contextId: mutationContextId,
|
|
359
|
+
spotId: detailWidgetSpotId,
|
|
360
|
+
})
|
|
361
|
+
const refreshDetail = React.useCallback(async () => {
|
|
362
|
+
await loadDetail()
|
|
363
|
+
await loadCredentials()
|
|
364
|
+
}, [loadCredentials, loadDetail])
|
|
365
|
+
const refreshLogs = React.useCallback(async () => {
|
|
366
|
+
await loadLogs()
|
|
367
|
+
}, [loadLogs])
|
|
368
|
+
const injectionContext = React.useMemo(
|
|
369
|
+
() => ({
|
|
370
|
+
formId: mutationContextId,
|
|
371
|
+
integrationDetailWidgetSpotId: detailWidgetSpotId,
|
|
372
|
+
resourceKind: 'integrations.integration',
|
|
373
|
+
resourceId: integrationId ?? detail?.integration.id,
|
|
374
|
+
integrationId: integrationId ?? detail?.integration.id,
|
|
375
|
+
integration: detail?.integration ?? null,
|
|
376
|
+
detail,
|
|
377
|
+
state: detail?.state ?? null,
|
|
378
|
+
credentialValues: credValues,
|
|
379
|
+
latestHealthResult,
|
|
380
|
+
activeTab,
|
|
381
|
+
setActiveTab,
|
|
382
|
+
refreshDetail,
|
|
383
|
+
refreshLogs,
|
|
384
|
+
retryLastMutation,
|
|
385
|
+
}),
|
|
386
|
+
[
|
|
387
|
+
activeTab,
|
|
388
|
+
credValues,
|
|
389
|
+
detail,
|
|
390
|
+
detailWidgetSpotId,
|
|
391
|
+
integrationId,
|
|
392
|
+
latestHealthResult,
|
|
393
|
+
mutationContextId,
|
|
394
|
+
refreshDetail,
|
|
395
|
+
refreshLogs,
|
|
396
|
+
retryLastMutation,
|
|
397
|
+
],
|
|
398
|
+
)
|
|
399
|
+
const { widgets: detailWidgets } = useInjectionWidgets(detailWidgetSpotId, {
|
|
400
|
+
context: injectionContext,
|
|
401
|
+
triggerOnLoad: true,
|
|
402
|
+
})
|
|
403
|
+
const stackedDetailWidgets = React.useMemo(
|
|
404
|
+
() => filterIntegrationDetailWidgetsByKind(detailWidgets, 'stack'),
|
|
405
|
+
[detailWidgets],
|
|
406
|
+
)
|
|
407
|
+
const groupedDetailWidgets = React.useMemo(
|
|
408
|
+
() => filterIntegrationDetailWidgetsByKind(detailWidgets, 'group'),
|
|
409
|
+
[detailWidgets],
|
|
410
|
+
)
|
|
411
|
+
const injectedTabs = React.useMemo(
|
|
412
|
+
() => buildIntegrationDetailInjectedTabs(
|
|
413
|
+
detailWidgets,
|
|
414
|
+
(widget) => (
|
|
415
|
+
widget.placement?.groupLabel
|
|
416
|
+
? t(widget.placement.groupLabel, widget.module.metadata.title ?? widget.widgetId)
|
|
417
|
+
: (widget.module.metadata.title ?? widget.widgetId)
|
|
418
|
+
),
|
|
419
|
+
),
|
|
420
|
+
[detailWidgets, t],
|
|
421
|
+
)
|
|
422
|
+
const customTabIds = React.useMemo(
|
|
423
|
+
() => injectedTabs.map((tab) => tab.id),
|
|
424
|
+
[injectedTabs],
|
|
425
|
+
)
|
|
426
|
+
const runMutationWithContext = React.useCallback(
|
|
427
|
+
async <T,>({
|
|
428
|
+
operation,
|
|
429
|
+
mutationPayload,
|
|
430
|
+
actionId,
|
|
431
|
+
tabId,
|
|
432
|
+
operationType = 'update',
|
|
433
|
+
}: {
|
|
434
|
+
operation: () => Promise<T>
|
|
435
|
+
mutationPayload?: Record<string, unknown>
|
|
436
|
+
actionId: string
|
|
437
|
+
tabId?: string
|
|
438
|
+
operationType?: 'create' | 'update' | 'delete'
|
|
439
|
+
}): Promise<T> => {
|
|
440
|
+
return runMutation({
|
|
441
|
+
operation,
|
|
442
|
+
mutationPayload,
|
|
443
|
+
context: {
|
|
444
|
+
...injectionContext,
|
|
445
|
+
operation: operationType,
|
|
446
|
+
actionId,
|
|
447
|
+
activeTab: tabId ?? activeTab,
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
},
|
|
451
|
+
[activeTab, injectionContext, runMutation],
|
|
452
|
+
)
|
|
140
453
|
|
|
141
454
|
React.useEffect(() => { void loadDetail() }, [loadDetail])
|
|
142
455
|
React.useEffect(() => { void loadCredentials() }, [loadCredentials])
|
|
143
456
|
React.useEffect(() => { void loadLogs() }, [loadLogs])
|
|
457
|
+
React.useEffect(() => {
|
|
458
|
+
setExpandedLogId((current) => (current && logs.some((log) => log.id === current) ? current : null))
|
|
459
|
+
}, [logs])
|
|
144
460
|
|
|
145
461
|
const handleToggleState = React.useCallback(async (enabled: boolean) => {
|
|
462
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
463
|
+
if (!currentIntegrationId) return
|
|
146
464
|
setIsTogglingState(true)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
465
|
+
try {
|
|
466
|
+
const call = await runMutationWithContext({
|
|
467
|
+
actionId: 'toggle-state',
|
|
468
|
+
mutationPayload: { integrationId: currentIntegrationId, isEnabled: enabled },
|
|
469
|
+
operation: () => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/state`, {
|
|
470
|
+
method: 'PUT',
|
|
471
|
+
headers: { 'Content-Type': 'application/json' },
|
|
472
|
+
body: JSON.stringify({ isEnabled: enabled }),
|
|
473
|
+
}, { fallback: null }),
|
|
474
|
+
})
|
|
475
|
+
if (call.ok) {
|
|
476
|
+
setDetail((prev) => prev ? { ...prev, state: { ...prev.state, isEnabled: enabled } } : prev)
|
|
477
|
+
flash(t('integrations.detail.stateUpdated'), 'success')
|
|
478
|
+
} else {
|
|
479
|
+
flash(t('integrations.detail.stateError'), 'error')
|
|
480
|
+
}
|
|
481
|
+
} catch {
|
|
156
482
|
flash(t('integrations.detail.stateError'), 'error')
|
|
483
|
+
} finally {
|
|
484
|
+
setIsTogglingState(false)
|
|
157
485
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
486
|
+
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
487
|
+
|
|
488
|
+
const handleSaveCredentials = React.useCallback(async (values: Record<string, unknown>) => {
|
|
489
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
490
|
+
if (!currentIntegrationId) return
|
|
491
|
+
setIsSavingCredentials(true)
|
|
492
|
+
try {
|
|
493
|
+
const call = await runMutationWithContext({
|
|
494
|
+
actionId: 'save-credentials',
|
|
495
|
+
tabId: 'credentials',
|
|
496
|
+
mutationPayload: { integrationId: currentIntegrationId, credentials: values },
|
|
497
|
+
operation: () => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
|
|
498
|
+
method: 'PUT',
|
|
499
|
+
headers: { 'Content-Type': 'application/json' },
|
|
500
|
+
body: JSON.stringify({ credentials: values }),
|
|
501
|
+
}, { fallback: null }),
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
if (call.ok) {
|
|
505
|
+
setCredValues(values)
|
|
506
|
+
setCredentialsFormKey((current) => current + 1)
|
|
507
|
+
flash(t('integrations.detail.credentials.saved'), 'success')
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const result = call.result as {
|
|
512
|
+
error?: string
|
|
513
|
+
details?: { fieldErrors?: Record<string, string>; formErrors?: string[] }
|
|
514
|
+
} | null
|
|
515
|
+
throw createCrudFormError(
|
|
516
|
+
result?.error ?? t('integrations.detail.credentials.saveError', 'Failed to save credentials'),
|
|
517
|
+
result?.details?.fieldErrors,
|
|
518
|
+
{ details: result?.details },
|
|
519
|
+
)
|
|
520
|
+
} finally {
|
|
521
|
+
setIsSavingCredentials(false)
|
|
172
522
|
}
|
|
173
|
-
|
|
174
|
-
}, [integrationId, credValues, t])
|
|
523
|
+
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
175
524
|
|
|
176
525
|
const handleVersionChange = React.useCallback(async (version: string) => {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
526
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
527
|
+
if (!currentIntegrationId) return
|
|
528
|
+
try {
|
|
529
|
+
const call = await runMutationWithContext({
|
|
530
|
+
actionId: 'change-version',
|
|
531
|
+
tabId: 'version',
|
|
532
|
+
mutationPayload: { integrationId: currentIntegrationId, apiVersion: version },
|
|
533
|
+
operation: () => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/version`, {
|
|
534
|
+
method: 'PUT',
|
|
535
|
+
headers: { 'Content-Type': 'application/json' },
|
|
536
|
+
body: JSON.stringify({ apiVersion: version }),
|
|
537
|
+
}, { fallback: null }),
|
|
538
|
+
})
|
|
539
|
+
if (call.ok) {
|
|
540
|
+
setDetail((prev) => prev ? { ...prev, state: { ...prev.state, apiVersion: version } } : prev)
|
|
541
|
+
flash(t('integrations.detail.version.saved'), 'success')
|
|
542
|
+
} else {
|
|
543
|
+
flash(t('integrations.detail.version.saveError'), 'error')
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
186
546
|
flash(t('integrations.detail.version.saveError'), 'error')
|
|
187
547
|
}
|
|
188
|
-
}, [
|
|
548
|
+
}, [resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
189
549
|
|
|
190
550
|
const handleHealthCheck = React.useCallback(async () => {
|
|
551
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
552
|
+
if (!currentIntegrationId) return
|
|
191
553
|
setIsCheckingHealth(true)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
554
|
+
try {
|
|
555
|
+
const call = await runMutationWithContext({
|
|
556
|
+
actionId: 'run-health-check',
|
|
557
|
+
tabId: 'health',
|
|
558
|
+
mutationPayload: { integrationId: currentIntegrationId },
|
|
559
|
+
operation: () => apiCall<HealthCheckResponse>(
|
|
560
|
+
`/api/integrations/${encodeURIComponent(currentIntegrationId)}/health`,
|
|
561
|
+
{ method: 'POST' },
|
|
562
|
+
{ fallback: null },
|
|
563
|
+
),
|
|
564
|
+
})
|
|
565
|
+
const result = call.result
|
|
566
|
+
if (call.ok && result) {
|
|
567
|
+
setLatestHealthResult(result)
|
|
568
|
+
setDetail((prev) => prev ? {
|
|
569
|
+
...prev,
|
|
570
|
+
state: {
|
|
571
|
+
...prev.state,
|
|
572
|
+
lastHealthStatus: result.status,
|
|
573
|
+
lastHealthCheckedAt: result.checkedAt,
|
|
574
|
+
},
|
|
575
|
+
} : prev)
|
|
576
|
+
void refreshLogs()
|
|
577
|
+
} else {
|
|
578
|
+
flash(t('integrations.detail.health.checkError'), 'error')
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
207
581
|
flash(t('integrations.detail.health.checkError'), 'error')
|
|
582
|
+
} finally {
|
|
583
|
+
setIsCheckingHealth(false)
|
|
208
584
|
}
|
|
209
|
-
|
|
210
|
-
|
|
585
|
+
}, [refreshLogs, resolveCurrentIntegrationId, runMutationWithContext, t])
|
|
586
|
+
|
|
587
|
+
const hasVersions = Boolean(detail?.integration.apiVersions?.length)
|
|
588
|
+
const integration = detail?.integration ?? null
|
|
589
|
+
const state = detail?.state ?? null
|
|
590
|
+
const editableCredentialFields = React.useMemo(
|
|
591
|
+
() => (detail?.integration.credentials?.fields ?? detail?.bundle?.credentials?.fields ?? []).filter(isEditableCredentialField),
|
|
592
|
+
[detail?.bundle?.credentials?.fields, detail?.integration.credentials?.fields],
|
|
593
|
+
)
|
|
594
|
+
const credentialFormFields = React.useMemo(
|
|
595
|
+
() => buildCredentialFields(editableCredentialFields),
|
|
596
|
+
[editableCredentialFields],
|
|
597
|
+
)
|
|
598
|
+
const credentialSchema = React.useMemo(() => (
|
|
599
|
+
z.object({}).passthrough().superRefine((rawValues, ctx) => {
|
|
600
|
+
const values = rawValues as Record<string, unknown>
|
|
601
|
+
|
|
602
|
+
editableCredentialFields.forEach((field) => {
|
|
603
|
+
const value = values[field.key]
|
|
604
|
+
|
|
605
|
+
if (field.type === 'boolean') {
|
|
606
|
+
if (value !== undefined && value !== null && typeof value !== 'boolean') {
|
|
607
|
+
ctx.addIssue({
|
|
608
|
+
code: z.ZodIssueCode.custom,
|
|
609
|
+
path: [field.key],
|
|
610
|
+
message: t('integrations.detail.credentials.validation.boolean', 'Select a valid value.'),
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
if (field.required && typeof value !== 'boolean') {
|
|
614
|
+
ctx.addIssue({
|
|
615
|
+
code: z.ZodIssueCode.custom,
|
|
616
|
+
path: [field.key],
|
|
617
|
+
message: t('integrations.detail.credentials.validation.required', '{field} is required.', { field: field.label }),
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
return
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (value !== undefined && value !== null && typeof value !== 'string') {
|
|
624
|
+
ctx.addIssue({
|
|
625
|
+
code: z.ZodIssueCode.custom,
|
|
626
|
+
path: [field.key],
|
|
627
|
+
message: t('integrations.detail.credentials.validation.text', 'Enter a valid value.'),
|
|
628
|
+
})
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const normalizedValue = typeof value === 'string' ? value : ''
|
|
633
|
+
|
|
634
|
+
if (field.required && normalizedValue.trim().length === 0) {
|
|
635
|
+
ctx.addIssue({
|
|
636
|
+
code: z.ZodIssueCode.custom,
|
|
637
|
+
path: [field.key],
|
|
638
|
+
message: t('integrations.detail.credentials.validation.required', '{field} is required.', { field: field.label }),
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (normalizedValue.length > 20_000) {
|
|
643
|
+
ctx.addIssue({
|
|
644
|
+
code: z.ZodIssueCode.custom,
|
|
645
|
+
path: [field.key],
|
|
646
|
+
message: t('integrations.detail.credentials.validation.tooLong', 'Value is too long.'),
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (
|
|
651
|
+
field.type === 'select'
|
|
652
|
+
&& normalizedValue
|
|
653
|
+
&& field.options
|
|
654
|
+
&& !field.options.some((option) => option.value === normalizedValue)
|
|
655
|
+
) {
|
|
656
|
+
ctx.addIssue({
|
|
657
|
+
code: z.ZodIssueCode.custom,
|
|
658
|
+
path: [field.key],
|
|
659
|
+
message: t('integrations.detail.credentials.validation.option', 'Select one of the available options.'),
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
) as z.ZodType<Record<string, unknown>>, [editableCredentialFields, t])
|
|
665
|
+
const latestHealthLog = React.useMemo(() => logs.find(isHealthLog) ?? null, [logs])
|
|
666
|
+
const healthMessage =
|
|
667
|
+
latestHealthResult?.message ??
|
|
668
|
+
(typeof latestHealthLog?.payload?.message === 'string' ? latestHealthLog.payload.message : null)
|
|
669
|
+
const healthDetailsSource = latestHealthResult?.details ?? extractHealthDetails(latestHealthLog?.payload)
|
|
670
|
+
const healthDetails = latestHealthLog?.code
|
|
671
|
+
? { ...healthDetailsSource, code: latestHealthLog.code }
|
|
672
|
+
: healthDetailsSource
|
|
673
|
+
const healthDetailEntries = Object.entries(healthDetails)
|
|
674
|
+
const healthStatusDescription = state?.lastHealthStatus
|
|
675
|
+
? t(
|
|
676
|
+
`integrations.detail.health.meaning.${state.lastHealthStatus}`,
|
|
677
|
+
state.lastHealthStatus === 'healthy'
|
|
678
|
+
? 'The provider responded successfully using the current credentials.'
|
|
679
|
+
: state.lastHealthStatus === 'degraded'
|
|
680
|
+
? 'The provider responded, but reported warnings or limited functionality.'
|
|
681
|
+
: integration?.id === 'gateway_stripe'
|
|
682
|
+
? 'Stripe rejected the last check. This usually means the secret key is invalid, missing required permissions, revoked, or Stripe was temporarily unavailable.'
|
|
683
|
+
: 'The last check failed. This usually means invalid credentials, missing permissions, or a provider outage.',
|
|
684
|
+
)
|
|
685
|
+
: null
|
|
686
|
+
|
|
687
|
+
React.useEffect(() => {
|
|
688
|
+
setActiveTab(resolveRequestedIntegrationDetailTab(searchParams?.get('tab'), hasVersions, customTabIds))
|
|
689
|
+
}, [customTabIds, hasVersions, searchParams])
|
|
690
|
+
|
|
691
|
+
const handleTabChange = React.useCallback((nextValue: string) => {
|
|
692
|
+
const currentIntegrationId = resolveCurrentIntegrationId()
|
|
693
|
+
const nextTab = resolveRequestedIntegrationDetailTab(nextValue, hasVersions, customTabIds)
|
|
694
|
+
setActiveTab(nextTab)
|
|
695
|
+
if (!currentIntegrationId) return
|
|
696
|
+
const basePath = `/backend/integrations/${encodeURIComponent(currentIntegrationId)}`
|
|
697
|
+
router.replace(nextTab === 'credentials' ? basePath : `${basePath}?tab=${encodeURIComponent(nextTab)}`)
|
|
698
|
+
}, [customTabIds, hasVersions, resolveCurrentIntegrationId, router])
|
|
211
699
|
|
|
212
700
|
if (isLoading) return <Page><PageBody><LoadingMessage label={t('integrations.detail.title')} /></PageBody></Page>
|
|
213
701
|
if (error || !detail) return <Page><PageBody><ErrorMessage label={error ?? t('integrations.detail.loadError')} /></PageBody></Page>
|
|
214
702
|
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const
|
|
703
|
+
const resolvedIntegration = detail.integration
|
|
704
|
+
const resolvedState = detail.state
|
|
705
|
+
const CategoryIcon = resolvedIntegration.category ? CATEGORY_ICONS[resolvedIntegration.category] : null
|
|
706
|
+
|
|
707
|
+
const showCredentialActions = activeTab === 'credentials' && credentialFormFields.length > 0
|
|
218
708
|
|
|
219
709
|
return (
|
|
220
710
|
<Page>
|
|
221
711
|
<PageBody className="space-y-6">
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
712
|
+
<FormHeader
|
|
713
|
+
backHref="/backend/integrations"
|
|
714
|
+
title={resolvedIntegration.title}
|
|
715
|
+
actions={{
|
|
716
|
+
cancelHref: showCredentialActions ? '/backend/integrations' : undefined,
|
|
717
|
+
submit: showCredentialActions
|
|
718
|
+
? {
|
|
719
|
+
formId: credentialsFormId,
|
|
720
|
+
pending: isSavingCredentials,
|
|
721
|
+
label: t('integrations.detail.credentials.save', 'Save credentials'),
|
|
722
|
+
pendingLabel: t('ui.forms.status.saving', 'Saving...'),
|
|
723
|
+
}
|
|
724
|
+
: undefined,
|
|
725
|
+
}}
|
|
726
|
+
/>
|
|
727
|
+
|
|
728
|
+
<div className="space-y-2">
|
|
729
|
+
{resolvedIntegration.description ? (
|
|
730
|
+
<p className="text-sm text-muted-foreground">{resolvedIntegration.description}</p>
|
|
731
|
+
) : null}
|
|
732
|
+
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
|
|
733
|
+
{resolvedIntegration.category ? (
|
|
734
|
+
<div className="flex items-center gap-2">
|
|
735
|
+
{CategoryIcon ? <CategoryIcon className="h-4 w-4" /> : null}
|
|
736
|
+
<span>{formatTypeLabel(resolvedIntegration.category)}</span>
|
|
737
|
+
</div>
|
|
738
|
+
) : null}
|
|
739
|
+
{resolvedIntegration.hub ? (
|
|
740
|
+
<div className="flex items-center gap-2">
|
|
741
|
+
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground/80">
|
|
742
|
+
{t('integrations.detail.hub.label', 'Hub')}
|
|
743
|
+
</span>
|
|
744
|
+
<span>{formatTypeLabel(resolvedIntegration.hub)}</span>
|
|
745
|
+
</div>
|
|
746
|
+
) : null}
|
|
747
|
+
</div>
|
|
226
748
|
</div>
|
|
227
749
|
|
|
228
|
-
<
|
|
229
|
-
<div>
|
|
230
|
-
<
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
750
|
+
<section className="rounded-lg border bg-card p-4">
|
|
751
|
+
<div className="flex items-center justify-between gap-4">
|
|
752
|
+
<div className="space-y-1">
|
|
753
|
+
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">
|
|
754
|
+
{t('integrations.detail.state.label', 'State')}
|
|
755
|
+
</p>
|
|
756
|
+
<p className="text-sm font-medium">
|
|
757
|
+
{resolvedState.isEnabled
|
|
758
|
+
? t('integrations.detail.state.enabled', 'Enabled')
|
|
759
|
+
: t('integrations.detail.state.disabled', 'Disabled')}
|
|
760
|
+
</p>
|
|
237
761
|
</div>
|
|
238
|
-
</div>
|
|
239
|
-
<div className="flex items-center gap-3">
|
|
240
|
-
<span className="text-sm text-muted-foreground">
|
|
241
|
-
{state.isEnabled ? t('integrations.detail.enable') : t('integrations.detail.disable')}
|
|
242
|
-
</span>
|
|
243
762
|
<Switch
|
|
244
|
-
checked={
|
|
763
|
+
checked={resolvedState.isEnabled}
|
|
245
764
|
disabled={isTogglingState}
|
|
246
765
|
onCheckedChange={(checked) => void handleToggleState(checked)}
|
|
247
766
|
/>
|
|
248
767
|
</div>
|
|
249
|
-
</
|
|
768
|
+
</section>
|
|
769
|
+
|
|
770
|
+
{stackedDetailWidgets.length > 0 ? (
|
|
771
|
+
<section className="space-y-4">
|
|
772
|
+
<InjectionSpot
|
|
773
|
+
spotId={detailWidgetSpotId}
|
|
774
|
+
context={injectionContext}
|
|
775
|
+
data={detail}
|
|
776
|
+
onDataChange={(next) => setDetail(next as IntegrationDetail)}
|
|
777
|
+
widgetsOverride={stackedDetailWidgets}
|
|
778
|
+
/>
|
|
779
|
+
</section>
|
|
780
|
+
) : null}
|
|
781
|
+
|
|
782
|
+
{groupedDetailWidgets.length > 0 ? (
|
|
783
|
+
<section className="grid gap-4 lg:grid-cols-2">
|
|
784
|
+
{groupedDetailWidgets.map((widget) => (
|
|
785
|
+
<Card
|
|
786
|
+
key={widget.widgetId}
|
|
787
|
+
className={widget.placement?.column === 2 ? 'lg:col-start-2' : undefined}
|
|
788
|
+
>
|
|
789
|
+
<CardHeader>
|
|
790
|
+
<CardTitle>
|
|
791
|
+
{widget.placement?.groupLabel
|
|
792
|
+
? t(widget.placement.groupLabel, widget.module.metadata.title)
|
|
793
|
+
: widget.module.metadata.title}
|
|
794
|
+
</CardTitle>
|
|
795
|
+
{widget.placement?.groupDescription ? (
|
|
796
|
+
<p className="text-sm text-muted-foreground">
|
|
797
|
+
{widget.placement.groupDescription}
|
|
798
|
+
</p>
|
|
799
|
+
) : null}
|
|
800
|
+
</CardHeader>
|
|
801
|
+
<CardContent>
|
|
802
|
+
<widget.module.Widget
|
|
803
|
+
context={injectionContext}
|
|
804
|
+
data={detail}
|
|
805
|
+
onDataChange={(next) => setDetail(next as IntegrationDetail)}
|
|
806
|
+
/>
|
|
807
|
+
</CardContent>
|
|
808
|
+
</Card>
|
|
809
|
+
))}
|
|
810
|
+
</section>
|
|
811
|
+
) : null}
|
|
250
812
|
|
|
251
|
-
<Tabs
|
|
813
|
+
<Tabs value={activeTab} onValueChange={handleTabChange} className="space-y-4">
|
|
252
814
|
<TabsList>
|
|
253
815
|
<TabsTrigger value="credentials">{t('integrations.detail.tabs.credentials')}</TabsTrigger>
|
|
254
|
-
{hasVersions
|
|
816
|
+
{hasVersions ? <TabsTrigger value="version">{t('integrations.detail.tabs.version')}</TabsTrigger> : null}
|
|
255
817
|
<TabsTrigger value="health">{t('integrations.detail.tabs.health')}</TabsTrigger>
|
|
256
818
|
<TabsTrigger value="logs">{t('integrations.detail.tabs.logs')}</TabsTrigger>
|
|
819
|
+
{injectedTabs.map((tab) => (
|
|
820
|
+
<TabsTrigger key={tab.id} value={tab.id}>{tab.label}</TabsTrigger>
|
|
821
|
+
))}
|
|
257
822
|
</TabsList>
|
|
258
823
|
|
|
259
|
-
<TabsContent value="credentials" className="
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
))}
|
|
285
|
-
</select>
|
|
286
|
-
) : field.type === 'boolean' ? (
|
|
287
|
-
<Switch
|
|
288
|
-
checked={Boolean(credValues[field.key])}
|
|
289
|
-
onCheckedChange={(checked) => setCredValues((prev) => ({ ...prev, [field.key]: checked }))}
|
|
290
|
-
/>
|
|
291
|
-
) : (
|
|
292
|
-
<Input
|
|
293
|
-
type={field.type === 'secret' ? 'password' : 'text'}
|
|
294
|
-
placeholder={field.placeholder}
|
|
295
|
-
value={(credValues[field.key] as string) ?? ''}
|
|
296
|
-
onChange={(e) => setCredValues((prev) => ({ ...prev, [field.key]: e.target.value }))}
|
|
297
|
-
/>
|
|
298
|
-
)}
|
|
299
|
-
</div>
|
|
300
|
-
))}
|
|
301
|
-
<Button type="button" onClick={() => void handleSaveCredentials()} disabled={isSavingCreds}>
|
|
302
|
-
{isSavingCreds ? <Spinner className="mr-2 h-4 w-4" /> : null}
|
|
303
|
-
{t('integrations.detail.credentials.save')}
|
|
304
|
-
</Button>
|
|
305
|
-
</CardContent>
|
|
306
|
-
</Card>
|
|
307
|
-
)}
|
|
824
|
+
<TabsContent value="credentials" className="mt-0">
|
|
825
|
+
<section className="space-y-4 rounded-lg border bg-card p-6">
|
|
826
|
+
{detail.bundle ? (
|
|
827
|
+
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 text-sm text-blue-800">
|
|
828
|
+
{t('integrations.detail.credentials.bundleShared', { bundle: detail.bundle.title })}
|
|
829
|
+
</div>
|
|
830
|
+
) : null}
|
|
831
|
+
{credentialFormFields.length === 0 ? (
|
|
832
|
+
<p className="text-sm text-muted-foreground">
|
|
833
|
+
{t('integrations.detail.credentials.notConfigured')}
|
|
834
|
+
</p>
|
|
835
|
+
) : (
|
|
836
|
+
<CrudForm<Record<string, unknown>>
|
|
837
|
+
key={`${resolvedIntegration.id}:${credentialsFormKey}`}
|
|
838
|
+
formId={credentialsFormId}
|
|
839
|
+
entityId="integrations.integration"
|
|
840
|
+
schema={credentialSchema}
|
|
841
|
+
fields={credentialFormFields}
|
|
842
|
+
initialValues={credValues}
|
|
843
|
+
onSubmit={handleSaveCredentials}
|
|
844
|
+
embedded
|
|
845
|
+
hideFooterActions
|
|
846
|
+
/>
|
|
847
|
+
)}
|
|
848
|
+
</section>
|
|
308
849
|
</TabsContent>
|
|
309
850
|
|
|
310
|
-
{hasVersions
|
|
311
|
-
<TabsContent value="version" className="space-y-4
|
|
851
|
+
{hasVersions ? (
|
|
852
|
+
<TabsContent value="version" className="mt-0 space-y-4">
|
|
312
853
|
<Card>
|
|
313
854
|
<CardHeader>
|
|
314
855
|
<CardTitle>{t('integrations.detail.version.select')}</CardTitle>
|
|
315
856
|
</CardHeader>
|
|
316
857
|
<CardContent className="space-y-3">
|
|
317
|
-
{
|
|
318
|
-
const
|
|
858
|
+
{resolvedIntegration.apiVersions?.map((version) => {
|
|
859
|
+
const stableVersion = resolvedIntegration.apiVersions?.find((item) => item.status === 'stable')?.id
|
|
860
|
+
const isSelected = (resolvedState.apiVersion ?? stableVersion) === version.id
|
|
319
861
|
return (
|
|
320
862
|
<div
|
|
321
|
-
key={
|
|
322
|
-
className={`flex items-center justify-between rounded-lg border p-3
|
|
323
|
-
onClick={() => void handleVersionChange(
|
|
863
|
+
key={version.id}
|
|
864
|
+
className={`flex cursor-pointer items-center justify-between rounded-lg border p-3 transition-colors ${isSelected ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'}`}
|
|
865
|
+
onClick={() => void handleVersionChange(version.id)}
|
|
324
866
|
>
|
|
325
867
|
<div>
|
|
326
|
-
<span className="font-medium
|
|
868
|
+
<span className="text-sm font-medium">{version.label ?? version.id}</span>
|
|
327
869
|
<Badge
|
|
328
|
-
variant={
|
|
870
|
+
variant={version.status === 'stable' ? 'default' : version.status === 'deprecated' ? 'destructive' : 'secondary'}
|
|
329
871
|
className="ml-2"
|
|
330
872
|
>
|
|
331
|
-
{t(`integrations.detail.version.${
|
|
873
|
+
{t(`integrations.detail.version.${version.status}`)}
|
|
332
874
|
</Badge>
|
|
333
|
-
{
|
|
334
|
-
<span className="text-xs text-muted-foreground
|
|
335
|
-
{t('integrations.detail.version.sunsetAt', { date: new Date(
|
|
875
|
+
{version.status === 'deprecated' && version.sunsetAt ? (
|
|
876
|
+
<span className="ml-2 text-xs text-muted-foreground">
|
|
877
|
+
{t('integrations.detail.version.sunsetAt', { date: new Date(version.sunsetAt).toLocaleDateString() })}
|
|
336
878
|
</span>
|
|
337
|
-
)}
|
|
879
|
+
) : null}
|
|
338
880
|
</div>
|
|
339
|
-
{isSelected
|
|
881
|
+
{isSelected ? <Badge variant="outline">{t('integrations.detail.version.current')}</Badge> : null}
|
|
340
882
|
</div>
|
|
341
883
|
)
|
|
342
884
|
})}
|
|
343
885
|
</CardContent>
|
|
344
886
|
</Card>
|
|
345
887
|
</TabsContent>
|
|
346
|
-
)}
|
|
888
|
+
) : null}
|
|
347
889
|
|
|
348
|
-
<TabsContent value="health" className="space-y-4
|
|
890
|
+
<TabsContent value="health" className="mt-0 space-y-4">
|
|
349
891
|
<Card>
|
|
350
892
|
<CardHeader>
|
|
351
893
|
<div className="flex items-center justify-between">
|
|
352
894
|
<CardTitle>{t('integrations.detail.health.title')}</CardTitle>
|
|
353
|
-
<Button
|
|
354
|
-
|
|
895
|
+
<Button
|
|
896
|
+
type="button"
|
|
897
|
+
variant="outline"
|
|
898
|
+
size="sm"
|
|
899
|
+
onClick={() => void handleHealthCheck()}
|
|
900
|
+
disabled={isCheckingHealth}
|
|
901
|
+
>
|
|
902
|
+
{isCheckingHealth ? <Spinner className="mr-2 h-4 w-4" /> : <Zap className="mr-2 h-4 w-4" />}
|
|
355
903
|
{isCheckingHealth ? t('integrations.detail.health.checking') : t('integrations.detail.health.check')}
|
|
356
904
|
</Button>
|
|
357
905
|
</div>
|
|
@@ -359,17 +907,45 @@ export default function IntegrationDetailPage() {
|
|
|
359
907
|
<CardContent className="space-y-3">
|
|
360
908
|
<div className="flex items-center gap-3">
|
|
361
909
|
<span className="text-sm font-medium">{t('integrations.detail.health.title')}:</span>
|
|
362
|
-
{
|
|
363
|
-
<Badge className={HEALTH_STATUS_STYLES[
|
|
364
|
-
{t(`integrations.detail.health.${
|
|
910
|
+
{resolvedState.lastHealthStatus ? (
|
|
911
|
+
<Badge className={HEALTH_STATUS_STYLES[resolvedState.lastHealthStatus] ?? ''}>
|
|
912
|
+
{t(`integrations.detail.health.${resolvedState.lastHealthStatus}`)}
|
|
365
913
|
</Badge>
|
|
366
914
|
) : (
|
|
367
|
-
<span className="text-sm text-muted-foreground">
|
|
915
|
+
<span className="text-sm text-muted-foreground">
|
|
916
|
+
{t('integrations.detail.health.unknown')}
|
|
917
|
+
</span>
|
|
368
918
|
)}
|
|
369
919
|
</div>
|
|
920
|
+
{healthStatusDescription ? (
|
|
921
|
+
<p className="text-sm text-muted-foreground">{healthStatusDescription}</p>
|
|
922
|
+
) : null}
|
|
923
|
+
{healthMessage ? (
|
|
924
|
+
<div className="rounded-lg border bg-muted/30 p-3">
|
|
925
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
926
|
+
{t('integrations.detail.health.lastResult', 'Last result')}
|
|
927
|
+
</p>
|
|
928
|
+
<p className="mt-1 text-sm">{healthMessage}</p>
|
|
929
|
+
</div>
|
|
930
|
+
) : null}
|
|
931
|
+
{healthDetailEntries.length > 0 ? (
|
|
932
|
+
<div className="rounded-lg border p-3">
|
|
933
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
934
|
+
{t('integrations.detail.health.details', 'Details')}
|
|
935
|
+
</p>
|
|
936
|
+
<dl className="mt-3 grid gap-3 sm:grid-cols-2">
|
|
937
|
+
{healthDetailEntries.map(([key, value]) => (
|
|
938
|
+
<div key={key}>
|
|
939
|
+
<dt className="text-xs font-medium text-muted-foreground">{key}</dt>
|
|
940
|
+
<dd className="mt-1 text-sm">{formatHealthValue(value)}</dd>
|
|
941
|
+
</div>
|
|
942
|
+
))}
|
|
943
|
+
</dl>
|
|
944
|
+
</div>
|
|
945
|
+
) : null}
|
|
370
946
|
<p className="text-xs text-muted-foreground">
|
|
371
|
-
{
|
|
372
|
-
? t('integrations.detail.health.lastChecked', { date: new Date(
|
|
947
|
+
{resolvedState.lastHealthCheckedAt
|
|
948
|
+
? t('integrations.detail.health.lastChecked', { date: new Date(resolvedState.lastHealthCheckedAt).toLocaleString() })
|
|
373
949
|
: t('integrations.detail.health.neverChecked')
|
|
374
950
|
}
|
|
375
951
|
</p>
|
|
@@ -377,23 +953,26 @@ export default function IntegrationDetailPage() {
|
|
|
377
953
|
</Card>
|
|
378
954
|
</TabsContent>
|
|
379
955
|
|
|
380
|
-
<TabsContent value="logs" className="space-y-4
|
|
956
|
+
<TabsContent value="logs" className="mt-0 space-y-4">
|
|
381
957
|
<div className="flex items-center gap-3">
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
958
|
+
<div className="relative inline-flex">
|
|
959
|
+
<select
|
|
960
|
+
className="h-11 min-w-40 appearance-none rounded-xl border border-border bg-card pl-4 pr-11 text-sm font-medium text-foreground shadow-sm transition-colors focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
961
|
+
value={logLevel}
|
|
962
|
+
onChange={(event) => setLogLevel(event.target.value)}
|
|
963
|
+
>
|
|
964
|
+
<option value="">{t('integrations.detail.logs.level.all')}</option>
|
|
965
|
+
<option value="info">{t('integrations.detail.logs.level.info')}</option>
|
|
966
|
+
<option value="warn">{t('integrations.detail.logs.level.warn')}</option>
|
|
967
|
+
<option value="error">{t('integrations.detail.logs.level.error')}</option>
|
|
968
|
+
</select>
|
|
969
|
+
<ChevronDown className="pointer-events-none absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
970
|
+
</div>
|
|
392
971
|
</div>
|
|
393
972
|
{isLoadingLogs ? (
|
|
394
973
|
<div className="flex justify-center py-8"><Spinner /></div>
|
|
395
974
|
) : logs.length === 0 ? (
|
|
396
|
-
<p className="
|
|
975
|
+
<p className="py-4 text-sm text-muted-foreground">{t('integrations.detail.logs.empty')}</p>
|
|
397
976
|
) : (
|
|
398
977
|
<div className="rounded-lg border">
|
|
399
978
|
<table className="w-full text-sm">
|
|
@@ -405,24 +984,145 @@ export default function IntegrationDetailPage() {
|
|
|
405
984
|
</tr>
|
|
406
985
|
</thead>
|
|
407
986
|
<tbody>
|
|
408
|
-
{logs.map((log) =>
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
987
|
+
{logs.map((log) => {
|
|
988
|
+
const isExpanded = expandedLogId === log.id
|
|
989
|
+
const metadataEntries = [
|
|
990
|
+
['Time', new Date(log.createdAt).toLocaleString()],
|
|
991
|
+
['Level', log.level],
|
|
992
|
+
['Code', log.code ?? null],
|
|
993
|
+
['Run ID', log.runId ?? null],
|
|
994
|
+
['Entity Type', log.scopeEntityType ?? null],
|
|
995
|
+
['Entity ID', log.scopeEntityId ?? null],
|
|
996
|
+
].filter((entry): entry is [string, string] => typeof entry[1] === 'string' && entry[1].trim().length > 0)
|
|
997
|
+
const { inlineEntries, nestedEntries } = splitLogPayload(log.payload)
|
|
998
|
+
|
|
999
|
+
return (
|
|
1000
|
+
<React.Fragment key={log.id}>
|
|
1001
|
+
<tr className="border-b last:border-0">
|
|
1002
|
+
<td className="whitespace-nowrap px-4 py-2 text-muted-foreground">
|
|
1003
|
+
{new Date(log.createdAt).toLocaleString()}
|
|
1004
|
+
</td>
|
|
1005
|
+
<td className="px-4 py-2">
|
|
1006
|
+
<Badge variant="secondary" className={LOG_LEVEL_STYLES[log.level] ?? ''}>
|
|
1007
|
+
{log.level}
|
|
1008
|
+
</Badge>
|
|
1009
|
+
</td>
|
|
1010
|
+
<td className="px-4 py-2">
|
|
1011
|
+
<Button
|
|
1012
|
+
type="button"
|
|
1013
|
+
variant="ghost"
|
|
1014
|
+
size="sm"
|
|
1015
|
+
className="h-auto w-full justify-start gap-2 px-0 py-0 text-left hover:bg-transparent"
|
|
1016
|
+
onClick={() => setExpandedLogId((current) => (current === log.id ? null : log.id))}
|
|
1017
|
+
>
|
|
1018
|
+
{isExpanded ? <ChevronDown className="h-4 w-4 shrink-0" /> : <ChevronRight className="h-4 w-4 shrink-0" />}
|
|
1019
|
+
<span className="truncate">{log.message}</span>
|
|
1020
|
+
</Button>
|
|
1021
|
+
</td>
|
|
1022
|
+
</tr>
|
|
1023
|
+
{isExpanded ? (
|
|
1024
|
+
<tr className="border-b bg-muted/20 last:border-0">
|
|
1025
|
+
<td colSpan={3} className="px-4 py-4">
|
|
1026
|
+
<div className="space-y-4 rounded-lg border bg-card p-4">
|
|
1027
|
+
<div className="grid gap-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
|
|
1028
|
+
<div className="space-y-4">
|
|
1029
|
+
<section className="space-y-3">
|
|
1030
|
+
<div>
|
|
1031
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
1032
|
+
{t('integrations.detail.logs.details.summary', 'Summary')}
|
|
1033
|
+
</p>
|
|
1034
|
+
<p className="mt-1 text-sm font-medium">{log.message}</p>
|
|
1035
|
+
</div>
|
|
1036
|
+
{metadataEntries.length > 0 ? (
|
|
1037
|
+
<dl className="grid gap-3 sm:grid-cols-2">
|
|
1038
|
+
{metadataEntries.map(([label, value]) => (
|
|
1039
|
+
<div key={label} className="rounded-md border bg-muted/30 px-3 py-2">
|
|
1040
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
1041
|
+
{label}
|
|
1042
|
+
</dt>
|
|
1043
|
+
<dd className="mt-1 break-all text-sm">{value}</dd>
|
|
1044
|
+
</div>
|
|
1045
|
+
))}
|
|
1046
|
+
</dl>
|
|
1047
|
+
) : null}
|
|
1048
|
+
</section>
|
|
1049
|
+
|
|
1050
|
+
{inlineEntries.length > 0 ? (
|
|
1051
|
+
<section className="space-y-3">
|
|
1052
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
1053
|
+
{t('integrations.detail.logs.details.fields', 'Fields')}
|
|
1054
|
+
</p>
|
|
1055
|
+
<dl className="grid gap-3 sm:grid-cols-2">
|
|
1056
|
+
{inlineEntries.map(([key, value]) => (
|
|
1057
|
+
<div key={key} className="rounded-md border bg-muted/30 px-3 py-2">
|
|
1058
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
1059
|
+
{formatLogDetailLabel(key)}
|
|
1060
|
+
</dt>
|
|
1061
|
+
<dd className="mt-1 break-words text-sm">
|
|
1062
|
+
{formatLogPrimitiveValue(value)}
|
|
1063
|
+
</dd>
|
|
1064
|
+
</div>
|
|
1065
|
+
))}
|
|
1066
|
+
</dl>
|
|
1067
|
+
</section>
|
|
1068
|
+
) : null}
|
|
1069
|
+
</div>
|
|
1070
|
+
|
|
1071
|
+
<div className="space-y-3">
|
|
1072
|
+
{nestedEntries.map(([key, value]) => (
|
|
1073
|
+
<JsonDisplay
|
|
1074
|
+
key={key}
|
|
1075
|
+
data={value}
|
|
1076
|
+
title={formatLogDetailLabel(key)}
|
|
1077
|
+
defaultExpanded
|
|
1078
|
+
maxInitialDepth={1}
|
|
1079
|
+
theme="dark"
|
|
1080
|
+
maxHeight="16rem"
|
|
1081
|
+
className="p-4"
|
|
1082
|
+
/>
|
|
1083
|
+
))}
|
|
1084
|
+
{log.payload && nestedEntries.length === 0 ? (
|
|
1085
|
+
<JsonDisplay
|
|
1086
|
+
data={log.payload}
|
|
1087
|
+
title={t('integrations.detail.logs.details.payload', 'Payload')}
|
|
1088
|
+
defaultExpanded
|
|
1089
|
+
maxInitialDepth={1}
|
|
1090
|
+
theme="dark"
|
|
1091
|
+
maxHeight="16rem"
|
|
1092
|
+
className="p-4"
|
|
1093
|
+
/>
|
|
1094
|
+
) : null}
|
|
1095
|
+
{!log.payload ? (
|
|
1096
|
+
<div className="rounded-lg border border-dashed px-4 py-6 text-sm text-muted-foreground">
|
|
1097
|
+
{t('integrations.detail.logs.details.noPayload', 'No structured payload was stored for this log entry.')}
|
|
1098
|
+
</div>
|
|
1099
|
+
) : null}
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
</div>
|
|
1103
|
+
</td>
|
|
1104
|
+
</tr>
|
|
1105
|
+
) : null}
|
|
1106
|
+
</React.Fragment>
|
|
1107
|
+
)
|
|
1108
|
+
})}
|
|
421
1109
|
</tbody>
|
|
422
1110
|
</table>
|
|
423
1111
|
</div>
|
|
424
1112
|
)}
|
|
425
1113
|
</TabsContent>
|
|
1114
|
+
|
|
1115
|
+
{injectedTabs.map((tab) => (
|
|
1116
|
+
<TabsContent key={tab.id} value={tab.id} className="mt-0 space-y-4">
|
|
1117
|
+
<InjectionSpot
|
|
1118
|
+
spotId={detailWidgetSpotId}
|
|
1119
|
+
context={injectionContext}
|
|
1120
|
+
data={detail}
|
|
1121
|
+
onDataChange={(next) => setDetail(next as IntegrationDetail)}
|
|
1122
|
+
widgetsOverride={tab.widgets}
|
|
1123
|
+
/>
|
|
1124
|
+
</TabsContent>
|
|
1125
|
+
))}
|
|
426
1126
|
</Tabs>
|
|
427
1127
|
</PageBody>
|
|
428
1128
|
</Page>
|