mirror of
https://github.com/langgenius/dify.git
synced 2026-01-06 06:26:00 +00:00
feat: oauth config init
This commit is contained in:
@@ -15,6 +15,17 @@ import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
|
||||
const getInputType = (type: FormTypeEnum) => {
|
||||
switch (type) {
|
||||
case FormTypeEnum.secretInput:
|
||||
return 'password'
|
||||
case FormTypeEnum.textNumber:
|
||||
return 'number'
|
||||
default:
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
|
||||
export type BaseFieldProps = {
|
||||
fieldClassName?: string
|
||||
labelClassName?: string
|
||||
@@ -24,6 +35,7 @@ export type BaseFieldProps = {
|
||||
field: AnyFieldApi
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const BaseField = ({
|
||||
fieldClassName,
|
||||
labelClassName,
|
||||
@@ -42,19 +54,19 @@ const BaseField = ({
|
||||
labelClassName: formLabelClassName,
|
||||
show_on = [],
|
||||
disabled: formSchemaDisabled,
|
||||
showRadioUI,
|
||||
type: formItemType,
|
||||
} = formSchema
|
||||
const disabled = propsDisabled || formSchemaDisabled
|
||||
|
||||
const memorizedLabel = useMemo(() => {
|
||||
if (isValidElement(label))
|
||||
return label
|
||||
|
||||
if (typeof label === 'string')
|
||||
if (isValidElement(label) || typeof label === 'string')
|
||||
return label
|
||||
|
||||
if (typeof label === 'object' && label !== null)
|
||||
return renderI18nObject(label as Record<string, string>)
|
||||
}, [label, renderI18nObject])
|
||||
|
||||
const memorizedPlaceholder = useMemo(() => {
|
||||
if (typeof placeholder === 'string')
|
||||
return placeholder
|
||||
@@ -62,25 +74,36 @@ const BaseField = ({
|
||||
if (typeof placeholder === 'object' && placeholder !== null)
|
||||
return renderI18nObject(placeholder as Record<string, string>)
|
||||
}, [placeholder, renderI18nObject])
|
||||
const optionValues = useStore(field.form.store, (s) => {
|
||||
|
||||
const watchedVariables = useMemo(() => {
|
||||
const variables = new Set<string>()
|
||||
|
||||
for (const option of options || []) {
|
||||
for (const condition of option.show_on || [])
|
||||
variables.add(condition.variable)
|
||||
}
|
||||
|
||||
for (const condition of show_on || [])
|
||||
variables.add(condition.variable)
|
||||
|
||||
return Array.from(variables)
|
||||
}, [options, show_on])
|
||||
|
||||
const watchedValues = useStore(field.form.store, (s) => {
|
||||
const result: Record<string, any> = {}
|
||||
options?.forEach((option) => {
|
||||
if (option.show_on?.length) {
|
||||
option.show_on.forEach((condition) => {
|
||||
result[condition.variable] = s.values[condition.variable]
|
||||
})
|
||||
}
|
||||
})
|
||||
for (const variable of watchedVariables)
|
||||
result[variable] = s.values[variable]
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const memorizedOptions = useMemo(() => {
|
||||
return options?.filter((option) => {
|
||||
if (!option.show_on || option.show_on.length === 0)
|
||||
if (!option.show_on?.length)
|
||||
return true
|
||||
|
||||
return option.show_on.every((condition) => {
|
||||
const conditionValue = optionValues[condition.variable]
|
||||
return conditionValue === condition.value
|
||||
return watchedValues[condition.variable] === condition.value
|
||||
})
|
||||
}).map((option) => {
|
||||
return {
|
||||
@@ -88,20 +111,15 @@ const BaseField = ({
|
||||
value: option.value,
|
||||
}
|
||||
}) || []
|
||||
}, [options, renderI18nObject, optionValues])
|
||||
}, [options, renderI18nObject, watchedValues])
|
||||
|
||||
const value = useStore(field.form.store, s => s.values[field.name])
|
||||
const values = useStore(field.form.store, (s) => {
|
||||
return show_on.reduce((acc, condition) => {
|
||||
acc[condition.variable] = s.values[condition.variable]
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
})
|
||||
|
||||
const show = useMemo(() => {
|
||||
return show_on.every((condition) => {
|
||||
const conditionValue = values[condition.variable]
|
||||
return conditionValue === condition.value
|
||||
return watchedValues[condition.variable] === condition.value
|
||||
})
|
||||
}, [values, show_on])
|
||||
}, [watchedValues, show_on])
|
||||
|
||||
const booleanRadioValue = useMemo(() => {
|
||||
if (value === null || value === undefined)
|
||||
@@ -124,7 +142,7 @@ const BaseField = ({
|
||||
</div>
|
||||
<div className={cn(inputContainerClassName)}>
|
||||
{
|
||||
formSchema.type === FormTypeEnum.textInput && (
|
||||
[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
@@ -134,41 +152,12 @@ const BaseField = ({
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
type={getInputType(formItemType)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.secretInput && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='password'
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.textNumber && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='number'
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.select && (
|
||||
formItemType === FormTypeEnum.select && (
|
||||
<PureSelect
|
||||
value={value}
|
||||
onChange={v => field.handleChange(v)}
|
||||
@@ -180,7 +169,7 @@ const BaseField = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.radio && (
|
||||
formItemType === FormTypeEnum.radio && (
|
||||
<div className={cn(
|
||||
memorizedOptions.length < 3 ? 'flex items-center space-x-2' : 'space-y-2',
|
||||
)}>
|
||||
@@ -189,21 +178,14 @@ const BaseField = ({
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
|
||||
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
|
||||
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
inputClassName,
|
||||
)}
|
||||
onClick={() => !disabled && field.handleChange(option.value)}
|
||||
>
|
||||
{
|
||||
formSchema.showRadioUI && (
|
||||
<RadioE
|
||||
className='mr-2'
|
||||
isChecked={value === option.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showRadioUI && <RadioE isChecked={value === option.value} />}
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
@@ -212,13 +194,13 @@ const BaseField = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.boolean && (
|
||||
formItemType === FormTypeEnum.boolean && (
|
||||
<Radio.Group
|
||||
className='flex w-fit items-center'
|
||||
className='flex w-fit items-center gap-1'
|
||||
value={booleanRadioValue}
|
||||
onChange={val => field.handleChange(val === 1)}
|
||||
>
|
||||
<Radio value={1} className='!mr-1'>True</Radio>
|
||||
<Radio value={1}>True</Radio>
|
||||
<Radio value={0}>False</Radio>
|
||||
</Radio.Group>
|
||||
)
|
||||
@@ -233,9 +215,7 @@ const BaseField = ({
|
||||
<span className='break-all'>
|
||||
{renderI18nObject(formSchema?.help as any)}
|
||||
</span>
|
||||
{
|
||||
<RiExternalLinkLine className='ml-1 h-3 w-3' />
|
||||
}
|
||||
<RiExternalLinkLine className='ml-1 h-3 w-3' />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export type Item = {
|
||||
name: string
|
||||
isGroup?: boolean
|
||||
disabled?: boolean
|
||||
extra?: React.ReactNode
|
||||
} & Record<string, any>
|
||||
|
||||
export type ISelectProps = {
|
||||
@@ -402,6 +403,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
|
||||
{!hideChecked && item.value === value && (
|
||||
<RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
|
||||
)}
|
||||
{item.extra}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ export enum AuthCategory {
|
||||
tool = 'tool',
|
||||
datasource = 'datasource',
|
||||
model = 'model',
|
||||
trigger = 'trigger',
|
||||
}
|
||||
|
||||
export type PluginPayload = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import DetailHeader from './detail-header'
|
||||
import EndpointList from './endpoint-list'
|
||||
@@ -11,6 +11,7 @@ import { TriggerEventsList } from './trigger-events-list'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { type PluginDetail, PluginType } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { usePluginStore } from './store'
|
||||
|
||||
type Props = {
|
||||
detail?: PluginDetail
|
||||
@@ -28,6 +29,12 @@ const PluginDetailPanel: FC<Props> = ({
|
||||
onHide()
|
||||
onUpdate()
|
||||
}
|
||||
const { setDetail } = usePluginStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (detail)
|
||||
setDetail(detail)
|
||||
}, [detail])
|
||||
|
||||
if (!detail)
|
||||
return null
|
||||
@@ -52,8 +59,8 @@ const PluginDetailPanel: FC<Props> = ({
|
||||
<div className='grow overflow-y-auto'>
|
||||
{detail.declaration.category === PluginType.trigger && (
|
||||
<>
|
||||
<SubscriptionList detail={detail} />
|
||||
<TriggerEventsList detail={detail} />
|
||||
<SubscriptionList />
|
||||
<TriggerEventsList />
|
||||
</>
|
||||
)}
|
||||
{!!detail.declaration.tool && <ActionList detail={detail} />}
|
||||
|
||||
22
web/app/components/plugins/plugin-detail-panel/store.ts
Normal file
22
web/app/components/plugins/plugin-detail-panel/store.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { create } from 'zustand'
|
||||
import type { PluginDetail } from '../types'
|
||||
|
||||
type Shape = {
|
||||
detail: PluginDetail | undefined
|
||||
setDetail: (detail: PluginDetail) => void
|
||||
}
|
||||
|
||||
export const usePluginStore = create<Shape>(set => ({
|
||||
detail: undefined,
|
||||
setDetail: (detail: PluginDetail) => set({ detail }),
|
||||
}))
|
||||
|
||||
type ShapeSubscription = {
|
||||
refresh?: () => void
|
||||
setRefresh: (refresh: () => void) => void
|
||||
}
|
||||
|
||||
export const usePluginSubscriptionStore = create<ShapeSubscription>(set => ({
|
||||
refresh: undefined,
|
||||
setRefresh: (refresh: () => void) => set({ refresh }),
|
||||
}))
|
||||
@@ -17,11 +17,10 @@ import {
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { usePluginStore } from '../../store'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
@@ -31,9 +30,9 @@ enum ApiKeyStep {
|
||||
Configuration = 'configuration',
|
||||
}
|
||||
|
||||
const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
export const ApiKeyCreateModal = ({ onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
// State
|
||||
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(ApiKeyStep.Verify)
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
@@ -50,9 +49,9 @@ const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
// Get provider name and schemas
|
||||
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
|
||||
const credentialsSchema = pluginDetail.declaration.trigger?.credentials_schema || []
|
||||
const parametersSchema = pluginDetail.declaration.trigger?.subscription_schema?.parameters_schema || []
|
||||
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
const credentialsSchema = detail?.declaration.trigger?.credentials_schema || []
|
||||
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || []
|
||||
|
||||
const handleVerify = () => {
|
||||
const credentialsFormValues = credentialsFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
@@ -310,5 +309,3 @@ const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiKeyAddModal
|
||||
@@ -0,0 +1,296 @@
|
||||
'use client'
|
||||
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { SupportedCreationMethods } from '@/app/components/plugins/types'
|
||||
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useTriggerSubscriptionBuilderLogs,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore } from '../../store'
|
||||
import LogViewer from '../log-viewer'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void
|
||||
createType: SupportedCreationMethods
|
||||
}
|
||||
|
||||
enum ApiKeyStep {
|
||||
Verify = 'verify',
|
||||
Configuration = 'configuration',
|
||||
}
|
||||
|
||||
const StatusStep = ({ isActive, text }: { isActive: boolean, text: string }) => {
|
||||
return <div className={`system-2xs-semibold-uppercase ${isActive
|
||||
? 'text-state-accent-solid'
|
||||
: 'text-text-tertiary'}`}>
|
||||
{text}
|
||||
</div>
|
||||
}
|
||||
|
||||
const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => {
|
||||
const { t } = useTranslation()
|
||||
return <div className='mb-2 flex w-1/3 items-center gap-2'>
|
||||
<StatusStep isActive={currentStep === ApiKeyStep.Verify} text={t('pluginTrigger.modal.steps.verify')} />
|
||||
<div className='h-px w-3 bg-divider-subtle'></div>
|
||||
<StatusStep isActive={currentStep === ApiKeyStep.Configuration} text={t('pluginTrigger.modal.steps.configuration')} />
|
||||
</div>
|
||||
}
|
||||
|
||||
export const CommonCreateModal = ({ onClose, createType }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
|
||||
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
|
||||
const [verificationError, setVerificationError] = useState<string>('')
|
||||
|
||||
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
const propertiesSchema = detail?.declaration.trigger.subscription_schema.properties_schema || [] // manual
|
||||
const propertiesFormRef = React.useRef<FormRefObject>(null)
|
||||
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || [] // apikey and oauth
|
||||
const parametersFormRef = React.useRef<FormRefObject>(null)
|
||||
const credentialsSchema = detail?.declaration.trigger?.credentials_schema || []
|
||||
const credentialsFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
const { data: logData } = useTriggerSubscriptionBuilderLogs(
|
||||
providerName,
|
||||
subscriptionBuilder?.id || '',
|
||||
{
|
||||
enabled: createType === SupportedCreationMethods.MANUAL && !!subscriptionBuilder?.id,
|
||||
refetchInterval: 3000,
|
||||
},
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscriptionBuilder) {
|
||||
createBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
credential_type: TriggerCredentialTypeEnum.Unauthorized,
|
||||
},
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
const builder = response.subscription_builder
|
||||
setSubscriptionBuilder(builder)
|
||||
},
|
||||
onError: (error) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.errors.createFailed'),
|
||||
})
|
||||
console.error('Failed to create subscription builder:', error)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}, [createBuilder, providerName, subscriptionBuilder, t])
|
||||
|
||||
const handleVerify = () => {
|
||||
const credentialsFormValues = credentialsFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
const credentials = credentialsFormValues.values
|
||||
|
||||
if (!Object.keys(credentials).length) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Please fill in all required credentials',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setVerificationError('')
|
||||
|
||||
verifyCredentials(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder?.id || '',
|
||||
credentials,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.apiKey.verify.success'),
|
||||
})
|
||||
setCurrentStep(ApiKeyStep.Configuration)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setVerificationError(error?.message || t('pluginTrigger.modal.apiKey.verify.error'))
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!subscriptionName.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.form.subscriptionName.required'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!subscriptionBuilder)
|
||||
return
|
||||
|
||||
const formValues = propertiesFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
if (!formValues.isCheckValidated) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.form.properties.required'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
buildSubscription(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
params: {
|
||||
name: subscriptionName,
|
||||
properties: formValues.values,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: 'Subscription created successfully',
|
||||
})
|
||||
// onSuccess()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.createFailed'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (currentStep === ApiKeyStep.Verify)
|
||||
handleVerify()
|
||||
else
|
||||
handleCreate()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t(`pluginTrigger.modal.${createType === SupportedCreationMethods.APIKEY ? 'apiKey' : createType.toLowerCase()}.title`)}
|
||||
confirmButtonText={
|
||||
currentStep === ApiKeyStep.Verify
|
||||
? isVerifyingCredentials ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')
|
||||
: isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')
|
||||
}
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
onConfirm={handleConfirm}
|
||||
>
|
||||
{createType === SupportedCreationMethods.APIKEY && <MultiSteps currentStep={currentStep} />}
|
||||
{currentStep === ApiKeyStep.Verify && (
|
||||
<>
|
||||
{credentialsSchema.length > 0 && (
|
||||
<div className='mb-4'>
|
||||
<BaseForm
|
||||
formSchemas={credentialsSchema}
|
||||
ref={credentialsFormRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{verificationError && (
|
||||
<div className='bg-state-destructive-bg mb-4 rounded-lg border border-state-destructive-border p-3'>
|
||||
<div className='text-state-destructive-text system-xs-medium'>
|
||||
{verificationError}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentStep === ApiKeyStep.Configuration && <div className='max-h-[70vh] overflow-y-auto'>
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.subscriptionName.label')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => setSubscriptionName(e.target.value)}
|
||||
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.label')}
|
||||
</label>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
value={subscriptionBuilder?.endpoint}
|
||||
readOnly
|
||||
className='pr-12'
|
||||
placeholder={t('pluginTrigger.modal.form.callbackUrl.placeholder')}
|
||||
/>
|
||||
<CopyFeedbackNew className='absolute right-1 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary' content={subscriptionBuilder?.endpoint || ''} />
|
||||
</div>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.description')}
|
||||
</div>
|
||||
</div>
|
||||
{createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && (
|
||||
<BaseForm
|
||||
formSchemas={parametersSchema}
|
||||
ref={parametersFormRef}
|
||||
/>
|
||||
)}
|
||||
{createType === SupportedCreationMethods.MANUAL && <>
|
||||
{propertiesSchema.length > 0 && (
|
||||
<div className='mb-6'>
|
||||
<BaseForm
|
||||
formSchemas={propertiesSchema}
|
||||
ref={propertiesFormRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='mb-6'>
|
||||
<div className='mb-3 flex items-center gap-2'>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>
|
||||
REQUESTS HISTORY
|
||||
</div>
|
||||
<div className='h-px flex-1 bg-gradient-to-r from-divider-regular to-transparent' />
|
||||
</div>
|
||||
|
||||
<div className='mb-1 flex items-center justify-center gap-1 rounded-lg bg-background-section p-3'>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
<RiLoader2Line className='h-full w-full animate-spin' />
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
Awaiting request from {detail?.declaration.name}...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LogViewer logs={logData?.logs || []} />
|
||||
</div>
|
||||
</>}
|
||||
|
||||
</div>}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import { ActionButton } from '@/app/components/base/action-button'
|
||||
import { Button } from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
import { useInitiateTriggerOAuth, useTriggerOAuthConfig, useTriggerProviderInfo } from '@/service/use-triggers'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiAddLine, RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SupportedCreationMethods } from '../../../types'
|
||||
import { usePluginStore } from '../../store'
|
||||
import { CommonCreateModal } from './common-modal'
|
||||
import { OAuthClientSettingsModal } from './oauth-client'
|
||||
|
||||
export const CreateModal = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
// onClose={onClose}
|
||||
className='!max-w-[520px] p-6'
|
||||
wrapperClassName='!z-[1002]'
|
||||
>
|
||||
<div className='flex items-center justify-between pb-3'>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.title')}
|
||||
</h3>
|
||||
<ActionButton
|
||||
// onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export enum CreateButtonType {
|
||||
FULL_BUTTON = 'full-button',
|
||||
ICON_BUTTON = 'icon-button',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
buttonType?: CreateButtonType
|
||||
}
|
||||
|
||||
export const DEFAULT_METHOD = 'default'
|
||||
|
||||
/**
|
||||
* 区分创建订阅的授权方式有几种
|
||||
* 1. 只有一种授权方式
|
||||
* - 按钮直接显示授权方式,点击按钮展示创建订阅弹窗
|
||||
* 2. 有多种授权方式
|
||||
* - 下拉框显示授权方式,点击按钮展示下拉框,点击选项展示创建订阅弹窗
|
||||
* 有订阅与无订阅时,按钮形态不同
|
||||
* oauth 的授权类型:
|
||||
* - 是否配置 client_id 和 client_secret
|
||||
* - 未配置则点击按钮去配置
|
||||
* - 已配置则点击按钮去创建
|
||||
* - 固定展示设置按钮
|
||||
*/
|
||||
export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BUTTON }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedCreateType, setSelectedCreateType] = useState<SupportedCreationMethods | null>(null)
|
||||
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const provider = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
|
||||
const { data: providerInfo } = useTriggerProviderInfo(provider, !!detail?.plugin_id && !!detail?.declaration.name)
|
||||
const supportedMethods = providerInfo?.supported_creation_methods || []
|
||||
const { data: oauthConfig } = useTriggerOAuthConfig(provider, supportedMethods.includes(SupportedCreationMethods.OAUTH))
|
||||
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
||||
|
||||
const methodType = supportedMethods.length === 1 ? supportedMethods[0] : DEFAULT_METHOD
|
||||
|
||||
const [isShowClientSettingsModal, {
|
||||
setTrue: showClientSettingsModal,
|
||||
setFalse: hideClientSettingsModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const buttonTextMap = useMemo(() => {
|
||||
return {
|
||||
[SupportedCreationMethods.OAUTH]: t('pluginTrigger.subscription.createButton.oauth'),
|
||||
[SupportedCreationMethods.APIKEY]: t('pluginTrigger.subscription.createButton.apiKey'),
|
||||
[SupportedCreationMethods.MANUAL]: t('pluginTrigger.subscription.createButton.manual'),
|
||||
[DEFAULT_METHOD]: t('pluginTrigger.subscription.empty.button'),
|
||||
}
|
||||
}, [t])
|
||||
|
||||
const onClickClientSettings = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
showClientSettingsModal()
|
||||
}
|
||||
|
||||
const allOptions = [
|
||||
{
|
||||
value: SupportedCreationMethods.OAUTH,
|
||||
name: t('pluginTrigger.subscription.addType.options.oauth.title'),
|
||||
extra: <ActionButton onClick={onClickClientSettings}><RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /></ActionButton>,
|
||||
show: supportedMethods.includes(SupportedCreationMethods.OAUTH),
|
||||
},
|
||||
{
|
||||
value: SupportedCreationMethods.APIKEY,
|
||||
name: t('pluginTrigger.subscription.addType.options.apiKey.title'),
|
||||
show: supportedMethods.includes(SupportedCreationMethods.APIKEY),
|
||||
},
|
||||
{
|
||||
value: SupportedCreationMethods.MANUAL,
|
||||
name: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
|
||||
tooltip: <Tooltip popupContent={t('pluginTrigger.subscription.addType.options.manual.tip')} />,
|
||||
show: supportedMethods.includes(SupportedCreationMethods.MANUAL),
|
||||
},
|
||||
]
|
||||
|
||||
const onChooseCreateType = (type: SupportedCreationMethods) => {
|
||||
if (type === SupportedCreationMethods.OAUTH) {
|
||||
if (oauthConfig?.configured) {
|
||||
initiateOAuth(provider, {
|
||||
onSuccess: (response) => {
|
||||
openOAuthPopup(response.authorization_url, (callbackData) => {
|
||||
if (callbackData) {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.authorized'),
|
||||
})
|
||||
setSelectedCreateType(SupportedCreationMethods.OAUTH)
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
showClientSettingsModal()
|
||||
}
|
||||
}
|
||||
else {
|
||||
setSelectedCreateType(type)
|
||||
}
|
||||
}
|
||||
|
||||
const onClickCreate = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (methodType === DEFAULT_METHOD)
|
||||
return
|
||||
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
onChooseCreateType(methodType)
|
||||
}
|
||||
|
||||
if (!supportedMethods.length)
|
||||
return null
|
||||
|
||||
return <>
|
||||
<PortalSelect
|
||||
readonly={methodType !== DEFAULT_METHOD}
|
||||
renderTrigger={() => {
|
||||
return buttonType === CreateButtonType.FULL_BUTTON ? (
|
||||
<Button
|
||||
variant='primary'
|
||||
size='medium'
|
||||
className='w-full'
|
||||
onClick={onClickCreate}
|
||||
>
|
||||
<RiAddLine className='mr-2 h-4 w-4' />
|
||||
{buttonTextMap[methodType]}
|
||||
{methodType === SupportedCreationMethods.OAUTH
|
||||
&& <ActionButton onClick={onClickClientSettings}>
|
||||
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
}
|
||||
</Button>
|
||||
) : <ActionButton onClick={onClickCreate}>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
}}
|
||||
triggerClassName='h-8'
|
||||
popupClassName={cn('z-[1000]')}
|
||||
popupInnerClassName={cn('w-[354px]')}
|
||||
value={methodType}
|
||||
items={allOptions.filter(option => option.show)}
|
||||
onSelect={item => onChooseCreateType(item.value as any)}
|
||||
/>
|
||||
{selectedCreateType && (
|
||||
<CommonCreateModal
|
||||
createType={selectedCreateType}
|
||||
onClose={() => setSelectedCreateType(null)}
|
||||
/>
|
||||
)}
|
||||
{isShowClientSettingsModal && (
|
||||
<OAuthClientSettingsModal
|
||||
oauthConfig={oauthConfig}
|
||||
onClose={hideClientSettingsModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -14,23 +14,23 @@ import {
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useTriggerSubscriptionBuilderLogs,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import LogViewer from './log-viewer'
|
||||
import LogViewer from '../log-viewer'
|
||||
import { usePluginStore } from '../../store'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
export const ManualCreateModal = ({ onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
|
||||
@@ -38,8 +38,8 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
|
||||
const propertiesSchema = pluginDetail.declaration.trigger.subscription_schema.properties_schema || []
|
||||
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
const propertiesSchema = detail?.declaration.trigger.subscription_schema.properties_schema || []
|
||||
const propertiesFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
const { data: logData } = useTriggerSubscriptionBuilderLogs(
|
||||
@@ -193,7 +193,7 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
<RiLoader2Line className='h-full w-full animate-spin' />
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
Awaiting request from {pluginDetail.declaration.name}...
|
||||
Awaiting request from {detail?.declaration.name}...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -217,5 +217,3 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManualAddModal
|
||||
@@ -0,0 +1,236 @@
|
||||
'use client'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Form from '@/app/components/base/form/form-scenarios/auth'
|
||||
import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { TriggerOAuthClientParams, TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import {
|
||||
useConfigureTriggerOAuth,
|
||||
useDeleteTriggerOAuth,
|
||||
useInitiateTriggerOAuth,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import {
|
||||
RiClipboardLine,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore } from '../../store'
|
||||
|
||||
type Props = {
|
||||
oauthConfig?: TriggerOAuthConfig
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
enum AuthorizationStatusEnum {
|
||||
Pending = 'pending',
|
||||
Success = 'success',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
enum ClientTypeEnum {
|
||||
Default = 'default',
|
||||
Custom = 'custom',
|
||||
}
|
||||
|
||||
export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const [authorizationUrl, setAuthorizationUrl] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
|
||||
const [authorizationStatus, setAuthorizationStatus] = useState<AuthorizationStatusEnum>()
|
||||
|
||||
const [clientType, setClientType] = useState<ClientTypeEnum>(oauthConfig?.custom_enabled ? ClientTypeEnum.Custom : ClientTypeEnum.Default)
|
||||
|
||||
const clientFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
const providerName = useMemo(() => !detail ? '' : `${detail?.plugin_id}/${detail?.declaration.name}`, [detail])
|
||||
const clientSchema = detail?.declaration.trigger?.oauth_schema?.client_schema || []
|
||||
|
||||
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
||||
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: configureOAuth } = useConfigureTriggerOAuth()
|
||||
const { mutate: deleteOAuth } = useDeleteTriggerOAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (providerName && oauthConfig?.params.client_id && oauthConfig?.params.client_secret) {
|
||||
initiateOAuth(providerName, {
|
||||
onSuccess: (response) => {
|
||||
setAuthorizationUrl(response.authorization_url)
|
||||
setSubscriptionBuilder(response.subscription_builder)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [initiateOAuth, providerName, t, oauthConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (providerName && subscriptionBuilder && authorizationStatus === AuthorizationStatusEnum.Pending) {
|
||||
const pollInterval = setInterval(() => {
|
||||
verifyBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAuthorizationStatus(AuthorizationStatusEnum.Success)
|
||||
// setCurrentStep(OAuthStepEnum.Configuration)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.authorization.authSuccess'),
|
||||
})
|
||||
clearInterval(pollInterval)
|
||||
},
|
||||
onError: () => {
|
||||
// Continue polling - auth might still be in progress
|
||||
},
|
||||
},
|
||||
)
|
||||
}, 3000)
|
||||
|
||||
return () => clearInterval(pollInterval)
|
||||
}
|
||||
}, [subscriptionBuilder, authorizationStatus, verifyBuilder, providerName, t])
|
||||
|
||||
const handleRemove = () => {
|
||||
deleteOAuth(providerName, {
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.configuration.success'),
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.oauth.configuration.failed'),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveOnly = () => {
|
||||
const clientParams = clientFormRef.current?.getFormValues({})?.values || {}
|
||||
if (clientParams.client_id === oauthConfig?.params.client_id)
|
||||
clientParams.client_id = '[__HIDDEN__]'
|
||||
|
||||
if (clientParams.client_secret === oauthConfig?.params.client_secret)
|
||||
clientParams.client_secret = '[__HIDDEN__]'
|
||||
|
||||
configureOAuth({
|
||||
provider: providerName,
|
||||
client_params: clientParams as TriggerOAuthClientParams,
|
||||
enabled: clientType === ClientTypeEnum.Custom,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.configuration.success'),
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.oauth.configuration.failed'),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveAuthorize = () => {
|
||||
handleSaveOnly()
|
||||
if (authorizationUrl) {
|
||||
setAuthorizationStatus(AuthorizationStatusEnum.Pending)
|
||||
// Open authorization URL in new window
|
||||
window.open(authorizationUrl, '_blank', 'width=500,height=600')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('pluginTrigger.modal.oauth.title')}
|
||||
confirmButtonText={t('plugin.auth.saveAndAuth')}
|
||||
cancelButtonText={t('plugin.auth.saveOnly')}
|
||||
extraButtonText={t('common.operation.cancel')}
|
||||
showExtraButton
|
||||
extraButtonVariant='secondary'
|
||||
onExtraButtonClick={onClose}
|
||||
onClose={onClose}
|
||||
onCancel={handleSaveOnly}
|
||||
onConfirm={handleSaveAuthorize}
|
||||
footerSlot={
|
||||
oauthConfig?.custom_enabled && oauthConfig?.params && (
|
||||
<div className='grow'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='text-components-button-destructive-secondary-text'
|
||||
// disabled={disabled || doingAction || !editValues}
|
||||
onClick={handleRemove}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className='system-sm-semibold mb-2 text-text-secondary'>OAuth Client</span>
|
||||
<div className='mb-4 flex w-full items-start justify-between gap-2'>
|
||||
{[ClientTypeEnum.Default, ClientTypeEnum.Custom].map(option => (
|
||||
<OptionCard
|
||||
key={option}
|
||||
title={option}
|
||||
onSelect={() => setClientType(option)}
|
||||
selected={clientType === option}
|
||||
className="flex-1"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{oauthConfig?.redirect_uri && (
|
||||
<div className='mb-4 flex items-start gap-3 rounded-xl bg-background-section-burn p-4'>
|
||||
<div className='rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg p-2 shadow-xs shadow-shadow-shadow-3'>
|
||||
<RiInformation2Fill className='h-5 w-5 shrink-0 text-text-accent' />
|
||||
</div>
|
||||
<div className='flex-1 text-text-secondary'>
|
||||
<div className='system-sm-regular whitespace-pre-wrap leading-4'>
|
||||
{t('pluginTrigger.modal.oauthRedirectInfo')}
|
||||
</div>
|
||||
<div className='system-sm-medium my-1.5 break-all leading-4'>
|
||||
{oauthConfig.redirect_uri}
|
||||
</div>
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(oauthConfig.redirect_uri)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.actionMsg.copySuccessfully'),
|
||||
})
|
||||
}}>
|
||||
<RiClipboardLine className='mr-1 h-[14px] w-[14px]' />
|
||||
{t('common.operation.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{clientType === ClientTypeEnum.Custom && clientSchema.length > 0 && (
|
||||
<Form
|
||||
formSchemas={clientSchema}
|
||||
ref={clientFormRef}
|
||||
defaultValues={oauthConfig?.params}
|
||||
/>
|
||||
)}
|
||||
</Modal >
|
||||
)
|
||||
}
|
||||
@@ -15,15 +15,14 @@ import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useInitiateTriggerOAuth,
|
||||
useTriggerOAuthConfig,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import type { TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import { usePluginStore } from '../../store'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
oauthConfig?: TriggerOAuthConfig
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
@@ -39,9 +38,9 @@ enum AuthorizationStatusEnum {
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
export const OAuthCreateModal = ({ oauthConfig, onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const [currentStep, setCurrentStep] = useState<OAuthStepEnum>(OAuthStepEnum.Setup)
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [authorizationUrl, setAuthorizationUrl] = useState('')
|
||||
@@ -51,16 +50,14 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const clientFormRef = React.useRef<FormRefObject>(null)
|
||||
const parametersFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
|
||||
const clientSchema = pluginDetail.declaration.trigger?.oauth_schema?.client_schema || []
|
||||
const parametersSchema = pluginDetail.declaration.trigger?.subscription_schema?.parameters_schema || []
|
||||
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
const clientSchema = detail?.declaration.trigger?.oauth_schema?.client_schema || []
|
||||
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || []
|
||||
|
||||
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
||||
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
const { data: oauthConfig } = useTriggerOAuthConfig(providerName)
|
||||
|
||||
useEffect(() => {
|
||||
initiateOAuth(providerName, {
|
||||
onSuccess: (response) => {
|
||||
@@ -290,5 +287,3 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default OAuthAddModal
|
||||
@@ -5,21 +5,19 @@ import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ActionButton } from '@/app/components/base/action-button'
|
||||
|
||||
enum SubscriptionAddTypeEnum {
|
||||
OAuth = 'oauth',
|
||||
APIKey = 'api-key',
|
||||
Manual = 'manual',
|
||||
}
|
||||
import { SupportedCreationMethods } from '../../../types'
|
||||
import type { TriggerOAuthConfig } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
type Props = {
|
||||
onSelect: (type: SubscriptionAddTypeEnum) => void
|
||||
onSelect: (type: SupportedCreationMethods) => void
|
||||
onClose: () => void
|
||||
position?: 'bottom' | 'right'
|
||||
className?: string
|
||||
supportedMethods: SupportedCreationMethods[]
|
||||
oauthConfig?: TriggerOAuthConfig
|
||||
}
|
||||
|
||||
const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom', className }: Props) => {
|
||||
export const CreateTypeDropdown = ({ onSelect, onClose, position = 'bottom', className, supportedMethods }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -37,24 +35,29 @@ const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom', className }:
|
||||
// todo: show client settings
|
||||
}
|
||||
|
||||
const options = [
|
||||
const allOptions = [
|
||||
{
|
||||
key: SubscriptionAddTypeEnum.OAuth,
|
||||
key: SupportedCreationMethods.OAUTH,
|
||||
title: t('pluginTrigger.subscription.addType.options.oauth.title'),
|
||||
extraContent: <ActionButton onClick={onClickClientSettings}><RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /></ActionButton>,
|
||||
show: supportedMethods.includes(SupportedCreationMethods.OAUTH),
|
||||
},
|
||||
{
|
||||
key: SubscriptionAddTypeEnum.APIKey,
|
||||
key: SupportedCreationMethods.APIKEY,
|
||||
title: t('pluginTrigger.subscription.addType.options.apiKey.title'),
|
||||
show: supportedMethods.includes(SupportedCreationMethods.APIKEY),
|
||||
},
|
||||
{
|
||||
key: SubscriptionAddTypeEnum.Manual,
|
||||
key: SupportedCreationMethods.MANUAL,
|
||||
title: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
|
||||
tooltip: t('pluginTrigger.subscription.addType.options.manual.tip'),
|
||||
show: supportedMethods.includes(SupportedCreationMethods.MANUAL),
|
||||
},
|
||||
]
|
||||
|
||||
const handleOptionClick = (type: SubscriptionAddTypeEnum) => {
|
||||
const options = allOptions.filter(option => option.show)
|
||||
|
||||
const handleOptionClick = (type: SupportedCreationMethods) => {
|
||||
onSelect(type)
|
||||
}
|
||||
|
||||
@@ -100,5 +103,3 @@ const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom', className }:
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddTypeDropdown
|
||||
@@ -1,56 +1,27 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import SubscriptionCard from './subscription-card'
|
||||
import SubscriptionAddModal from './subscription-add-modal'
|
||||
import AddTypeDropdown from './add-type-dropdown'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginStore, usePluginSubscriptionStore } from '../store'
|
||||
import { CreateButtonType, CreateSubscriptionButton } from './create'
|
||||
import SubscriptionCard from './subscription-card'
|
||||
|
||||
type Props = {
|
||||
detail: PluginDetail
|
||||
}
|
||||
|
||||
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
export const SubscriptionList = ({ detail }: Props) => {
|
||||
export const SubscriptionList = () => {
|
||||
const { t } = useTranslation()
|
||||
const showTopBorder = detail.declaration.tool || detail.declaration.endpoint
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
|
||||
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(`${detail.plugin_id}/${detail.declaration.name}`)
|
||||
const showTopBorder = detail?.declaration.tool || detail?.declaration.endpoint
|
||||
const provider = `${detail?.plugin_id}/${detail?.declaration.name}`
|
||||
|
||||
const [isShowAddModal, {
|
||||
setTrue: showAddModal,
|
||||
setFalse: hideAddModal,
|
||||
}] = useBoolean(false)
|
||||
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(provider, !!detail?.plugin_id && !!detail?.declaration.name)
|
||||
|
||||
const [selectedAddType, setSelectedAddType] = React.useState<SubscriptionAddType | null>(null)
|
||||
const { setRefresh } = usePluginSubscriptionStore()
|
||||
|
||||
const [isShowAddDropdown, {
|
||||
setTrue: showAddDropdown,
|
||||
setFalse: hideAddDropdown,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleAddTypeSelect = (type: SubscriptionAddType) => {
|
||||
setSelectedAddType(type)
|
||||
hideAddDropdown()
|
||||
showAddModal()
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
hideAddModal()
|
||||
setSelectedAddType(null)
|
||||
}
|
||||
|
||||
const handleRefreshList = () => {
|
||||
refetch()
|
||||
}
|
||||
useEffect(() => {
|
||||
if (refetch)
|
||||
setRefresh(refetch)
|
||||
}, [refetch])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -66,64 +37,28 @@ export const SubscriptionList = ({ detail }: Props) => {
|
||||
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
{!hasSubscriptions ? (
|
||||
<div className='relative w-full'>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='medium'
|
||||
className='w-full'
|
||||
onClick={showAddDropdown}
|
||||
>
|
||||
<RiAddLine className='mr-2 h-4 w-4' />
|
||||
{t('pluginTrigger.subscription.empty.button')}
|
||||
</Button>
|
||||
{isShowAddDropdown && (
|
||||
<AddTypeDropdown
|
||||
onSelect={handleAddTypeSelect}
|
||||
onClose={hideAddDropdown}
|
||||
<div className='relative mb-3 flex items-center justify-between'>
|
||||
{
|
||||
hasSubscriptions
|
||||
&& <div className='flex items-center gap-1'>
|
||||
<span className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
|
||||
</span>
|
||||
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
|
||||
</div>
|
||||
}
|
||||
<CreateSubscriptionButton buttonType={hasSubscriptions ? CreateButtonType.ICON_BUTTON : CreateButtonType.FULL_BUTTON} />
|
||||
</div>
|
||||
|
||||
{hasSubscriptions
|
||||
&& <div className='flex flex-col gap-1'>
|
||||
{subscriptions?.map(subscription => (
|
||||
<SubscriptionCard
|
||||
key={subscription.id}
|
||||
data={subscription}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase relative mb-3 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<span className='system-sm-semibold text-text-secondary'>
|
||||
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
|
||||
</span>
|
||||
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
|
||||
</div>
|
||||
<ActionButton onClick={showAddDropdown}>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
{isShowAddDropdown && (
|
||||
<AddTypeDropdown
|
||||
onSelect={handleAddTypeSelect}
|
||||
onClose={hideAddDropdown}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-1'>
|
||||
{subscriptions?.map(subscription => (
|
||||
<SubscriptionCard
|
||||
key={subscription.id}
|
||||
data={subscription}
|
||||
onRefresh={handleRefreshList}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isShowAddModal && selectedAddType && (
|
||||
<SubscriptionAddModal
|
||||
type={selectedAddType}
|
||||
pluginDetail={detail}
|
||||
onClose={handleModalClose}
|
||||
onSuccess={handleRefreshList}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
// import { useTranslation } from 'react-i18next'
|
||||
// import Modal from '@/app/components/base/modal'
|
||||
import ManualAddModal from './manual-add-modal'
|
||||
import ApiKeyAddModal from './api-key-add-modal'
|
||||
import OAuthAddModal from './oauth-add-modal'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
|
||||
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
type Props = {
|
||||
type: SubscriptionAddType
|
||||
pluginDetail: PluginDetail
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
const SubscriptionAddModal = ({ type, pluginDetail, onClose, onSuccess }: Props) => {
|
||||
// const { t } = useTranslation()
|
||||
|
||||
const renderModalContent = () => {
|
||||
switch (type) {
|
||||
case 'manual':
|
||||
return (
|
||||
<ManualAddModal
|
||||
pluginDetail={pluginDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)
|
||||
case 'api-key':
|
||||
return (
|
||||
<ApiKeyAddModal
|
||||
pluginDetail={pluginDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)
|
||||
case 'oauth':
|
||||
return (
|
||||
<OAuthAddModal
|
||||
pluginDetail={pluginDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return renderModalContent()
|
||||
}
|
||||
|
||||
export default SubscriptionAddModal
|
||||
@@ -1,30 +1,30 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiWebhookLine,
|
||||
} from '@remixicon/react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePluginSubscriptionStore } from '../store'
|
||||
|
||||
type Props = {
|
||||
data: TriggerSubscription
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
const SubscriptionCard = ({ data, onRefresh }: Props) => {
|
||||
const SubscriptionCard = ({ data }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowDeleteModal, {
|
||||
setTrue: showDeleteModal,
|
||||
setFalse: hideDeleteModal,
|
||||
}] = useBoolean(false)
|
||||
const { refresh } = usePluginSubscriptionStore()
|
||||
|
||||
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
|
||||
|
||||
@@ -35,7 +35,7 @@ const SubscriptionCard = ({ data, onRefresh }: Props) => {
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title'),
|
||||
})
|
||||
onRefresh()
|
||||
refresh?.()
|
||||
hideDeleteModal()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ToolItem from '@/app/components/tools/provider/tool-item'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { usePluginStore } from './store'
|
||||
|
||||
type Props = {
|
||||
detail: PluginDetail
|
||||
}
|
||||
|
||||
export const TriggerEventsList = ({
|
||||
detail,
|
||||
}: Props) => {
|
||||
export const TriggerEventsList = () => {
|
||||
const { t } = useTranslation()
|
||||
const triggers = detail.declaration.trigger?.triggers || []
|
||||
const detail = usePluginStore(state => state.detail)
|
||||
const triggers = detail?.declaration.trigger?.triggers || []
|
||||
|
||||
if (!triggers.length)
|
||||
return null
|
||||
@@ -27,7 +22,7 @@ export const TriggerEventsList = ({
|
||||
<div className='flex flex-col gap-2'>
|
||||
{triggers.map(triggerEvent => (
|
||||
<ToolItem
|
||||
key={`${detail.plugin_id}${triggerEvent.identity.name}`}
|
||||
key={`${detail?.plugin_id}${triggerEvent.identity.name}`}
|
||||
disabled={false}
|
||||
// collection={provider}
|
||||
// @ts-expect-error triggerEvent.identity.label is Record<Locale, string>
|
||||
|
||||
@@ -202,6 +202,12 @@ export type PluginManifestInMarket = {
|
||||
from: Dependency['type']
|
||||
}
|
||||
|
||||
export enum SupportedCreationMethods {
|
||||
OAUTH = 'OAUTH',
|
||||
APIKEY = 'APIKEY',
|
||||
MANUAL = 'MANUAL',
|
||||
}
|
||||
|
||||
export type PluginDetail = {
|
||||
id: string
|
||||
created_at: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PluginMeta } from '../../plugins/types'
|
||||
import type { PluginMeta, SupportedCreationMethods } from '../../plugins/types'
|
||||
import type { Collection, Trigger } from '../../tools/types'
|
||||
import type { TypeWithI18N } from '../../base/form/types'
|
||||
|
||||
@@ -151,6 +151,7 @@ export type TriggerProviderApiEntity = {
|
||||
tags: string[]
|
||||
plugin_id?: string
|
||||
plugin_unique_identifier: string
|
||||
supported_creation_methods: SupportedCreationMethods[]
|
||||
credentials_schema: TriggerCredentialField[]
|
||||
oauth_client_schema: TriggerCredentialField[]
|
||||
subscription_schema: TriggerSubscriptionSchema
|
||||
|
||||
@@ -7,6 +7,11 @@ const translation = {
|
||||
description: 'Create your first subscription to start receiving events',
|
||||
button: 'New subscription',
|
||||
},
|
||||
createButton: {
|
||||
oauth: 'New subscription with OAuth',
|
||||
apiKey: 'New subscription with API Key',
|
||||
manual: 'Paste URL to create a new subscription',
|
||||
},
|
||||
list: {
|
||||
title: 'Subscriptions',
|
||||
addButton: 'Add',
|
||||
|
||||
@@ -16,6 +16,7 @@ const translation = {
|
||||
agent: 'Agent Strategy',
|
||||
extension: 'Extension',
|
||||
bundle: 'Bundle',
|
||||
trigger: 'Trigger',
|
||||
},
|
||||
search: 'Search',
|
||||
allCategories: 'All Categories',
|
||||
|
||||
@@ -7,6 +7,11 @@ const translation = {
|
||||
description: '创建您的第一个订阅以开始接收事件',
|
||||
button: '新建订阅',
|
||||
},
|
||||
createButton: {
|
||||
oauth: '通过 OAuth 新建订阅',
|
||||
apiKey: '通过 API Key 新建订阅',
|
||||
manual: '粘贴 URL 以创建新订阅',
|
||||
},
|
||||
list: {
|
||||
title: '订阅列表',
|
||||
addButton: '添加',
|
||||
|
||||
@@ -16,6 +16,7 @@ const translation = {
|
||||
agent: 'Agent 策略',
|
||||
extension: '扩展',
|
||||
bundle: '插件集',
|
||||
trigger: '触发器',
|
||||
},
|
||||
search: '搜索',
|
||||
allCategories: '所有类别',
|
||||
|
||||
@@ -95,6 +95,15 @@ export const useInvalidateAllTriggerPlugins = () => {
|
||||
}
|
||||
|
||||
// ===== Trigger Subscriptions Management =====
|
||||
|
||||
export const useTriggerProviderInfo = (provider: string, enabled = true) => {
|
||||
return useQuery<TriggerProviderApiEntity>({
|
||||
queryKey: [NAME_SPACE, 'provider-info', provider],
|
||||
queryFn: () => get<TriggerProviderApiEntity>(`/workspaces/current/trigger-provider/${provider}/info`),
|
||||
enabled: enabled && !!provider,
|
||||
})
|
||||
}
|
||||
|
||||
export const useTriggerSubscriptions = (provider: string, enabled = true) => {
|
||||
return useQuery<TriggerSubscription[]>({
|
||||
queryKey: [NAME_SPACE, 'list-subscriptions', provider],
|
||||
|
||||
Reference in New Issue
Block a user