mirror of https://github.com/langgenius/dify.git
feat: add modal style opt
This commit is contained in:
parent
11e55088c9
commit
91e5e33440
|
|
@ -1,21 +1,25 @@
|
|||
'use client'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiQuestionLine,
|
||||
RiSettings4Line,
|
||||
} from '@remixicon/react'
|
||||
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'
|
||||
|
||||
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
type Props = {
|
||||
onSelect: (type: SubscriptionAddType) => void
|
||||
onClose: () => void
|
||||
position?: 'bottom' | 'right'
|
||||
enum SubscriptionAddTypeEnum {
|
||||
OAuth = 'oauth',
|
||||
APIKey = 'api-key',
|
||||
Manual = 'manual',
|
||||
}
|
||||
|
||||
const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom' }: Props) => {
|
||||
type Props = {
|
||||
onSelect: (type: SubscriptionAddTypeEnum) => void
|
||||
onClose: () => void
|
||||
position?: 'bottom' | 'right'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom', className }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
|
|
@ -29,28 +33,28 @@ const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom' }: Props) => {
|
|||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [onClose])
|
||||
|
||||
const onClickClientSettings = () => {
|
||||
// todo: show client settings
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
key: 'oauth' as const,
|
||||
key: SubscriptionAddTypeEnum.OAuth,
|
||||
title: t('pluginTrigger.subscription.addType.options.oauth.title'),
|
||||
rightIcon: RiSettings4Line,
|
||||
hasRightIcon: true,
|
||||
extraContent: <ActionButton onClick={onClickClientSettings}><RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /></ActionButton>,
|
||||
},
|
||||
{
|
||||
key: 'api-key' as const,
|
||||
key: SubscriptionAddTypeEnum.APIKey,
|
||||
title: t('pluginTrigger.subscription.addType.options.apiKey.title'),
|
||||
hasRightIcon: false,
|
||||
},
|
||||
{
|
||||
key: 'manual' as const,
|
||||
key: SubscriptionAddTypeEnum.Manual,
|
||||
title: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
|
||||
rightIcon: RiQuestionLine,
|
||||
hasRightIcon: true,
|
||||
tooltip: t('pluginTrigger.subscription.addType.options.manual.tip'),
|
||||
},
|
||||
]
|
||||
|
||||
const handleOptionClick = (type: SubscriptionAddType) => {
|
||||
const handleOptionClick = (type: SubscriptionAddTypeEnum) => {
|
||||
onSelect(type)
|
||||
}
|
||||
|
||||
|
|
@ -58,89 +62,41 @@ const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom' }: Props) => {
|
|||
<div
|
||||
ref={dropdownRef}
|
||||
className={cn(
|
||||
'absolute z-50 w-full rounded-xl border-[0.5px] border-components-panel-border bg-white/95 shadow-xl backdrop-blur-sm',
|
||||
'absolute z-50 w-full rounded-xl border-[0.5px] border-components-panel-border bg-white/95 p-1 shadow-xl backdrop-blur-sm',
|
||||
position === 'bottom'
|
||||
? 'left-1/2 top-full mt-2 -translate-x-1/2'
|
||||
: 'right-full top-0 mr-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Context Menu Content */}
|
||||
<div className="flex flex-col">
|
||||
{/* First Group - OAuth & API Key */}
|
||||
<div className="p-1">
|
||||
{options.slice(0, 2).map((option, index) => {
|
||||
const RightIconComponent = option.rightIcon
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
onClick={() => handleOptionClick(option.key)}
|
||||
className={cn(
|
||||
'flex h-8 w-full items-center gap-1 rounded-lg px-2 py-1 text-left transition-colors hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
{/* Label */}
|
||||
<div className="flex grow items-center px-1 py-0.5">
|
||||
<div className="grow truncate text-[14px] leading-[20px] text-[#354052]">
|
||||
{option.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Icon */}
|
||||
{option.hasRightIcon && RightIconComponent && (
|
||||
<div className="flex items-center justify-center rounded-md p-0.5">
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
<div className="relative h-4 w-4">
|
||||
<RightIconComponent className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="h-px bg-[rgba(16,24,40,0.04)]" />
|
||||
|
||||
{/* Second Group - Manual */}
|
||||
<div className="p-1">
|
||||
{options.slice(2).map((option) => {
|
||||
const RightIconComponent = option.rightIcon
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
onClick={() => handleOptionClick(option.key)}
|
||||
className="flex h-8 w-full items-center gap-1 rounded-lg px-2 py-1 text-left transition-colors hover:bg-[rgba(200,206,218,0.2)]"
|
||||
title={option.tooltip}
|
||||
>
|
||||
{/* Label */}
|
||||
<div className="flex grow items-center px-1 py-0.5">
|
||||
<div className="grow truncate text-[14px] leading-[20px] text-[#354052]">
|
||||
{option.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Icon */}
|
||||
{option.hasRightIcon && RightIconComponent && (
|
||||
<div className="relative h-4 w-4 shrink-0">
|
||||
<div className="absolute inset-0 flex items-center justify-center p-0.5">
|
||||
<div className="relative h-4 w-4">
|
||||
<div className="absolute inset-[8.333%]">
|
||||
<RightIconComponent className="h-full w-full text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Border overlay */}
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl border-[0.5px] border-[rgba(16,24,40,0.08)]" />
|
||||
{options.map((option, index) => {
|
||||
return (
|
||||
<>
|
||||
{index === options.length - 1 && <div className="my-1 h-px bg-divider-subtle" />}
|
||||
<button
|
||||
key={option.key}
|
||||
onClick={() => handleOptionClick(option.key)}
|
||||
className={cn(
|
||||
'flex h-8 w-full items-center gap-1 rounded-lg px-2 py-1 text-left transition-colors hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<div className="system-md-regular grow truncate text-text-secondary">
|
||||
{option.title}
|
||||
</div>
|
||||
{
|
||||
option.tooltip && (
|
||||
<Tooltip
|
||||
popupContent={option.tooltip}
|
||||
triggerClassName='h-4 w-4 shrink-0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{option.extraContent ? option.extraContent : null}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiBookOpenLine,
|
||||
RiWebhookLine,
|
||||
} from '@remixicon/react'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import SubscriptionCard from './subscription-card'
|
||||
import SubscriptionAddModal from './subscription-add-modal'
|
||||
import AddTypeDropdown from './add-type-dropdown'
|
||||
|
|
@ -26,13 +21,10 @@ type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
|
|||
|
||||
export const SubscriptionList = ({ detail }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const showTopBorder = detail.declaration.tool || detail.declaration.endpoint
|
||||
|
||||
// Fetch subscriptions
|
||||
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(`${detail.plugin_id}/${detail.declaration.name}`)
|
||||
|
||||
// Modal states
|
||||
const [isShowAddModal, {
|
||||
setTrue: showAddModal,
|
||||
setFalse: hideAddModal,
|
||||
|
|
@ -40,7 +32,6 @@ export const SubscriptionList = ({ detail }: Props) => {
|
|||
|
||||
const [selectedAddType, setSelectedAddType] = React.useState<SubscriptionAddType | null>(null)
|
||||
|
||||
// Dropdown state for add button
|
||||
const [isShowAddDropdown, {
|
||||
setTrue: showAddDropdown,
|
||||
setFalse: hideAddDropdown,
|
||||
|
|
@ -90,55 +81,27 @@ export const SubscriptionList = ({ detail }: Props) => {
|
|||
<AddTypeDropdown
|
||||
onSelect={handleAddTypeSelect}
|
||||
onClose={hideAddDropdown}
|
||||
position='bottom'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// List state with header and secondary add button
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase mb-3 flex items-center justify-between'>
|
||||
<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
|
||||
position='right'
|
||||
popupClassName='w-[240px] p-4 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border'
|
||||
popupContent={
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle'>
|
||||
<RiWebhookLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('pluginTrigger.subscription.list.tooltip')}
|
||||
</div>
|
||||
<a
|
||||
href={docLink('/plugins/schema-definition/trigger')}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<div className='system-xs-regular inline-flex cursor-pointer items-center gap-1 text-text-accent'>
|
||||
<RiBookOpenLine className='h-3 w-3' />
|
||||
{t('pluginTrigger.subscription.list.tooltip.viewDocument')}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
<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='relative'>
|
||||
<ActionButton onClick={showAddDropdown}>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
{isShowAddDropdown && (
|
||||
<AddTypeDropdown
|
||||
onSelect={handleAddTypeSelect}
|
||||
onClose={hideAddDropdown}
|
||||
position='right'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-1'>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiCheckboxCircleFill,
|
||||
RiErrorWarningFill,
|
||||
RiFileCopyLine,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
type Props = {
|
||||
logs: TriggerLogEntity[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
const LogViewer = ({ logs, className }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set())
|
||||
|
||||
const toggleLogExpansion = (logId: string) => {
|
||||
const newExpanded = new Set(expandedLogs)
|
||||
if (newExpanded.has(logId))
|
||||
newExpanded.delete(logId)
|
||||
else
|
||||
newExpanded.add(logId)
|
||||
|
||||
setExpandedLogs(newExpanded)
|
||||
}
|
||||
|
||||
const parseRequestData = (data: any) => {
|
||||
if (typeof data === 'string' && data.startsWith('payload=')) {
|
||||
try {
|
||||
const urlDecoded = decodeURIComponent(data.substring(8)) // Remove 'payload='
|
||||
return JSON.parse(urlDecoded)
|
||||
}
|
||||
catch {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof data === 'object')
|
||||
return data
|
||||
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
}
|
||||
catch {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
const renderJsonContent = (data: any, title: string) => {
|
||||
const parsedData = title === 'REQUEST' ? parseRequestData(data) : data
|
||||
const isJsonObject = typeof parsedData === 'object'
|
||||
|
||||
if (isJsonObject) {
|
||||
return (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div className="system-xs-semibold-uppercase text-text-secondary">{title}</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={parsedData}
|
||||
isJSONStringifyBeauty
|
||||
nodeId=""
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='rounded-md bg-components-input-bg-normal'>
|
||||
<div className='flex items-center justify-between px-2 py-1'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
{title}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(String(parsedData))
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.actionMsg.copySuccessfully'),
|
||||
})
|
||||
}}
|
||||
className='rounded-md p-0.5 hover:bg-components-panel-border'
|
||||
>
|
||||
<RiFileCopyLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='px-2 pb-2 pt-1'>
|
||||
<pre className='code-xs-regular whitespace-pre-wrap break-all text-text-secondary'>
|
||||
{String(parsedData)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!logs || logs.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-1', className)}>
|
||||
{logs.map((log, index) => {
|
||||
const logId = log.id || index.toString()
|
||||
const isExpanded = expandedLogs.has(logId)
|
||||
const isSuccess = log.response.status_code === 200
|
||||
const isError = log.response.status_code >= 400
|
||||
|
||||
return (
|
||||
<div
|
||||
key={logId}
|
||||
className={cn(
|
||||
'relative rounded-lg border bg-components-panel-on-panel-item-bg shadow-sm hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
isError && 'border-state-destructive-border',
|
||||
!isError && isExpanded && 'border-components-panel-border',
|
||||
!isError && !isExpanded && 'border-components-panel-border-subtle',
|
||||
)}
|
||||
>
|
||||
{isError && (
|
||||
<div className='absolute -left-1 -top-4 h-16 w-16 opacity-10'>
|
||||
<div className='h-full w-full rounded-full bg-text-destructive' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => toggleLogExpansion(logId)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between px-2 py-1.5 text-left',
|
||||
isExpanded ? 'pb-1 pt-2' : 'min-h-7',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-0'>
|
||||
{isExpanded ? (
|
||||
<RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
|
||||
) : (
|
||||
<RiArrowRightSLine className='h-4 w-4 text-text-tertiary' />
|
||||
)}
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
REQUEST #{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{dayjs(log.created_at).format('HH:mm:ss')}
|
||||
</div>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
{isSuccess ? (
|
||||
<RiCheckboxCircleFill className='h-full w-full text-text-success' />
|
||||
) : (
|
||||
<RiErrorWarningFill className='h-full w-full text-text-destructive' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className='flex flex-col gap-1 px-1 pb-1'>
|
||||
{renderJsonContent(log.request.data, 'REQUEST')}
|
||||
{renderJsonContent(log.response.data, 'RESPONSE')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogViewer
|
||||
|
|
@ -2,12 +2,8 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiCheckboxCircleLine,
|
||||
RiCloseLine,
|
||||
RiErrorWarningLine,
|
||||
RiFileCopyLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
|
@ -16,16 +12,16 @@ import Toast from '@/app/components/base/toast'
|
|||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
// useTriggerSubscriptionBuilderLogs,
|
||||
useTriggerSubscriptionBuilderLogs,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
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 type { FormRefObject } from '@/app/components/base/form/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'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
|
|
@ -33,110 +29,30 @@ type Props = {
|
|||
onSuccess: () => void
|
||||
}
|
||||
|
||||
// type LogEntry = {
|
||||
// timestamp: string
|
||||
// method: string
|
||||
// path: string
|
||||
// status: number
|
||||
// headers: Record<string, any>
|
||||
// body: any
|
||||
// response: any
|
||||
// }
|
||||
|
||||
const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Form state
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
|
||||
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set())
|
||||
// const formRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
// API mutations
|
||||
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
// Get provider name
|
||||
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
|
||||
const propertiesSchema = pluginDetail.declaration.trigger.subscription_schema.properties_schema || []
|
||||
const propertiesFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
// const { data: logs, isLoading: isLoadingLogs } = useTriggerSubscriptionBuilderLogs(
|
||||
// providerName,
|
||||
// subscriptionBuilder?.id || '',
|
||||
// {
|
||||
// enabled: !!subscriptionBuilder?.id,
|
||||
// refetchInterval: 3000, // Poll every 3 seconds
|
||||
// },
|
||||
// )
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockLogs = [
|
||||
const { data: logData } = useTriggerSubscriptionBuilderLogs(
|
||||
providerName,
|
||||
subscriptionBuilder?.id || '',
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-01-15T18:09:14Z',
|
||||
method: 'POST',
|
||||
path: '/webhook',
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Slack-Hooks/1.0',
|
||||
'X-Slack-Signature': 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503',
|
||||
},
|
||||
body: {
|
||||
verification_token: 'secret_tMrlL1qK5vuQAhCh',
|
||||
event: {
|
||||
type: 'message',
|
||||
text: 'Hello world',
|
||||
user: 'U1234567890',
|
||||
},
|
||||
},
|
||||
response: {
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to process webhook',
|
||||
},
|
||||
enabled: !!subscriptionBuilder?.id,
|
||||
refetchInterval: 3000,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-01-15T18:09:14Z',
|
||||
method: 'POST',
|
||||
path: '/webhook',
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Slack-Hooks/1.0',
|
||||
},
|
||||
body: {
|
||||
verification_token: 'secret_tMrlL1qK5vuQAhCh',
|
||||
},
|
||||
response: {
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-01-15T18:09:14Z',
|
||||
method: 'POST',
|
||||
path: '/webhook',
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Slack-Hooks/1.0',
|
||||
},
|
||||
body: {
|
||||
verification_token: 'secret_tMrlL1qK5vuQAhCh',
|
||||
},
|
||||
response: {
|
||||
output: {
|
||||
output: 'I am the GPT-3 model from OpenAI, an artificial intelligence assistant.',
|
||||
},
|
||||
raw_output: 'I am the GPT-3 model from OpenAI, an artificial intelligence assistant.',
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
const logs = mockLogs
|
||||
const isLoadingLogs = false
|
||||
const logs = logData?.logs || []
|
||||
|
||||
// Create subscription builder on mount
|
||||
useEffect(() => {
|
||||
if (!subscriptionBuilder) {
|
||||
createBuilder(
|
||||
|
|
@ -173,13 +89,23 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
if (!subscriptionBuilder)
|
||||
return
|
||||
|
||||
// Get form values using the ref (for future use if needed)
|
||||
// const formValues = formRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
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: () => {
|
||||
|
|
@ -200,16 +126,6 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
)
|
||||
}
|
||||
|
||||
const toggleLogExpansion = (logId: string) => {
|
||||
const newExpanded = new Set(expandedLogs)
|
||||
if (newExpanded.has(logId))
|
||||
newExpanded.delete(logId)
|
||||
else
|
||||
newExpanded.add(logId)
|
||||
|
||||
setExpandedLogs(newExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
|
|
@ -227,7 +143,6 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
</div>
|
||||
|
||||
<div className='max-h-[70vh] overflow-y-auto p-6 pt-2'>
|
||||
{/* Subscription Name */}
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.subscriptionName.label')}
|
||||
|
|
@ -239,7 +154,6 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Callback URL */}
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.label')}
|
||||
|
|
@ -257,210 +171,37 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
{t('pluginTrigger.modal.form.callbackUrl.description')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic Parameters Form */}
|
||||
{/* {parametersSchema.length > 0 && (
|
||||
{propertiesSchema.length > 0 && (
|
||||
<div className='mb-6'>
|
||||
<div className='system-sm-medium mb-3 text-text-primary'>
|
||||
Subscription Parameters
|
||||
</div>
|
||||
<BaseForm
|
||||
formSchemas={parametersSchema}
|
||||
ref={formRef}
|
||||
formSchemas={propertiesSchema}
|
||||
ref={propertiesFormRef}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
)}
|
||||
|
||||
{/* Request Logs */}
|
||||
{subscriptionBuilder && (
|
||||
<div className='mb-6'>
|
||||
{/* Divider with Title */}
|
||||
<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 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>
|
||||
|
||||
{/* Request List */}
|
||||
<div className='flex flex-col gap-1'>
|
||||
{isLoadingLogs && (
|
||||
<div className='flex items-center justify-center gap-1 rounded-lg bg-background-section p-3'>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
<svg className='animate-spin' viewBox='0 0 24 24'>
|
||||
<circle cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='2' fill='none' strokeDasharray='31.416' strokeDashoffset='31.416'>
|
||||
<animate attributeName='stroke-dasharray' dur='2s' values='0 31.416;15.708 15.708;0 31.416' repeatCount='indefinite' />
|
||||
<animate attributeName='stroke-dashoffset' dur='2s' values='0;-15.708;-31.416' repeatCount='indefinite' />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
Awaiting request from Slack...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingLogs && logs && logs.length > 0 && (
|
||||
<>
|
||||
{logs.map((log, index) => {
|
||||
const logId = log.id || index.toString()
|
||||
const isExpanded = expandedLogs.has(logId)
|
||||
const isSuccess = log.status >= 200 && log.status < 300
|
||||
const isError = log.status >= 400
|
||||
|
||||
return (
|
||||
<div
|
||||
key={logId}
|
||||
className={cn(
|
||||
'relative rounded-lg border shadow-sm',
|
||||
isError && 'border-state-destructive-border bg-white',
|
||||
!isError && isExpanded && 'border-components-panel-border bg-white',
|
||||
!isError && !isExpanded && 'border-components-panel-border bg-background-section',
|
||||
)}
|
||||
>
|
||||
{/* Error background decoration */}
|
||||
{isError && (
|
||||
<div className='absolute -left-1 -top-4 h-16 w-16 opacity-10'>
|
||||
<div className='h-full w-full rounded-full bg-text-destructive' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Request Header */}
|
||||
<button
|
||||
onClick={() => toggleLogExpansion(logId)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between px-2 py-1.5 text-left',
|
||||
isExpanded ? 'pb-1 pt-2' : 'min-h-7',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-0'>
|
||||
{isExpanded ? (
|
||||
<RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
|
||||
) : (
|
||||
<RiArrowRightSLine className='h-4 w-4 text-text-tertiary' />
|
||||
)}
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
REQUEST #{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{new Date(log.timestamp).toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
{isSuccess ? (
|
||||
<RiCheckboxCircleLine className='text-state-success-text h-full w-full' />
|
||||
) : (
|
||||
<RiErrorWarningLine className='text-state-destructive-text h-full w-full' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className='flex flex-col gap-1 px-1 pb-1'>
|
||||
{/* Request Block */}
|
||||
<div className='rounded-md bg-components-input-bg-normal'>
|
||||
<div className='flex items-center justify-between px-2 py-1'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
REQUEST
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(JSON.stringify(log.body, null, 2))
|
||||
Toast.notify({ type: 'success', message: 'Copied to clipboard' })
|
||||
}}
|
||||
className='rounded-md p-0.5 hover:bg-components-panel-border'
|
||||
>
|
||||
<RiFileCopyLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex px-0 pb-2 pt-1'>
|
||||
<div className='w-7 pr-3 text-right'>
|
||||
<div className='code-xs-regular text-text-quaternary'>
|
||||
{JSON.stringify(log.body, null, 2).split('\n').map((_, i) => (
|
||||
<div key={i}>{String(i + 1).padStart(2, '0')}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 px-3'>
|
||||
<pre className='code-xs-regular text-text-secondary'>
|
||||
{JSON.stringify(log.body, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Response Block */}
|
||||
<div className='rounded-md bg-components-input-bg-normal'>
|
||||
<div className='flex items-center justify-between px-2 py-1'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
RESPONSE
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(JSON.stringify(log.response, null, 2))
|
||||
Toast.notify({ type: 'success', message: 'Copied to clipboard' })
|
||||
}}
|
||||
className='rounded-md p-0.5 hover:bg-components-panel-border'
|
||||
>
|
||||
<RiFileCopyLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex px-0 pb-2 pt-1'>
|
||||
<div className='w-7 pr-3 text-right'>
|
||||
<div className='code-xs-regular text-text-quaternary'>
|
||||
{JSON.stringify(log.response, null, 2).split('\n').map((_, i) => (
|
||||
<div key={i}>{String(i + 1).padStart(2, '0')}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 px-3'>
|
||||
<pre className='code-xs-regular text-text-secondary'>
|
||||
{JSON.stringify(log.response, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoadingLogs && (!logs || logs.length === 0) && (
|
||||
<div className='flex items-center justify-center gap-1 rounded-lg bg-background-section p-3'>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
<svg className='animate-spin' viewBox='0 0 24 24'>
|
||||
<circle cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='2' fill='none' strokeDasharray='31.416' strokeDashoffset='31.416'>
|
||||
<animate attributeName='stroke-dasharray' dur='2s' values='0 31.416;15.708 15.708;0 31.416' repeatCount='indefinite' />
|
||||
<animate attributeName='stroke-dashoffset' dur='2s' values='0;-15.708;-31.416' repeatCount='indefinite' />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
Awaiting request from Slack...
|
||||
</div>
|
||||
</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 {pluginDetail.declaration.name}...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LogViewer logs={logs} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='flex justify-end gap-2 border-t border-divider-subtle p-6 pt-4'>
|
||||
<div className='flex justify-end gap-2 p-6 pt-4'>
|
||||
<Button variant='secondary' onClick={onClose}>
|
||||
{t('pluginTrigger.modal.common.cancel')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiClipboardLine,
|
||||
RiCloseLine,
|
||||
RiExternalLinkLine,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
|
@ -13,12 +14,13 @@ import Form from '@/app/components/base/form/form-scenarios/auth'
|
|||
import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useConfigureTriggerOAuth,
|
||||
useInitiateTriggerOAuth,
|
||||
useTriggerOAuthConfig,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
|
|
@ -26,37 +28,56 @@ type Props = {
|
|||
onSuccess: () => void
|
||||
}
|
||||
|
||||
type OAuthStep = 'setup' | 'authorize' | 'configuration'
|
||||
enum OAuthStepEnum {
|
||||
Setup = 'setup',
|
||||
Configuration = 'configuration',
|
||||
}
|
||||
|
||||
enum AuthorizationStatusEnum {
|
||||
Pending = 'pending',
|
||||
Success = 'success',
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// State
|
||||
const [currentStep, setCurrentStep] = useState<OAuthStep>('setup')
|
||||
const [currentStep, setCurrentStep] = useState<OAuthStepEnum>(OAuthStepEnum.Setup)
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [authorizationUrl, setAuthorizationUrl] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<any>(null)
|
||||
const [redirectUrl, setRedirectUrl] = useState('')
|
||||
const [authorizationStatus, setAuthorizationStatus] = useState<'pending' | 'success' | 'failed'>('pending')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
|
||||
const [authorizationStatus, setAuthorizationStatus] = useState<AuthorizationStatusEnum>()
|
||||
|
||||
// Form refs
|
||||
const clientFormRef = React.useRef<FormRefObject>(null)
|
||||
const parametersFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
// API mutations
|
||||
const { mutate: initiateOAuth, isPending: isInitiating } = useInitiateTriggerOAuth()
|
||||
const { mutate: configureOAuth, isPending: isConfiguring } = useConfigureTriggerOAuth()
|
||||
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
// Get provider name and schemas
|
||||
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 || []
|
||||
|
||||
// Poll for authorization status
|
||||
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
|
||||
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
const { data: oauthConfig } = useTriggerOAuthConfig(providerName)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep === 'authorize' && subscriptionBuilder && authorizationStatus === 'pending') {
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep === OAuthStepEnum.Setup && subscriptionBuilder && authorizationStatus === AuthorizationStatusEnum.Pending) {
|
||||
const pollInterval = setInterval(() => {
|
||||
verifyBuilder(
|
||||
{
|
||||
|
|
@ -65,12 +86,13 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAuthorizationStatus('success')
|
||||
setCurrentStep('configuration')
|
||||
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
|
||||
|
|
@ -83,7 +105,7 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
}
|
||||
}, [currentStep, subscriptionBuilder, authorizationStatus, verifyBuilder, providerName, t])
|
||||
|
||||
const handleSetupOAuth = () => {
|
||||
const handleAuthorize = () => {
|
||||
const clientFormValues = clientFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
const clientParams = clientFormValues.values
|
||||
|
||||
|
|
@ -94,48 +116,7 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
})
|
||||
return
|
||||
}
|
||||
|
||||
// First configure OAuth client
|
||||
configureOAuth(
|
||||
{
|
||||
provider: providerName,
|
||||
client_params: clientParams as any,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Then get redirect URL and initiate OAuth
|
||||
const baseUrl = window.location.origin
|
||||
const redirectPath = `/plugins/oauth/callback/${providerName}`
|
||||
const fullRedirectUrl = `${baseUrl}${redirectPath}`
|
||||
setRedirectUrl(fullRedirectUrl)
|
||||
|
||||
// Initiate OAuth flow
|
||||
initiateOAuth(providerName, {
|
||||
onSuccess: (response) => {
|
||||
setAuthorizationUrl(response.authorization_url)
|
||||
setSubscriptionBuilder(response.subscription_builder)
|
||||
setCurrentStep('authorize')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleAuthorize = () => {
|
||||
setAuthorizationStatus(AuthorizationStatusEnum.Pending)
|
||||
if (authorizationUrl) {
|
||||
// Open authorization URL in new window
|
||||
window.open(authorizationUrl, '_blank', 'width=500,height=600')
|
||||
|
|
@ -154,10 +135,16 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
if (!subscriptionBuilder)
|
||||
return
|
||||
|
||||
const parameters = parametersFormRef.current?.getFormValues({})?.values
|
||||
|
||||
buildSubscription(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
params: {
|
||||
name: subscriptionName,
|
||||
parameters,
|
||||
} as Record<string, any>,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
|
@ -182,121 +169,61 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
<Modal
|
||||
isShow
|
||||
onClose={onClose}
|
||||
className='!max-w-[520px] !p-0'
|
||||
className='!max-w-[520px] p-6'
|
||||
wrapperClassName='!z-[1002]'
|
||||
>
|
||||
<div className='flex items-center justify-between border-b border-divider-subtle p-6 pb-4'>
|
||||
<div className='flex items-center justify-between pb-3'>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.title')}
|
||||
</h3>
|
||||
<Button variant='ghost' size='small' onClick={onClose}>
|
||||
<ActionButton onClick={onClose}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<div className='p-6'>
|
||||
{currentStep === 'setup' && (
|
||||
<div>
|
||||
<div className='mb-4'>
|
||||
<h4 className='system-sm-semibold mb-2 text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.title')}
|
||||
</h4>
|
||||
<p className='system-xs-regular text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='py-3'>
|
||||
{currentStep === OAuthStepEnum.Setup && (
|
||||
<>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{clientSchema.length > 0 && (
|
||||
<div className='mb-4'>
|
||||
<Form
|
||||
formSchemas={clientSchema}
|
||||
ref={clientFormRef}
|
||||
/>
|
||||
</div>
|
||||
<Form
|
||||
formSchemas={clientSchema}
|
||||
ref={clientFormRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentStep === 'authorize' && (
|
||||
{currentStep === OAuthStepEnum.Configuration && (
|
||||
<div>
|
||||
<div className='mb-4'>
|
||||
<h4 className='system-sm-semibold mb-2 text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.title')}
|
||||
</h4>
|
||||
<p className='system-xs-regular mb-4 text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Redirect URL */}
|
||||
{redirectUrl && (
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.redirectUrl')}
|
||||
</label>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
value={redirectUrl}
|
||||
readOnly
|
||||
className='bg-background-section pr-12'
|
||||
/>
|
||||
<CopyFeedbackNew
|
||||
content={redirectUrl}
|
||||
className='absolute right-1 top-1/2 -translate-y-1/2 text-text-tertiary'
|
||||
/>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.redirectUrlHelp')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authorization Status */}
|
||||
<div className='mb-4 rounded-lg bg-background-section p-4'>
|
||||
{authorizationStatus === 'pending' && (
|
||||
<div className='system-sm-regular text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.waitingAuth')}
|
||||
</div>
|
||||
)}
|
||||
{authorizationStatus === 'success' && (
|
||||
<div className='system-sm-regular text-text-success'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.authSuccess')}
|
||||
</div>
|
||||
)}
|
||||
{authorizationStatus === 'failed' && (
|
||||
<div className='system-sm-regular text-text-destructive'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.authFailed')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authorize Button */}
|
||||
{authorizationStatus === 'pending' && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleAuthorize}
|
||||
disabled={!authorizationUrl}
|
||||
className='w-full'
|
||||
>
|
||||
<RiExternalLinkLine className='mr-2 h-4 w-4' />
|
||||
{t('pluginTrigger.modal.oauth.authorization.authorizeButton', { provider: providerName })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'configuration' && (
|
||||
<div>
|
||||
<div className='mb-4'>
|
||||
<h4 className='system-sm-semibold mb-2 text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.configuration.title')}
|
||||
</h4>
|
||||
<p className='system-xs-regular text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.configuration.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Subscription Name */}
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.subscriptionName.label')}
|
||||
|
|
@ -308,7 +235,6 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Callback URL (read-only) */}
|
||||
{subscriptionBuilder?.endpoint && (
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
|
|
@ -325,37 +251,32 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Dynamic Parameters Form */}
|
||||
{parametersSchema.length > 0 && (
|
||||
<div className='mb-4'>
|
||||
<Form
|
||||
formSchemas={parametersSchema}
|
||||
ref={parametersFormRef}
|
||||
/>
|
||||
</div>
|
||||
<Form
|
||||
formSchemas={parametersSchema}
|
||||
ref={parametersFormRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='flex justify-end gap-2 border-t border-divider-subtle p-6 pt-4'>
|
||||
<div className='flex justify-end gap-2 pt-5'>
|
||||
<Button variant='secondary' onClick={onClose}>
|
||||
{t('pluginTrigger.modal.common.cancel')}
|
||||
</Button>
|
||||
|
||||
{currentStep === 'setup' && (
|
||||
{currentStep === OAuthStepEnum.Setup && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSetupOAuth}
|
||||
loading={isConfiguring || isInitiating}
|
||||
// disabled={clientSchema.length > 0}
|
||||
onClick={handleAuthorize}
|
||||
loading={authorizationStatus === AuthorizationStatusEnum.Pending}
|
||||
>
|
||||
{(isConfiguring || isInitiating) ? t('pluginTrigger.modal.common.authorizing') : t('pluginTrigger.modal.common.authorize')}
|
||||
{authorizationStatus === AuthorizationStatusEnum.Pending ? t('pluginTrigger.modal.common.authorizing') : t('pluginTrigger.modal.common.authorize')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === 'configuration' && (
|
||||
{currentStep === OAuthStepEnum.Configuration && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreate}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const SubscriptionCard = ({ data, onRefresh }: Props) => {
|
|||
</div>
|
||||
<div className="mx-2 text-xs text-text-tertiary opacity-30">·</div>
|
||||
<div className='system-xs-regular shrink-0 text-text-tertiary'>
|
||||
{isActive ? 'Used by 3 workflows' : 'No workflow used'}
|
||||
{data.workflows_in_use > 0 ? t('pluginTrigger.subscription.list.item.usedByNum', { num: data.workflows_in_use }) : t('pluginTrigger.subscription.list.item.noUsed')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ type TriggerSubscriptionStructure = {
|
|||
endpoint: string
|
||||
parameters: TriggerSubParameters
|
||||
properties: TriggerSubProperties
|
||||
workflows_in_use: number
|
||||
}
|
||||
|
||||
export type TriggerSubscription = TriggerSubscriptionStructure
|
||||
|
|
@ -212,6 +213,7 @@ export type TriggerOAuthConfig = {
|
|||
configured: boolean
|
||||
custom_configured: boolean
|
||||
custom_enabled: boolean
|
||||
redirect_uri: string
|
||||
params: {
|
||||
client_id: string
|
||||
client_secret: string
|
||||
|
|
@ -230,3 +232,48 @@ export type TriggerOAuthResponse = {
|
|||
authorization_url: string
|
||||
subscription_builder: TriggerSubscriptionBuilder
|
||||
}
|
||||
|
||||
export type TriggerLogEntity = {
|
||||
id: string
|
||||
endpoint: string
|
||||
request: LogRequest
|
||||
response: LogResponse
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type LogRequest = {
|
||||
method: string
|
||||
url: string
|
||||
headers: LogRequestHeaders
|
||||
data: string
|
||||
}
|
||||
|
||||
export type LogRequestHeaders = {
|
||||
'Host': string
|
||||
'User-Agent': string
|
||||
'Content-Length': string
|
||||
'Accept': string
|
||||
'Content-Type': string
|
||||
'X-Forwarded-For': string
|
||||
'X-Forwarded-Host': string
|
||||
'X-Forwarded-Proto': string
|
||||
'X-Github-Delivery': string
|
||||
'X-Github-Event': string
|
||||
'X-Github-Hook-Id': string
|
||||
'X-Github-Hook-Installation-Target-Id': string
|
||||
'X-Github-Hook-Installation-Target-Type': string
|
||||
'Accept-Encoding': string
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export type LogResponse = {
|
||||
status_code: number
|
||||
headers: LogResponseHeaders
|
||||
data: string
|
||||
}
|
||||
|
||||
export type LogResponseHeaders = {
|
||||
'Content-Type': string
|
||||
'Content-Length': string
|
||||
[key: string]: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const translation = {
|
|||
list: {
|
||||
title: 'Subscriptions',
|
||||
addButton: 'Add',
|
||||
tip: 'Receive events via Subscription',
|
||||
item: {
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
|
|
@ -32,6 +33,8 @@ const translation = {
|
|||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
},
|
||||
usedByNum: 'Used by {{num}} workflows',
|
||||
noUsed: 'No workflow used',
|
||||
},
|
||||
},
|
||||
addType: {
|
||||
|
|
@ -48,7 +51,7 @@ const translation = {
|
|||
},
|
||||
manual: {
|
||||
title: 'Manual Setup',
|
||||
description: 'Manually configure webhook URL and settings',
|
||||
description: 'Paste URL to create a new subscription',
|
||||
tip: 'Configure URL on third-party platform manually',
|
||||
},
|
||||
},
|
||||
|
|
@ -70,6 +73,7 @@ const translation = {
|
|||
verifying: 'Verifying...',
|
||||
authorizing: 'Authorizing...',
|
||||
},
|
||||
oauthRedirectInfo: 'As no system client secrets found for this tool provider, setup it manually is required, for redirect_uri, please use',
|
||||
apiKey: {
|
||||
title: 'Create via API Key',
|
||||
verify: {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const translation = {
|
|||
list: {
|
||||
title: '订阅列表',
|
||||
addButton: '添加',
|
||||
tip: '通过订阅接收事件',
|
||||
item: {
|
||||
enabled: '已启用',
|
||||
disabled: '已禁用',
|
||||
|
|
@ -32,6 +33,8 @@ const translation = {
|
|||
active: '活跃',
|
||||
inactive: '非活跃',
|
||||
},
|
||||
usedByNum: '被 {{num}} 个工作流使用',
|
||||
noUsed: '未被工作流使用',
|
||||
},
|
||||
},
|
||||
addType: {
|
||||
|
|
@ -39,16 +42,16 @@ const translation = {
|
|||
description: '选择创建触发器订阅的方式',
|
||||
options: {
|
||||
apiKey: {
|
||||
title: '通过API密钥',
|
||||
title: '通过 API Key',
|
||||
description: '使用API凭据自动创建订阅',
|
||||
},
|
||||
oauth: {
|
||||
title: '通过OAuth',
|
||||
title: '通过 OAuth',
|
||||
description: '与第三方平台授权以创建订阅',
|
||||
},
|
||||
manual: {
|
||||
title: '手动设置',
|
||||
description: '手动配置Webhook URL和设置',
|
||||
description: '粘贴 URL 以创建新订阅',
|
||||
tip: '手动配置 URL 到第三方平台',
|
||||
},
|
||||
},
|
||||
|
|
@ -70,6 +73,7 @@ const translation = {
|
|||
verifying: '验证中...',
|
||||
authorizing: '授权中...',
|
||||
},
|
||||
oauthRedirectInfo: '由于未找到此工具提供方的系统客户端密钥,需要手动设置,对于 redirect_uri,请使用',
|
||||
apiKey: {
|
||||
title: '通过API密钥创建',
|
||||
verify: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { del, get, post } from './base'
|
||||
import type {
|
||||
TriggerLogEntity,
|
||||
TriggerOAuthClientParams,
|
||||
TriggerOAuthConfig,
|
||||
TriggerProviderApiEntity,
|
||||
|
|
@ -169,10 +170,12 @@ export const useBuildTriggerSubscription = () => {
|
|||
mutationFn: (payload: {
|
||||
provider: string
|
||||
subscriptionBuilderId: string
|
||||
params?: Record<string, any>
|
||||
}) => {
|
||||
const { provider, subscriptionBuilderId } = payload
|
||||
const { provider, subscriptionBuilderId, ...body } = payload
|
||||
return post(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/build/${subscriptionBuilderId}`,
|
||||
{ body },
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -199,7 +202,7 @@ export const useTriggerSubscriptionBuilderLogs = (
|
|||
) => {
|
||||
const { enabled = true, refetchInterval = false } = options
|
||||
|
||||
return useQuery<Record<string, any>[]>({
|
||||
return useQuery<{ logs: TriggerLogEntity[] }>({
|
||||
queryKey: [NAME_SPACE, 'subscription-builder-logs', provider, subscriptionBuilderId],
|
||||
queryFn: () => get(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/logs/${subscriptionBuilderId}`,
|
||||
|
|
@ -250,7 +253,7 @@ export const useInitiateTriggerOAuth = () => {
|
|||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'initiate-oauth'],
|
||||
mutationFn: (provider: string) => {
|
||||
return get<{ authorization_url: string; subscription_builder: any }>(
|
||||
return get<{ authorization_url: string; subscription_builder: TriggerSubscriptionBuilder }>(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/oauth/authorize`,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue