diff --git a/web/app/components/base/encrypted-bottom/index.tsx b/web/app/components/base/encrypted-bottom/index.tsx new file mode 100644 index 0000000000..7bb64bb0a3 --- /dev/null +++ b/web/app/components/base/encrypted-bottom/index.tsx @@ -0,0 +1,30 @@ +import cn from '@/utils/classnames' +import { RiLock2Fill } from '@remixicon/react' +import Link from 'next/link' +import { useTranslation } from 'react-i18next' + +type Props = { + className?: string + frontTextKey?: string + backTextKey?: string +} + +export const EncryptedBottom = (props: Props) => { + const { t } = useTranslation() + const { frontTextKey, backTextKey, className } = props + + return ( +
+ + {t(frontTextKey || 'common.provider.encrypted.front')} + + PKCS1_OAEP + + {t(backTextKey || 'common.provider.encrypted.back')} +
+ ) +} diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index a64516d813..443c4cb708 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -1,6 +1,6 @@ import CheckboxList from '@/app/components/base/checkbox-list' -import type { FormSchema } from '@/app/components/base/form/types' -import { FormTypeEnum } from '@/app/components/base/form/types' +import type { FieldState, FormSchema } from '@/app/components/base/form/types' +import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types' import Input from '@/app/components/base/input' import Radio from '@/app/components/base/radio' import RadioE from '@/app/components/base/radio/ui' @@ -31,6 +31,29 @@ const getExtraProps = (type: FormTypeEnum) => { } } +const VALIDATE_STATUS_STYLE_MAP: Record = { + [FormItemValidateStatusEnum.Error]: { + componentClassName: 'border-components-input-border-destructive focus:border-components-input-border-destructive', + textClassName: 'text-text-destructive', + infoFieldName: 'errors', + }, + [FormItemValidateStatusEnum.Warning]: { + componentClassName: 'border-components-input-border-warning focus:border-components-input-border-warning', + textClassName: 'text-text-warning', + infoFieldName: 'warnings', + }, + [FormItemValidateStatusEnum.Success]: { + componentClassName: '', + textClassName: '', + infoFieldName: '', + }, + [FormItemValidateStatusEnum.Validating]: { + componentClassName: '', + textClassName: '', + infoFieldName: '', + }, +} + export type BaseFieldProps = { fieldClassName?: string labelClassName?: string @@ -40,6 +63,7 @@ export type BaseFieldProps = { field: AnyFieldApi disabled?: boolean onChange?: (field: string, value: any) => void + fieldState?: FieldState } const BaseField = ({ @@ -51,6 +75,7 @@ const BaseField = ({ field, disabled: propsDisabled, onChange, + fieldState, }: BaseFieldProps) => { const renderI18nObject = useRenderI18nObject() const { @@ -168,7 +193,7 @@ const BaseField = ({ { handleChange(e.target.value) @@ -266,6 +291,14 @@ const BaseField = ({ ) } + {fieldState?.validateStatus && [FormItemValidateStatusEnum.Error, FormItemValidateStatusEnum.Warning].includes(fieldState?.validateStatus) && ( +
+ {fieldState?.[VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus].infoFieldName as keyof FieldState]} +
+ )} { formSchema.url && ( >({}) + const showOnValues = useStore(form.store, (s: any) => { const result: Record = {} formSchemas.forEach((schema) => { @@ -85,6 +91,34 @@ const BaseForm = ({ return result }) + const setFields = useCallback((fields: SetFieldsParam[]) => { + const newFieldStates: Record = { ...fieldStates } + + for (const field of fields) { + const { name, value, errors, warnings, validateStatus, help } = field + + if (value !== undefined) + form.setFieldValue(name, value) + + let finalValidateStatus = validateStatus + if (!finalValidateStatus) { + if (errors && errors.length > 0) + finalValidateStatus = FormItemValidateStatusEnum.Error + else if (warnings && warnings.length > 0) + finalValidateStatus = FormItemValidateStatusEnum.Warning + } + + newFieldStates[name] = { + validateStatus: finalValidateStatus, + help, + errors, + warnings, + } + } + + setFieldStates(newFieldStates) + }, [form, fieldStates]) + useImperativeHandle(ref, () => { return { getForm() { @@ -93,8 +127,9 @@ const BaseForm = ({ getFormValues: (option) => { return getFormValues(option) }, + setFields, } - }, [form, getFormValues]) + }, [form, getFormValues, setFields]) const renderField = useCallback((field: AnyFieldApi) => { const formSchema = formSchemas?.find(schema => schema.name === field.name) @@ -110,12 +145,13 @@ const BaseForm = ({ inputClassName={inputClassName} disabled={disabled} onChange={onChange} + fieldState={fieldStates[field.name]} /> ) } return null - }, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange]) + }, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange, fieldStates]) const renderFieldWrapper = useCallback((formSchema: FormSchema) => { const validators = getValidators(formSchema) diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index 1b438fe01c..9cb9b5fda6 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -45,6 +45,13 @@ export type FormOption = { export type AnyValidators = FieldValidators +export enum FormItemValidateStatusEnum { + Success = 'success', + Warning = 'warning', + Error = 'error', + Validating = 'validating', +} + export type FormSchema = { type: FormTypeEnum name: string @@ -79,11 +86,25 @@ export type GetValuesOptions = { needTransformWhenSecretFieldIsPristine?: boolean needCheckValidatedValues?: boolean } + +export type FieldState = { + validateStatus?: FormItemValidateStatusEnum + help?: string | ReactNode + errors?: string[] + warnings?: string[] +} + +export type SetFieldsParam = { + name: string + value?: any +} & FieldState + export type FormRefObject = { getForm: () => AnyFormApi getFormValues: (obj: GetValuesOptions) => { values: Record isCheckValidated: boolean } + setFields: (fields: SetFieldsParam[]) => void } export type FormRef = ForwardedRef diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index 7239b8c913..96119d0027 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -1,5 +1,6 @@ 'use client' // import { CopyFeedbackNew } from '@/app/components/base/copy-feedback' +import { EncryptedBottom } from '@/app/components/base/encrypted-bottom' import { BaseForm } from '@/app/components/base/form/components/base' import type { FormRefObject } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types' @@ -15,10 +16,11 @@ import { useUpdateTriggerSubscriptionBuilder, useVerifyTriggerSubscriptionBuilder, } from '@/service/use-triggers' +import { parsePluginErrorMessage } from '@/utils/error-parser' import { RiLoader2Line } from '@remixicon/react' +import { debounce } from 'lodash-es' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { debounce } from 'lodash-es' import LogViewer from '../log-viewer' import { usePluginStore, usePluginSubscriptionStore } from '../store' @@ -68,7 +70,6 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { const [currentStep, setCurrentStep] = useState(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration) const [subscriptionBuilder, setSubscriptionBuilder] = useState(builder) - const [verificationError, setVerificationError] = useState('') const isInitializedRef = useRef(false) const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyTriggerSubscriptionBuilder() @@ -175,7 +176,10 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { return } - setVerificationError('') + apiKeyCredentialsFormRef.current?.setFields([{ + name: Object.keys(credentials)[0], + errors: [], + }]) verifyCredentials( { @@ -191,8 +195,12 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { }) setCurrentStep(ApiKeyStep.Configuration) }, - onError: (error: any) => { - setVerificationError(error?.message || t('pluginTrigger.modal.apiKey.verify.error')) + onError: async (error: any) => { + const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.apiKey.verify.error') + apiKeyCredentialsFormRef.current?.setFields([{ + name: Object.keys(credentials)[0], + errors: [errorMessage], + }]) }, }, ) @@ -238,10 +246,11 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { onClose() refresh?.() }, - onError: (error: any) => { + onError: async (error: any) => { + const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.modal.errors.createFailed') Toast.notify({ type: 'error', - message: error?.message || t('pluginTrigger.modal.errors.createFailed'), + message: errorMessage, }) }, }, @@ -255,6 +264,13 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { handleCreate() } + const handleApiKeyCredentialsChange = () => { + apiKeyCredentialsFormRef.current?.setFields([{ + name: apiKeyCredentialsSchema[0].name, + errors: [], + }]) + } + return ( { onClose={onClose} onCancel={onClose} onConfirm={handleConfirm} + disabled={isVerifyingCredentials || isBuilding} + bottomSlot={currentStep === ApiKeyStep.Verify ? : null} > {createType === SupportedCreationMethods.APIKEY && } {currentStep === ApiKeyStep.Verify && ( @@ -278,16 +296,10 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { labelClassName='system-sm-medium mb-2 block text-text-primary' preventDefaultSubmit={true} formClassName='space-y-4' + onChange={handleApiKeyCredentialsChange} /> )} - {verificationError && ( -
-
- {verificationError} -
-
- )} )} {currentStep === ApiKeyStep.Configuration &&
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx index 6929bc93e4..897a9c5813 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx @@ -21,30 +21,23 @@ export const SubscriptionList = withErrorBoundary(({ selectedId, onSelect, }: SubscriptionListProps) => { - const { subscriptions, isLoading, hasSubscriptions } = useSubscriptionList() - - // console.log('detail', detail) + const { subscriptions, isLoading } = useSubscriptionList() if (mode === SubscriptionListMode.SELECTOR) { return ( ) } - // const showTopBorder = !!(detail?.declaration?.tool || detail?.declaration?.endpoint) - return ( ) }) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx index 1fe106ca41..5ca2e2bb36 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx @@ -11,14 +11,12 @@ type SubscriptionListViewProps = { subscriptions?: TriggerSubscription[] isLoading: boolean showTopBorder?: boolean - hasSubscriptions: boolean } export const SubscriptionListView: React.FC = ({ subscriptions, isLoading, showTopBorder = false, - hasSubscriptions, }) => { const { t } = useTranslation() @@ -35,7 +33,7 @@ export const SubscriptionListView: React.FC = ({ return (
- {hasSubscriptions && ( + {subscriptions?.length && (
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })} @@ -44,11 +42,11 @@ export const SubscriptionListView: React.FC = ({
)}
- {hasSubscriptions && ( + {subscriptions?.length && (
{subscriptions?.map(subscription => ( void } @@ -21,7 +19,6 @@ type SubscriptionSelectorProps = { export const SubscriptionSelectorView: React.FC = ({ subscriptions, isLoading, - hasSubscriptions, selectedId, onSelect, }) => { @@ -38,7 +35,7 @@ export const SubscriptionSelectorView: React.FC = ({ return (
- {hasSubscriptions &&
+ {subscriptions?.length &&
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })} @@ -50,7 +47,7 @@ export const SubscriptionSelectorView: React.FC = ({ />
}
- {hasSubscriptions ? ( + {subscriptions?.length ? ( <> {subscriptions?.map(subscription => (