Compare commits
9 Commits
7695f9151c
...
3c009830b6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c009830b6 | ||
|
|
883278b583 | ||
|
|
bab28d9a4c | ||
|
|
311fddb4ef | ||
|
|
c2af26e9df | ||
|
|
0bca67e45a | ||
|
|
40f8a5e755 | ||
|
|
f8503f31f3 | ||
|
|
ade3e24dd2 |
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from flask import make_response, redirect, request
|
||||
from flask_restx import Resource, reqparse
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest, Forbidden
|
||||
|
||||
@@ -32,6 +35,34 @@ from ..wraps import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerSubscriptionUpdateRequest(BaseModel):
|
||||
"""Request payload for updating a trigger subscription"""
|
||||
|
||||
name: str | None = Field(default=None, description="The name for the subscription")
|
||||
credentials: Mapping[str, Any] | None = Field(default=None, description="The credentials for the subscription")
|
||||
parameters: Mapping[str, Any] | None = Field(
|
||||
default_factory=dict, description="The parameters for the subscription"
|
||||
)
|
||||
properties: Mapping[str, Any] | None = Field(default=None, description="The properties for the subscription")
|
||||
|
||||
|
||||
class TriggerSubscriptionVerifyRequest(BaseModel):
|
||||
"""Request payload for verifying subscription credentials."""
|
||||
|
||||
credentials: Mapping[str, Any] = Field(description="The credentials to verify")
|
||||
|
||||
|
||||
console_ns.schema_model(
|
||||
TriggerSubscriptionUpdateRequest.__name__,
|
||||
TriggerSubscriptionUpdateRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
)
|
||||
|
||||
console_ns.schema_model(
|
||||
TriggerSubscriptionVerifyRequest.__name__,
|
||||
TriggerSubscriptionVerifyRequest.model_json_schema(ref_template="#/definitions/{model}"),
|
||||
)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/trigger-provider/<path:provider>/icon")
|
||||
class TriggerProviderIconApi(Resource):
|
||||
@setup_required
|
||||
@@ -155,16 +186,16 @@ parser_api = (
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify/<path:subscription_builder_id>",
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/builder/verify-and-update/<path:subscription_builder_id>",
|
||||
)
|
||||
class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
class TriggerSubscriptionBuilderVerifyAndUpdateApi(Resource):
|
||||
@console_ns.expect(parser_api)
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@account_initialization_required
|
||||
def post(self, provider, subscription_builder_id):
|
||||
"""Verify a subscription instance for a trigger provider"""
|
||||
"""Verify and update a subscription instance for a trigger provider"""
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
@@ -289,6 +320,74 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
raise ValueError(str(e)) from e
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/update",
|
||||
)
|
||||
class TriggerSubscriptionUpdateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionUpdateRequest.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@account_initialization_required
|
||||
def post(self, subscription_id: str):
|
||||
"""Update a subscription instance"""
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
args = TriggerSubscriptionUpdateRequest.model_validate(console_ns.payload)
|
||||
|
||||
subscription = TriggerProviderService.get_subscription_by_id(
|
||||
tenant_id=user.current_tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
if not subscription:
|
||||
raise NotFoundError(f"Subscription {subscription_id} not found")
|
||||
|
||||
provider_id = TriggerProviderID(subscription.provider_id)
|
||||
|
||||
try:
|
||||
# rename only for update name
|
||||
is_rename_only = args.name is not None and args.credentials is None and args.parameters is None
|
||||
if is_rename_only:
|
||||
TriggerProviderService.update_trigger_subscription(
|
||||
tenant_id=user.current_tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
name=args.name,
|
||||
)
|
||||
return 200
|
||||
|
||||
# rebuild for create automatically by the provider
|
||||
match subscription.credential_type:
|
||||
case CredentialType.UNAUTHORIZED:
|
||||
TriggerProviderService.update_trigger_subscription(
|
||||
tenant_id=user.current_tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
name=args.name,
|
||||
properties=args.properties,
|
||||
)
|
||||
return 200
|
||||
case CredentialType.API_KEY | CredentialType.OAUTH2:
|
||||
if not args.credentials or not args.parameters:
|
||||
raise BadRequest("Credentials and parameters are required for rebuild")
|
||||
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=user.current_tenant_id,
|
||||
name=args.name,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription_id,
|
||||
credentials=args.credentials,
|
||||
parameters=args.parameters,
|
||||
)
|
||||
return 200
|
||||
case _:
|
||||
raise BadRequest("Invalid credential type")
|
||||
except ValueError as e:
|
||||
raise BadRequest(str(e))
|
||||
except Exception as e:
|
||||
logger.exception("Error updating subscription", exc_info=e)
|
||||
raise
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:subscription_id>/subscriptions/delete",
|
||||
)
|
||||
@@ -576,3 +675,38 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
except Exception as e:
|
||||
logger.exception("Error removing OAuth client", exc_info=e)
|
||||
raise
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/workspaces/current/trigger-provider/<path:provider>/subscriptions/verify/<path:subscription_id>",
|
||||
)
|
||||
class TriggerSubscriptionVerifyApi(Resource):
|
||||
@console_ns.expect(console_ns.models[TriggerSubscriptionVerifyRequest.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@edit_permission_required
|
||||
@account_initialization_required
|
||||
def post(self, provider, subscription_id):
|
||||
"""Verify credentials for an existing subscription (edit mode only)"""
|
||||
user = current_user
|
||||
assert user.current_tenant_id is not None
|
||||
|
||||
verify_request: TriggerSubscriptionVerifyRequest = TriggerSubscriptionVerifyRequest.model_validate(
|
||||
console_ns.payload
|
||||
)
|
||||
|
||||
try:
|
||||
result = TriggerProviderService.verify_subscription_credentials(
|
||||
tenant_id=user.current_tenant_id,
|
||||
user_id=user.id,
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_id=subscription_id,
|
||||
credentials=verify_request.credentials,
|
||||
)
|
||||
return result
|
||||
except ValueError as e:
|
||||
logger.warning("Credential verification failed", exc_info=e)
|
||||
raise BadRequest(str(e)) from e
|
||||
except Exception as e:
|
||||
logger.exception("Error verifying subscription credentials", exc_info=e)
|
||||
raise BadRequest(str(e)) from e
|
||||
|
||||
@@ -67,12 +67,16 @@ def create_trigger_provider_encrypter_for_subscription(
|
||||
|
||||
|
||||
def delete_cache_for_subscription(tenant_id: str, provider_id: str, subscription_id: str):
|
||||
cache = TriggerProviderCredentialsCache(
|
||||
TriggerProviderCredentialsCache(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider_id,
|
||||
credential_id=subscription_id,
|
||||
)
|
||||
cache.delete()
|
||||
).delete()
|
||||
TriggerProviderPropertiesCache(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription_id,
|
||||
).delete()
|
||||
|
||||
|
||||
def create_trigger_provider_encrypter_for_properties(
|
||||
|
||||
@@ -94,16 +94,23 @@ class TriggerProviderService:
|
||||
|
||||
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||
for subscription in subscriptions:
|
||||
encrypter, _ = create_trigger_provider_encrypter_for_subscription(
|
||||
credential_encrypter, _ = create_trigger_provider_encrypter_for_subscription(
|
||||
tenant_id=tenant_id,
|
||||
controller=provider_controller,
|
||||
subscription=subscription,
|
||||
)
|
||||
subscription.credentials = dict(
|
||||
encrypter.mask_credentials(dict(encrypter.decrypt(subscription.credentials)))
|
||||
credential_encrypter.mask_credentials(dict(credential_encrypter.decrypt(subscription.credentials)))
|
||||
)
|
||||
subscription.properties = dict(encrypter.mask_credentials(dict(encrypter.decrypt(subscription.properties))))
|
||||
subscription.parameters = dict(encrypter.mask_credentials(dict(encrypter.decrypt(subscription.parameters))))
|
||||
properties_encrypter, _ = create_trigger_provider_encrypter_for_properties(
|
||||
tenant_id=tenant_id,
|
||||
controller=provider_controller,
|
||||
subscription=subscription,
|
||||
)
|
||||
subscription.properties = dict(
|
||||
properties_encrypter.mask_credentials(dict(properties_encrypter.decrypt(subscription.properties)))
|
||||
)
|
||||
subscription.parameters = dict(subscription.parameters)
|
||||
count = workflows_in_use_map.get(subscription.id)
|
||||
subscription.workflows_in_use = count if count is not None else 0
|
||||
|
||||
@@ -209,6 +216,101 @@ class TriggerProviderService:
|
||||
logger.exception("Failed to add trigger provider")
|
||||
raise ValueError(str(e))
|
||||
|
||||
@classmethod
|
||||
def update_trigger_subscription(
|
||||
cls,
|
||||
tenant_id: str,
|
||||
subscription_id: str,
|
||||
name: str | None = None,
|
||||
properties: Mapping[str, Any] | None = None,
|
||||
parameters: Mapping[str, Any] | None = None,
|
||||
credentials: Mapping[str, Any] | None = None,
|
||||
credential_expires_at: int | None = None,
|
||||
expires_at: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Update an existing trigger subscription.
|
||||
|
||||
:param tenant_id: Tenant ID
|
||||
:param subscription_id: Subscription instance ID
|
||||
:param name: Optional new name for this subscription
|
||||
:param properties: Optional new properties
|
||||
:param parameters: Optional new parameters
|
||||
:param credentials: Optional new credentials
|
||||
:param credential_expires_at: Optional new credential expiration timestamp
|
||||
:param expires_at: Optional new expiration timestamp
|
||||
:return: Success response with updated subscription info
|
||||
"""
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
# Use distributed lock to prevent race conditions on the same subscription
|
||||
lock_key = f"trigger_subscription_update_lock:{tenant_id}_{subscription_id}"
|
||||
with redis_client.lock(lock_key, timeout=20):
|
||||
subscription: TriggerSubscription | None = (
|
||||
session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first()
|
||||
)
|
||||
if not subscription:
|
||||
raise ValueError(f"Trigger subscription {subscription_id} not found")
|
||||
|
||||
provider_id = TriggerProviderID(subscription.provider_id)
|
||||
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||
|
||||
# Check for name uniqueness if name is being updated
|
||||
if name is not None and name != subscription.name:
|
||||
existing = (
|
||||
session.query(TriggerSubscription)
|
||||
.filter_by(tenant_id=tenant_id, provider_id=str(provider_id), name=name)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
raise ValueError(f"Subscription name '{name}' already exists for this provider")
|
||||
subscription.name = name
|
||||
|
||||
# Update properties if provided
|
||||
if properties is not None:
|
||||
properties_encrypter, _ = create_provider_encrypter(
|
||||
tenant_id=tenant_id,
|
||||
config=provider_controller.get_properties_schema(),
|
||||
cache=NoOpProviderCredentialCache(),
|
||||
)
|
||||
# Handle hidden values - preserve original encrypted values
|
||||
original_properties = properties_encrypter.decrypt(subscription.properties)
|
||||
new_properties: dict[str, Any] = {
|
||||
key: value if value != HIDDEN_VALUE else original_properties.get(key, UNKNOWN_VALUE)
|
||||
for key, value in properties.items()
|
||||
}
|
||||
subscription.properties = dict(properties_encrypter.encrypt(new_properties))
|
||||
|
||||
# Update parameters if provided
|
||||
if parameters is not None:
|
||||
subscription.parameters = dict(parameters)
|
||||
|
||||
# Update credentials if provided
|
||||
if credentials is not None:
|
||||
credential_type = CredentialType.of(subscription.credential_type)
|
||||
credential_encrypter, _ = create_provider_encrypter(
|
||||
tenant_id=tenant_id,
|
||||
config=provider_controller.get_credential_schema_config(credential_type),
|
||||
cache=NoOpProviderCredentialCache(),
|
||||
)
|
||||
subscription.credentials = dict(credential_encrypter.encrypt(dict(credentials)))
|
||||
|
||||
# Update credential expiration timestamp if provided
|
||||
if credential_expires_at is not None:
|
||||
subscription.credential_expires_at = credential_expires_at
|
||||
|
||||
# Update expiration timestamp if provided
|
||||
if expires_at is not None:
|
||||
subscription.expires_at = expires_at
|
||||
|
||||
session.commit()
|
||||
|
||||
# Clear subscription cache
|
||||
delete_cache_for_subscription(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=subscription.provider_id,
|
||||
subscription_id=subscription.id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_subscription_by_id(cls, tenant_id: str, subscription_id: str | None = None) -> TriggerSubscription | None:
|
||||
"""
|
||||
@@ -258,7 +360,9 @@ class TriggerProviderService:
|
||||
|
||||
credential_type: CredentialType = CredentialType.of(subscription.credential_type)
|
||||
is_auto_created: bool = credential_type in [CredentialType.OAUTH2, CredentialType.API_KEY]
|
||||
if is_auto_created:
|
||||
if not is_auto_created:
|
||||
return None
|
||||
|
||||
provider_id = TriggerProviderID(subscription.provider_id)
|
||||
provider_controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider(
|
||||
tenant_id=tenant_id, provider_id=provider_id
|
||||
@@ -280,8 +384,8 @@ class TriggerProviderService:
|
||||
except Exception as e:
|
||||
logger.exception("Error unsubscribing trigger", exc_info=e)
|
||||
|
||||
# Clear cache
|
||||
session.delete(subscription)
|
||||
# Clear cache
|
||||
delete_cache_for_subscription(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=subscription.provider_id,
|
||||
@@ -688,3 +792,129 @@ class TriggerProviderService:
|
||||
)
|
||||
subscription.properties = dict(properties_encrypter.decrypt(subscription.properties))
|
||||
return subscription
|
||||
|
||||
@classmethod
|
||||
def verify_subscription_credentials(
|
||||
cls,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
provider_id: TriggerProviderID,
|
||||
subscription_id: str,
|
||||
credentials: Mapping[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Verify credentials for an existing subscription without updating it.
|
||||
|
||||
This is used in edit mode to validate new credentials before rebuild.
|
||||
|
||||
:param tenant_id: Tenant ID
|
||||
:param user_id: User ID
|
||||
:param provider_id: Provider identifier
|
||||
:param subscription_id: Subscription ID
|
||||
:param credentials: New credentials to verify
|
||||
:return: dict with 'verified' boolean
|
||||
"""
|
||||
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||
if not provider_controller:
|
||||
raise ValueError(f"Provider {provider_id} not found")
|
||||
|
||||
subscription = cls.get_subscription_by_id(
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
if not subscription:
|
||||
raise ValueError(f"Subscription {subscription_id} not found")
|
||||
|
||||
credential_type = CredentialType.of(subscription.credential_type)
|
||||
|
||||
# For API Key, validate the new credentials
|
||||
if credential_type == CredentialType.API_KEY:
|
||||
try:
|
||||
provider_controller.validate_credentials(user_id, credentials)
|
||||
return {"verified": True}
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid credentials: {e}") from e
|
||||
|
||||
return {"verified": True}
|
||||
|
||||
@classmethod
|
||||
def rebuild_trigger_subscription(
|
||||
cls,
|
||||
tenant_id: str,
|
||||
provider_id: TriggerProviderID,
|
||||
subscription_id: str,
|
||||
credentials: Mapping[str, Any],
|
||||
parameters: Mapping[str, Any],
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Create a subscription builder for rebuilding an existing subscription.
|
||||
|
||||
This method creates a builder pre-filled with data from the rebuild request,
|
||||
keeping the same subscription_id and endpoint_id so the webhook URL remains unchanged.
|
||||
|
||||
:param tenant_id: Tenant ID
|
||||
:param name: Name for the subscription
|
||||
:param subscription_id: Subscription ID
|
||||
:param provider_id: Provider identifier
|
||||
:param credentials: Credentials for the subscription
|
||||
:param parameters: Parameters for the subscription
|
||||
:return: SubscriptionBuilderApiEntity
|
||||
"""
|
||||
provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
|
||||
if not provider_controller:
|
||||
raise ValueError(f"Provider {provider_id} not found")
|
||||
|
||||
subscription = TriggerProviderService.get_subscription_by_id(
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
)
|
||||
if not subscription:
|
||||
raise ValueError(f"Subscription {subscription_id} not found")
|
||||
|
||||
credential_type = CredentialType.of(subscription.credential_type)
|
||||
if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]:
|
||||
raise ValueError("Credential type not supported for rebuild")
|
||||
|
||||
# TODO: Tring to invoke update api of the plugin trigger provider
|
||||
|
||||
# FALLBACK: If the update api is not implemented, delete the previous subscription and create a new one
|
||||
|
||||
# Delete the previous subscription
|
||||
encrypter, _ = create_trigger_provider_encrypter_for_subscription(
|
||||
tenant_id=tenant_id,
|
||||
controller=provider_controller,
|
||||
subscription=subscription,
|
||||
)
|
||||
user_id = subscription.user_id
|
||||
TriggerManager.unsubscribe_trigger(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=provider_id,
|
||||
subscription=subscription.to_entity(),
|
||||
credentials=encrypter.decrypt(subscription.credentials),
|
||||
credential_type=credential_type,
|
||||
)
|
||||
new_credentials: dict[str, Any] = {
|
||||
key: value if value != HIDDEN_VALUE else subscription.credentials.get(key, UNKNOWN_VALUE)
|
||||
for key, value in credentials.items()
|
||||
}
|
||||
# Create a new subscription with the same subscription_id and endpoint_id
|
||||
new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=provider_id,
|
||||
endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id),
|
||||
parameters=parameters,
|
||||
credentials=new_credentials,
|
||||
credential_type=credential_type,
|
||||
)
|
||||
TriggerProviderService.update_trigger_subscription(
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription.id,
|
||||
name=name,
|
||||
parameters=parameters,
|
||||
credentials=new_credentials,
|
||||
properties=new_subscription.properties,
|
||||
expires_at=new_subscription.expires_at,
|
||||
)
|
||||
|
||||
@@ -453,11 +453,12 @@ class TriggerSubscriptionBuilderService:
|
||||
if not subscription_builder:
|
||||
return None
|
||||
|
||||
try:
|
||||
# response to validation endpoint
|
||||
controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider(
|
||||
tenant_id=subscription_builder.tenant_id, provider_id=TriggerProviderID(subscription_builder.provider_id)
|
||||
tenant_id=subscription_builder.tenant_id,
|
||||
provider_id=TriggerProviderID(subscription_builder.provider_id),
|
||||
)
|
||||
try:
|
||||
dispatch_response: TriggerDispatchResponse = controller.dispatch(
|
||||
request=request,
|
||||
subscription=subscription_builder.to_subscription(),
|
||||
|
||||
@@ -66,7 +66,7 @@ const PluginDetailPanel: FC<Props> = ({
|
||||
<div className='flex-1'>
|
||||
{detail.declaration.category === PluginCategoryEnum.trigger && (
|
||||
<>
|
||||
<SubscriptionList />
|
||||
<SubscriptionList pluginDetail={detail} />
|
||||
<TriggerEventsList />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useTriggerSubscriptionBuilderLogs,
|
||||
useUpdateTriggerSubscriptionBuilder,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
useVerifyAndUpdateTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
||||
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
|
||||
@@ -98,7 +98,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => {
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>(builder)
|
||||
const isInitializedRef = useRef(false)
|
||||
|
||||
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyAndUpdateTriggerSubscriptionBuilder()
|
||||
const { mutateAsync: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
const { mutate: updateBuilder } = useUpdateTriggerSubscriptionBuilder()
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useConfigureTriggerOAuth,
|
||||
useDeleteTriggerOAuth,
|
||||
useInitiateTriggerOAuth,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
useVerifyAndUpdateTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import {
|
||||
RiClipboardLine,
|
||||
@@ -64,7 +64,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
|
||||
|
||||
const providerName = detail?.provider || ''
|
||||
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
||||
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: verifyBuilder } = useVerifyAndUpdateTriggerSubscriptionBuilder()
|
||||
const { mutate: configureOAuth } = useConfigureTriggerOAuth()
|
||||
const { mutate: deleteOAuth } = useDeleteTriggerOAuth()
|
||||
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
'use client'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers'
|
||||
import { parsePluginErrorMessage } from '@/utils/error-parser'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { useSubscriptionList } from '../use-subscription-list'
|
||||
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
subscription: TriggerSubscription
|
||||
pluginDetail?: PluginDetail
|
||||
}
|
||||
|
||||
enum EditStep {
|
||||
EditCredentials = 'edit_credentials',
|
||||
EditConfiguration = 'edit_configuration',
|
||||
}
|
||||
|
||||
const normalizeFormType = (type: string): FormTypeEnum => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
return FormTypeEnum.textInput
|
||||
case 'password':
|
||||
case 'secret':
|
||||
return FormTypeEnum.secretInput
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return FormTypeEnum.textNumber
|
||||
case 'boolean':
|
||||
return FormTypeEnum.boolean
|
||||
case 'select':
|
||||
return FormTypeEnum.select
|
||||
default:
|
||||
if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
|
||||
return type as FormTypeEnum
|
||||
return FormTypeEnum.textInput
|
||||
}
|
||||
}
|
||||
|
||||
const StatusStep = ({ isActive, text }: { isActive: boolean, text: string }) => {
|
||||
return <div className={`system-2xs-semibold-uppercase flex items-center gap-1 ${isActive
|
||||
? 'text-state-accent-solid'
|
||||
: 'text-text-tertiary'}`}>
|
||||
{isActive && (
|
||||
<div className='h-1 w-1 rounded-full bg-state-accent-solid'></div>
|
||||
)}
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
|
||||
const MultiSteps = ({ currentStep }: { currentStep: EditStep }) => {
|
||||
const { t } = useTranslation()
|
||||
return <div className='mb-6 flex w-1/3 items-center gap-2'>
|
||||
<StatusStep isActive={currentStep === EditStep.EditCredentials} text={t('pluginTrigger.modal.steps.verify')} />
|
||||
<div className='h-px w-3 shrink-0 bg-divider-deep'></div>
|
||||
<StatusStep isActive={currentStep === EditStep.EditConfiguration} text={t('pluginTrigger.modal.steps.configuration')} />
|
||||
</div>
|
||||
}
|
||||
|
||||
export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { refetch } = useSubscriptionList()
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<EditStep>(EditStep.EditCredentials)
|
||||
|
||||
const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
|
||||
const { mutate: verifyCredentials, isPending: isVerifying } = useVerifyTriggerSubscription()
|
||||
|
||||
const parametersSchema = useMemo<ParametersSchema[]>(
|
||||
() => detail?.declaration?.trigger?.subscription_constructor?.parameters || [],
|
||||
[detail?.declaration?.trigger?.subscription_constructor?.parameters],
|
||||
)
|
||||
|
||||
const rawApiKeyCredentialsSchema = detail?.declaration.trigger?.subscription_constructor?.credentials_schema || []
|
||||
const apiKeyCredentialsSchema = useMemo(() => {
|
||||
return rawApiKeyCredentialsSchema.map(schema => ({
|
||||
...schema,
|
||||
tooltip: schema.help,
|
||||
}))
|
||||
}, [rawApiKeyCredentialsSchema])
|
||||
|
||||
const basicFormRef = useRef<FormRefObject>(null)
|
||||
const parametersFormRef = useRef<FormRefObject>(null)
|
||||
const credentialsFormRef = useRef<FormRefObject>(null)
|
||||
|
||||
const handleVerifyCredentials = () => {
|
||||
const credentialsFormValues = credentialsFormRef.current?.getFormValues({
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
}) || { values: {}, isCheckValidated: false }
|
||||
|
||||
if (!credentialsFormValues.isCheckValidated)
|
||||
return
|
||||
|
||||
const credentials = credentialsFormValues.values
|
||||
|
||||
// Clear previous errors
|
||||
if (Object.keys(credentials).length > 0) {
|
||||
credentialsFormRef.current?.setFields([{
|
||||
name: Object.keys(credentials)[0],
|
||||
errors: [],
|
||||
}])
|
||||
}
|
||||
|
||||
verifyCredentials(
|
||||
{
|
||||
provider: subscription.provider,
|
||||
subscriptionId: subscription.id,
|
||||
credentials,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.apiKey.verify.success'),
|
||||
})
|
||||
setCurrentStep(EditStep.EditConfiguration)
|
||||
},
|
||||
onError: async (error: any) => {
|
||||
const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.apiKey.verify.error')
|
||||
if (Object.keys(credentials).length > 0) {
|
||||
credentialsFormRef.current?.setFields([{
|
||||
name: Object.keys(credentials)[0],
|
||||
errors: [errorMessage],
|
||||
}])
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
const basicFormValues = basicFormRef.current?.getFormValues({})
|
||||
if (!basicFormValues?.isCheckValidated)
|
||||
return
|
||||
|
||||
const name = basicFormValues.values.subscription_name as string
|
||||
|
||||
let parameters: Record<string, any> | undefined
|
||||
|
||||
if (parametersSchema.length > 0) {
|
||||
const paramsFormValues = parametersFormRef.current?.getFormValues({
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
})
|
||||
if (!paramsFormValues?.isCheckValidated)
|
||||
return
|
||||
parameters = Object.keys(paramsFormValues.values).length > 0 ? paramsFormValues.values : undefined
|
||||
}
|
||||
|
||||
updateSubscription(
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
name,
|
||||
parameters,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
|
||||
})
|
||||
refetch?.()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.subscription.list.item.actions.edit.error'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (currentStep === EditStep.EditCredentials)
|
||||
handleVerifyCredentials()
|
||||
else
|
||||
handleUpdate()
|
||||
}
|
||||
|
||||
const handleCredentialsChange = () => {
|
||||
if (apiKeyCredentialsSchema.length > 0) {
|
||||
credentialsFormRef.current?.setFields([{
|
||||
name: apiKeyCredentialsSchema[0].name,
|
||||
errors: [],
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
const basicFormSchemas: FormSchema[] = useMemo(() => [
|
||||
{
|
||||
name: 'subscription_name',
|
||||
label: t('pluginTrigger.modal.form.subscriptionName.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: true,
|
||||
default: subscription.name,
|
||||
},
|
||||
{
|
||||
name: 'callback_url',
|
||||
label: t('pluginTrigger.modal.form.callbackUrl.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: false,
|
||||
default: subscription.endpoint || '',
|
||||
disabled: true,
|
||||
tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
|
||||
showCopy: true,
|
||||
},
|
||||
], [t, subscription.name, subscription.endpoint])
|
||||
|
||||
const credentialsFormSchemas: FormSchema[] = useMemo(() => {
|
||||
return apiKeyCredentialsSchema.map(schema => ({
|
||||
...schema,
|
||||
type: normalizeFormType(schema.type as string),
|
||||
tooltip: schema.help,
|
||||
default: subscription.credentials?.[schema.name] || schema.default,
|
||||
}))
|
||||
}, [apiKeyCredentialsSchema, subscription.credentials])
|
||||
|
||||
const parametersFormSchemas: FormSchema[] = useMemo(() => {
|
||||
return parametersSchema.map((schema: ParametersSchema) => {
|
||||
const normalizedType = normalizeFormType(schema.type as string)
|
||||
return {
|
||||
...schema,
|
||||
type: normalizedType,
|
||||
tooltip: schema.description,
|
||||
default: subscription.parameters?.[schema.name] || schema.default,
|
||||
dynamicSelectParams: normalizedType === FormTypeEnum.dynamicSelect
|
||||
? {
|
||||
plugin_id: detail?.plugin_id || '',
|
||||
provider: detail?.provider || '',
|
||||
action: 'provider',
|
||||
parameter: schema.name,
|
||||
credential_id: subscription.id,
|
||||
}
|
||||
: undefined,
|
||||
fieldClassName: schema.type === FormTypeEnum.boolean ? 'flex items-center justify-between' : undefined,
|
||||
labelClassName: schema.type === FormTypeEnum.boolean ? 'mb-0' : undefined,
|
||||
}
|
||||
})
|
||||
}, [parametersSchema, subscription.parameters, subscription.id, detail?.plugin_id, detail?.provider])
|
||||
|
||||
const getConfirmButtonText = () => {
|
||||
if (currentStep === EditStep.EditCredentials)
|
||||
return isVerifying ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')
|
||||
|
||||
return isUpdating ? t('common.operation.saving') : t('common.operation.save')
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('pluginTrigger.subscription.list.item.actions.edit.title')}
|
||||
confirmButtonText={getConfirmButtonText()}
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onConfirm={handleConfirm}
|
||||
disabled={isUpdating || isVerifying}
|
||||
clickOutsideNotClose
|
||||
wrapperClassName='!z-[101]'
|
||||
bottomSlot={currentStep === EditStep.EditCredentials ? <EncryptedBottom /> : null}
|
||||
>
|
||||
{pluginDetail && (
|
||||
<ReadmeEntrance pluginDetail={pluginDetail} showType={ReadmeShowType.modal} />
|
||||
)}
|
||||
|
||||
{/* Multi-step indicator */}
|
||||
<MultiSteps currentStep={currentStep} />
|
||||
|
||||
{/* Step 1: Edit Credentials */}
|
||||
{currentStep === EditStep.EditCredentials && (
|
||||
<div className='mb-4'>
|
||||
{credentialsFormSchemas.length > 0 && (
|
||||
<BaseForm
|
||||
formSchemas={credentialsFormSchemas}
|
||||
ref={credentialsFormRef}
|
||||
labelClassName='system-sm-medium mb-2 flex items-center gap-1 text-text-primary'
|
||||
formClassName='space-y-4'
|
||||
preventDefaultSubmit={true}
|
||||
onChange={handleCredentialsChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Edit Configuration */}
|
||||
{currentStep === EditStep.EditConfiguration && (
|
||||
<div className='max-h-[70vh]'>
|
||||
{/* Basic form: subscription name and callback URL */}
|
||||
<BaseForm
|
||||
formSchemas={basicFormSchemas}
|
||||
ref={basicFormRef}
|
||||
labelClassName='system-sm-medium mb-2 flex items-center gap-1 text-text-primary'
|
||||
formClassName='space-y-4 mb-4'
|
||||
/>
|
||||
|
||||
{/* Parameters */}
|
||||
{parametersFormSchemas.length > 0 && (
|
||||
<BaseForm
|
||||
formSchemas={parametersFormSchemas}
|
||||
ref={parametersFormRef}
|
||||
labelClassName='system-sm-medium mb-2 flex items-center gap-1 text-text-primary'
|
||||
formClassName='space-y-4'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { ManualEditModal } from './manual-edit-modal'
|
||||
import { OAuthEditModal } from './oauth-edit-modal'
|
||||
import { ApiKeyEditModal } from './apikey-edit-modal'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
subscription: TriggerSubscription
|
||||
pluginDetail?: PluginDetail
|
||||
}
|
||||
|
||||
export const EditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||
const credentialType = subscription.credential_type
|
||||
|
||||
switch (credentialType) {
|
||||
case TriggerCredentialTypeEnum.Unauthorized:
|
||||
return <ManualEditModal onClose={onClose} subscription={subscription} pluginDetail={pluginDetail} />
|
||||
case TriggerCredentialTypeEnum.Oauth2:
|
||||
return <OAuthEditModal onClose={onClose} subscription={subscription} pluginDetail={pluginDetail} />
|
||||
case TriggerCredentialTypeEnum.ApiKey:
|
||||
return <ApiKeyEditModal onClose={onClose} subscription={subscription} pluginDetail={pluginDetail} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { useSubscriptionList } from '../use-subscription-list'
|
||||
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
subscription: TriggerSubscription
|
||||
pluginDetail?: PluginDetail
|
||||
}
|
||||
|
||||
const normalizeFormType = (type: string): FormTypeEnum => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
return FormTypeEnum.textInput
|
||||
case 'password':
|
||||
case 'secret':
|
||||
return FormTypeEnum.secretInput
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return FormTypeEnum.textNumber
|
||||
case 'boolean':
|
||||
return FormTypeEnum.boolean
|
||||
case 'select':
|
||||
return FormTypeEnum.select
|
||||
default:
|
||||
if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
|
||||
return type as FormTypeEnum
|
||||
return FormTypeEnum.textInput
|
||||
}
|
||||
}
|
||||
|
||||
export const ManualEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { refetch } = useSubscriptionList()
|
||||
|
||||
const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
|
||||
|
||||
const propertiesSchema = useMemo<ParametersSchema[]>(
|
||||
() => detail?.declaration?.trigger?.subscription_schema || [],
|
||||
[detail?.declaration?.trigger?.subscription_schema],
|
||||
)
|
||||
|
||||
const formRef = useRef<FormRefObject>(null)
|
||||
|
||||
const handleConfirm = () => {
|
||||
const formValues = formRef.current?.getFormValues({
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
})
|
||||
if (!formValues?.isCheckValidated)
|
||||
return
|
||||
|
||||
const name = formValues.values.subscription_name as string
|
||||
|
||||
// Extract properties (exclude subscription_name and callback_url)
|
||||
const properties = { ...formValues.values }
|
||||
delete properties.subscription_name
|
||||
delete properties.callback_url
|
||||
|
||||
updateSubscription(
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
name,
|
||||
properties: Object.keys(properties).length > 0 ? properties : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
|
||||
})
|
||||
refetch?.()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.subscription.list.item.actions.edit.error'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const formSchemas: FormSchema[] = useMemo(() => [
|
||||
{
|
||||
name: 'subscription_name',
|
||||
label: t('pluginTrigger.modal.form.subscriptionName.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: true,
|
||||
default: subscription.name,
|
||||
},
|
||||
{
|
||||
name: 'callback_url',
|
||||
label: t('pluginTrigger.modal.form.callbackUrl.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: false,
|
||||
default: subscription.endpoint || '',
|
||||
disabled: true,
|
||||
tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
|
||||
showCopy: true,
|
||||
},
|
||||
...propertiesSchema.map((schema: ParametersSchema) => ({
|
||||
...schema,
|
||||
type: normalizeFormType(schema.type as string),
|
||||
tooltip: schema.description,
|
||||
default: subscription.properties?.[schema.name] || schema.default,
|
||||
})),
|
||||
], [t, subscription.name, subscription.endpoint, subscription.properties, propertiesSchema])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('pluginTrigger.subscription.list.item.actions.edit.title')}
|
||||
confirmButtonText={isUpdating ? t('common.operation.saving') : t('common.operation.save')}
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onConfirm={handleConfirm}
|
||||
disabled={isUpdating}
|
||||
clickOutsideNotClose
|
||||
wrapperClassName='!z-[101]'
|
||||
>
|
||||
{pluginDetail && (
|
||||
<ReadmeEntrance pluginDetail={pluginDetail} showType={ReadmeShowType.modal} />
|
||||
)}
|
||||
<BaseForm
|
||||
formSchemas={formSchemas}
|
||||
ref={formRef}
|
||||
labelClassName='system-sm-medium mb-2 flex items-center gap-1 text-text-primary'
|
||||
formClassName='space-y-4'
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
'use client'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import type { FormRefObject, FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { useUpdateTriggerSubscription } from '@/service/use-triggers'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { useSubscriptionList } from '../use-subscription-list'
|
||||
import { ReadmeShowType } from '../../../readme-panel/store'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
subscription: TriggerSubscription
|
||||
pluginDetail?: PluginDetail
|
||||
}
|
||||
|
||||
const normalizeFormType = (type: string): FormTypeEnum => {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
case 'text':
|
||||
return FormTypeEnum.textInput
|
||||
case 'password':
|
||||
case 'secret':
|
||||
return FormTypeEnum.secretInput
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return FormTypeEnum.textNumber
|
||||
case 'boolean':
|
||||
return FormTypeEnum.boolean
|
||||
case 'select':
|
||||
return FormTypeEnum.select
|
||||
default:
|
||||
if (Object.values(FormTypeEnum).includes(type as FormTypeEnum))
|
||||
return type as FormTypeEnum
|
||||
return FormTypeEnum.textInput
|
||||
}
|
||||
}
|
||||
|
||||
export const OAuthEditModal = ({ onClose, subscription, pluginDetail }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const { refetch } = useSubscriptionList()
|
||||
|
||||
const { mutate: updateSubscription, isPending: isUpdating } = useUpdateTriggerSubscription()
|
||||
|
||||
const parametersSchema = useMemo<ParametersSchema[]>(
|
||||
() => detail?.declaration?.trigger?.subscription_constructor?.parameters || [],
|
||||
[detail?.declaration?.trigger?.subscription_constructor?.parameters],
|
||||
)
|
||||
|
||||
const formRef = useRef<FormRefObject>(null)
|
||||
|
||||
const handleConfirm = () => {
|
||||
const formValues = formRef.current?.getFormValues({
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
})
|
||||
if (!formValues?.isCheckValidated)
|
||||
return
|
||||
|
||||
const name = formValues.values.subscription_name as string
|
||||
|
||||
// Extract parameters (exclude subscription_name and callback_url)
|
||||
const parameters = { ...formValues.values }
|
||||
delete parameters.subscription_name
|
||||
delete parameters.callback_url
|
||||
|
||||
updateSubscription(
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
name,
|
||||
parameters: Object.keys(parameters).length > 0 ? parameters : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.subscription.list.item.actions.edit.success'),
|
||||
})
|
||||
refetch?.()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.subscription.list.item.actions.edit.error'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const formSchemas: FormSchema[] = useMemo(() => [
|
||||
{
|
||||
name: 'subscription_name',
|
||||
label: t('pluginTrigger.modal.form.subscriptionName.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.subscriptionName.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: true,
|
||||
default: subscription.name,
|
||||
},
|
||||
{
|
||||
name: 'callback_url',
|
||||
label: t('pluginTrigger.modal.form.callbackUrl.label'),
|
||||
placeholder: t('pluginTrigger.modal.form.callbackUrl.placeholder'),
|
||||
type: FormTypeEnum.textInput,
|
||||
required: false,
|
||||
default: subscription.endpoint || '',
|
||||
disabled: true,
|
||||
tooltip: t('pluginTrigger.modal.form.callbackUrl.tooltip'),
|
||||
showCopy: true,
|
||||
},
|
||||
...parametersSchema.map((schema: ParametersSchema) => {
|
||||
const normalizedType = normalizeFormType(schema.type as string)
|
||||
return {
|
||||
...schema,
|
||||
type: normalizedType,
|
||||
tooltip: schema.description,
|
||||
default: subscription.parameters?.[schema.name] || schema.default,
|
||||
dynamicSelectParams: normalizedType === FormTypeEnum.dynamicSelect
|
||||
? {
|
||||
plugin_id: detail?.plugin_id || '',
|
||||
provider: detail?.provider || '',
|
||||
action: 'provider',
|
||||
parameter: schema.name,
|
||||
credential_id: subscription.id,
|
||||
}
|
||||
: undefined,
|
||||
fieldClassName: schema.type === FormTypeEnum.boolean ? 'flex items-center justify-between' : undefined,
|
||||
labelClassName: schema.type === FormTypeEnum.boolean ? 'mb-0' : undefined,
|
||||
}
|
||||
}),
|
||||
], [t, subscription.name, subscription.endpoint, subscription.parameters, subscription.id, parametersSchema, detail?.plugin_id, detail?.provider])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('pluginTrigger.subscription.list.item.actions.edit.title')}
|
||||
confirmButtonText={isUpdating ? t('common.operation.saving') : t('common.operation.save')}
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onConfirm={handleConfirm}
|
||||
disabled={isUpdating}
|
||||
clickOutsideNotClose
|
||||
wrapperClassName='!z-[101]'
|
||||
>
|
||||
{pluginDetail && (
|
||||
<ReadmeEntrance pluginDetail={pluginDetail} showType={ReadmeShowType.modal} />
|
||||
)}
|
||||
<BaseForm
|
||||
formSchemas={formSchemas}
|
||||
ref={formRef}
|
||||
labelClassName='system-sm-medium mb-2 flex items-center gap-1 text-text-primary'
|
||||
formClassName='space-y-4'
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { withErrorBoundary } from '@/app/components/base/error-boundary'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { SubscriptionListView } from './list-view'
|
||||
import { SubscriptionSelectorView } from './selector-view'
|
||||
import { useSubscriptionList } from './use-subscription-list'
|
||||
@@ -18,6 +19,7 @@ type SubscriptionListProps = {
|
||||
mode?: SubscriptionListMode
|
||||
selectedId?: string
|
||||
onSelect?: (v: SimpleSubscription, callback?: () => void) => void
|
||||
pluginDetail?: PluginDetail
|
||||
}
|
||||
|
||||
export { SubscriptionSelectorEntry } from './selector-entry'
|
||||
@@ -26,6 +28,7 @@ export const SubscriptionList = withErrorBoundary(({
|
||||
mode = SubscriptionListMode.PANEL,
|
||||
selectedId,
|
||||
onSelect,
|
||||
pluginDetail,
|
||||
}: SubscriptionListProps) => {
|
||||
const { isLoading, refetch } = useSubscriptionList()
|
||||
if (isLoading) {
|
||||
@@ -47,5 +50,5 @@ export const SubscriptionList = withErrorBoundary(({
|
||||
)
|
||||
}
|
||||
|
||||
return <SubscriptionListView />
|
||||
return <SubscriptionListView pluginDetail={pluginDetail} />
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -9,10 +10,12 @@ import { useSubscriptionList } from './use-subscription-list'
|
||||
|
||||
type SubscriptionListViewProps = {
|
||||
showTopBorder?: boolean
|
||||
pluginDetail?: PluginDetail
|
||||
}
|
||||
|
||||
export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
||||
showTopBorder = false,
|
||||
pluginDetail,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { subscriptions } = useSubscriptionList()
|
||||
@@ -41,6 +44,7 @@ export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
|
||||
<SubscriptionCard
|
||||
key={subscription.id}
|
||||
data={subscription}
|
||||
pluginDetail={pluginDetail}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
'use client'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiWebhookLine,
|
||||
} from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { DeleteConfirm } from './delete-confirm'
|
||||
import { EditModal } from './edit'
|
||||
|
||||
type Props = {
|
||||
data: TriggerSubscription
|
||||
pluginDetail?: PluginDetail
|
||||
}
|
||||
|
||||
const SubscriptionCard = ({ data }: Props) => {
|
||||
const SubscriptionCard = ({ data, pluginDetail }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowDeleteModal, {
|
||||
setTrue: showDeleteModal,
|
||||
setFalse: hideDeleteModal,
|
||||
}] = useBoolean(false)
|
||||
const [isShowEditModal, {
|
||||
setTrue: showEditModal,
|
||||
setFalse: hideEditModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -40,13 +48,21 @@ const SubscriptionCard = ({ data }: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='hidden items-center gap-1 group-hover:flex'>
|
||||
<ActionButton
|
||||
onClick={showEditModal}
|
||||
className='transition-colors hover:bg-state-base-hover'
|
||||
>
|
||||
<RiEditLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={showDeleteModal}
|
||||
className='subscription-delete-btn hidden transition-colors hover:bg-state-destructive-hover hover:text-text-destructive group-hover:block'
|
||||
className='subscription-delete-btn transition-colors hover:bg-state-destructive-hover hover:text-text-destructive'
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-1 flex items-center justify-between'>
|
||||
<Tooltip
|
||||
@@ -78,6 +94,14 @@ const SubscriptionCard = ({ data }: Props) => {
|
||||
workflowsInUse={data.workflows_in_use}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isShowEditModal && (
|
||||
<EditModal
|
||||
onClose={hideEditModal}
|
||||
subscription={data}
|
||||
pluginDetail={pluginDetail}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -237,32 +237,15 @@ type TriggerSubscriptionStructure = {
|
||||
name: string
|
||||
provider: string
|
||||
credential_type: TriggerCredentialTypeEnum
|
||||
credentials: TriggerSubCredentials
|
||||
credentials: Record<string, any>
|
||||
endpoint: string
|
||||
parameters: TriggerSubParameters
|
||||
properties: TriggerSubProperties
|
||||
parameters: Record<string, any>
|
||||
properties: Record<string, any>
|
||||
workflows_in_use: number
|
||||
}
|
||||
|
||||
export type TriggerSubscription = TriggerSubscriptionStructure
|
||||
|
||||
export type TriggerSubCredentials = {
|
||||
access_tokens: string
|
||||
}
|
||||
|
||||
export type TriggerSubParameters = {
|
||||
repository: string
|
||||
webhook_secret?: string
|
||||
}
|
||||
|
||||
export type TriggerSubProperties = {
|
||||
active: boolean
|
||||
events: string[]
|
||||
external_id: string
|
||||
repository: string
|
||||
webhook_secret?: string
|
||||
}
|
||||
|
||||
export type TriggerSubscriptionBuilder = TriggerSubscriptionStructure
|
||||
|
||||
// OAuth configuration types
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
useBuildTriggerSubscription,
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useUpdateTriggerSubscriptionBuilder,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
useVerifyAndUpdateTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
@@ -47,7 +47,7 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState
|
||||
|
||||
const createBuilder = useCreateTriggerSubscriptionBuilder()
|
||||
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
|
||||
const verifyBuilder = useVerifyTriggerSubscriptionBuilder()
|
||||
const verifyBuilder = useVerifyAndUpdateTriggerSubscriptionBuilder()
|
||||
const buildSubscription = useBuildTriggerSubscription()
|
||||
|
||||
const startAuth = useCallback(async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ const translation = {
|
||||
cancel: 'Cancel',
|
||||
clear: 'Clear',
|
||||
save: 'Save',
|
||||
saving: 'Saving...',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
deleteConfirmTitle: 'Delete?',
|
||||
|
||||
@@ -30,6 +30,11 @@ const translation = {
|
||||
unauthorized: 'Manual',
|
||||
},
|
||||
actions: {
|
||||
edit: {
|
||||
title: 'Edit Subscription',
|
||||
success: 'Subscription updated successfully',
|
||||
error: 'Failed to update subscription',
|
||||
},
|
||||
delete: 'Delete',
|
||||
deleteConfirm: {
|
||||
title: 'Delete {{name}}?',
|
||||
|
||||
@@ -162,9 +162,9 @@ export const useUpdateTriggerSubscriptionBuilder = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useVerifyTriggerSubscriptionBuilder = () => {
|
||||
export const useVerifyAndUpdateTriggerSubscriptionBuilder = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'verify-subscription-builder'],
|
||||
mutationKey: [NAME_SPACE, 'verify-and-update-subscription-builder'],
|
||||
mutationFn: (payload: {
|
||||
provider: string
|
||||
subscriptionBuilderId: string
|
||||
@@ -172,7 +172,25 @@ export const useVerifyTriggerSubscriptionBuilder = () => {
|
||||
}) => {
|
||||
const { provider, subscriptionBuilderId, ...body } = payload
|
||||
return post<{ verified: boolean }>(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify-and-update/${subscriptionBuilderId}`,
|
||||
{ body },
|
||||
{ silent: true },
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useVerifyTriggerSubscription = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'verify-subscription'],
|
||||
mutationFn: (payload: {
|
||||
provider: string;
|
||||
subscriptionId: string;
|
||||
credentials?: Record<string, any>;
|
||||
}) => {
|
||||
const { provider, subscriptionId, ...body } = payload
|
||||
return post<{ verified: boolean }>(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/verify/${subscriptionId}`,
|
||||
{ body },
|
||||
{ silent: true },
|
||||
)
|
||||
@@ -211,6 +229,26 @@ export const useDeleteTriggerSubscription = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export type UpdateTriggerSubscriptionPayload = {
|
||||
subscriptionId: string
|
||||
name?: string
|
||||
properties?: Record<string, any>
|
||||
parameters?: Record<string, any>
|
||||
}
|
||||
|
||||
export const useUpdateTriggerSubscription = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'update-subscription'],
|
||||
mutationFn: (payload: UpdateTriggerSubscriptionPayload) => {
|
||||
const { subscriptionId, ...body } = payload
|
||||
return post<{ result: string; id: string }>(
|
||||
`/workspaces/current/trigger-provider/${subscriptionId}/subscriptions/update`,
|
||||
{ body },
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useTriggerSubscriptionBuilderLogs = (
|
||||
provider: string,
|
||||
subscriptionBuilderId: string,
|
||||
|
||||
Reference in New Issue
Block a user