mirror of
https://github.com/langgenius/dify.git
synced 2026-05-24 02:47:53 +08:00
feat: split plugin settings by category
This commit is contained in:
@ -3,26 +3,27 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
|
||||
import type { AutoUpdateConfig } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import type { ReferenceSetting } from '@/app/components/plugins/types'
|
||||
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@langgenius/dify-ui/toggle-group'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { defaultValue as defaultAutoUpdateValue } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/config'
|
||||
import PluginsPicker from '@/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useMutationPluginAutoUpgradeSettings, usePluginAutoUpgradeSettings } from '@/service/use-plugins'
|
||||
|
||||
type Props = {
|
||||
defaultStrategy?: AUTO_UPDATE_STRATEGY
|
||||
referenceSetting: ReferenceSetting
|
||||
onSave: (payload: ReferenceSetting) => void
|
||||
category: PluginCategoryEnum
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type SegmentedOption<T extends string> = {
|
||||
@ -48,62 +49,68 @@ function SettingTimeZone({ children }: { children?: ReactNode }) {
|
||||
)
|
||||
}
|
||||
|
||||
const getAutoUpgrade = (
|
||||
referenceSetting: ReferenceSetting,
|
||||
defaultStrategy: AUTO_UPDATE_STRATEGY,
|
||||
): AutoUpdateConfig => ({
|
||||
...defaultAutoUpdateValue,
|
||||
...referenceSetting.auto_upgrade,
|
||||
strategy_setting: referenceSetting.auto_upgrade?.strategy_setting ?? defaultStrategy,
|
||||
exclude_plugins: referenceSetting.auto_upgrade?.exclude_plugins ?? defaultAutoUpdateValue.exclude_plugins,
|
||||
include_plugins: referenceSetting.auto_upgrade?.include_plugins ?? defaultAutoUpdateValue.include_plugins,
|
||||
})
|
||||
|
||||
const SegmentedControl = <T extends string>({
|
||||
ariaLabel,
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
ariaLabel: string
|
||||
className?: string
|
||||
options: Array<SegmentedOption<T>>
|
||||
value: T
|
||||
onChange: (value: T) => void
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
<ToggleGroup<T>
|
||||
aria-label={ariaLabel}
|
||||
className="inline-flex items-center gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5"
|
||||
role="radiogroup"
|
||||
className={cn('flex', className)}
|
||||
value={[value]}
|
||||
onValueChange={(nextValue) => {
|
||||
const selectedValue = nextValue[0]
|
||||
if (selectedValue)
|
||||
onChange(selectedValue)
|
||||
}}
|
||||
>
|
||||
{options.map(option => (
|
||||
<button
|
||||
<ToggleGroupItem<T>
|
||||
key={option.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={value === option.value}
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-lg px-2 py-1 system-sm-medium text-text-secondary hover:bg-state-base-hover-alt',
|
||||
value === option.value && 'border-[0.5px] border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-xs hover:bg-components-segmented-control-item-active-bg',
|
||||
)}
|
||||
onClick={() => onChange(option.value)}
|
||||
value={option.value}
|
||||
className="flex-1 hover:bg-state-base-hover-alt data-pressed:hover:bg-components-segmented-control-item-active-bg"
|
||||
>
|
||||
<span className="p-0.5 whitespace-nowrap">{option.label}</span>
|
||||
</button>
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
)
|
||||
}
|
||||
|
||||
const UpdateSettingPopover = ({
|
||||
defaultStrategy = AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
referenceSetting,
|
||||
onSave,
|
||||
category,
|
||||
disabled = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const timezone = userProfile.timezone || 'UTC'
|
||||
const [autoUpgrade, setAutoUpgrade] = useState(() => getAutoUpgrade(referenceSetting, defaultStrategy))
|
||||
const {
|
||||
data: autoUpgradeSetting,
|
||||
error,
|
||||
isFetching,
|
||||
isLoading,
|
||||
} = usePluginAutoUpgradeSettings(category)
|
||||
const { mutate: saveAutoUpgrade, isPending: isSavePending } = useMutationPluginAutoUpgradeSettings({
|
||||
category,
|
||||
onSuccess: () => {
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
const savedAutoUpgrade = autoUpgradeSetting?.auto_upgrade
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [draftAutoUpgrade, setDraftAutoUpgrade] = useState<AutoUpdateConfig>()
|
||||
const autoUpgrade = draftAutoUpgrade ?? savedAutoUpgrade
|
||||
const hasSettings = !!autoUpgrade
|
||||
const isSettingsLoading = !hasSettings && !error && (isLoading || isFetching)
|
||||
const getStrategyLabel = useCallback((strategy: AUTO_UPDATE_STRATEGY) => {
|
||||
switch (strategy) {
|
||||
case AUTO_UPDATE_STRATEGY.disabled:
|
||||
@ -116,8 +123,11 @@ const UpdateSettingPopover = ({
|
||||
return ''
|
||||
}
|
||||
}, [t])
|
||||
const selectedStrategyLabel = getStrategyLabel(autoUpgrade.strategy_setting)
|
||||
const selectedStrategyLabel = autoUpgrade ? getStrategyLabel(autoUpgrade.strategy_setting) : ''
|
||||
const plugins = useMemo(() => {
|
||||
if (!autoUpgrade)
|
||||
return []
|
||||
|
||||
switch (autoUpgrade.upgrade_mode) {
|
||||
case AUTO_UPDATE_MODE.partial:
|
||||
return autoUpgrade.include_plugins
|
||||
@ -126,11 +136,14 @@ const UpdateSettingPopover = ({
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}, [autoUpgrade.exclude_plugins, autoUpgrade.include_plugins, autoUpgrade.upgrade_mode])
|
||||
}, [autoUpgrade])
|
||||
const updateTimeValue = useMemo(() => {
|
||||
if (!autoUpgrade)
|
||||
return ''
|
||||
|
||||
const localSeconds = convertUTCDaySecondsToLocalSeconds(autoUpgrade.upgrade_time_of_day, timezone)
|
||||
return timeOfDayToDayjs(localSeconds).format('HH:mm')
|
||||
}, [autoUpgrade.upgrade_time_of_day, timezone])
|
||||
}, [autoUpgrade, timezone])
|
||||
const strategyOptions = useMemo<Array<SegmentedOption<AUTO_UPDATE_STRATEGY>>>(() => [
|
||||
{
|
||||
value: AUTO_UPDATE_STRATEGY.disabled,
|
||||
@ -161,18 +174,22 @@ const UpdateSettingPopover = ({
|
||||
], [t])
|
||||
|
||||
const updateAutoUpgrade = useCallback((payload: Partial<AutoUpdateConfig>) => {
|
||||
const nextAutoUpgrade = {
|
||||
...autoUpgrade,
|
||||
...payload,
|
||||
}
|
||||
setAutoUpgrade(nextAutoUpgrade)
|
||||
onSave({
|
||||
...referenceSetting,
|
||||
auto_upgrade: nextAutoUpgrade,
|
||||
setDraftAutoUpgrade((currentAutoUpgrade) => {
|
||||
const baseAutoUpgrade = currentAutoUpgrade ?? savedAutoUpgrade
|
||||
if (!baseAutoUpgrade)
|
||||
return undefined
|
||||
|
||||
return {
|
||||
...baseAutoUpgrade,
|
||||
...payload,
|
||||
}
|
||||
})
|
||||
}, [autoUpgrade, onSave, referenceSetting])
|
||||
}, [savedAutoUpgrade])
|
||||
|
||||
const handlePluginsChange = useCallback((newPlugins: string[]) => {
|
||||
if (!autoUpgrade)
|
||||
return
|
||||
|
||||
if (autoUpgrade.upgrade_mode === AUTO_UPDATE_MODE.partial) {
|
||||
updateAutoUpgrade({
|
||||
include_plugins: newPlugins,
|
||||
@ -183,7 +200,7 @@ const UpdateSettingPopover = ({
|
||||
exclude_plugins: newPlugins,
|
||||
})
|
||||
}
|
||||
}, [autoUpgrade.upgrade_mode, updateAutoUpgrade])
|
||||
}, [autoUpgrade, updateAutoUpgrade])
|
||||
const minuteFilter = useCallback((minutes: string[]) => {
|
||||
return minutes.filter((m) => {
|
||||
const time = Number.parseInt(m, 10)
|
||||
@ -195,14 +212,38 @@ const UpdateSettingPopover = ({
|
||||
upgrade_time_of_day: convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(value), timezone),
|
||||
})
|
||||
}, [timezone, updateAutoUpgrade])
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
if (disabled) {
|
||||
setIsOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsOpen(open)
|
||||
if (open)
|
||||
setDraftAutoUpgrade(savedAutoUpgrade)
|
||||
else
|
||||
setDraftAutoUpgrade(undefined)
|
||||
}, [disabled, savedAutoUpgrade])
|
||||
const handleCancel = useCallback(() => {
|
||||
setDraftAutoUpgrade(undefined)
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
const handleSave = useCallback(() => {
|
||||
if (!autoUpgrade)
|
||||
return
|
||||
|
||||
saveAutoUpgrade(autoUpgrade)
|
||||
setDraftAutoUpgrade(undefined)
|
||||
setIsOpen(false)
|
||||
}, [autoUpgrade, saveAutoUpgrade])
|
||||
const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="group flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg border-none bg-components-input-bg-normal px-2 text-left hover:bg-state-base-hover-alt focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
className="group flex h-8 w-full cursor-pointer items-center justify-center rounded-lg border-none bg-[#f5f4ee] px-3 text-center shadow-xs hover:bg-state-base-hover-alt focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex w-0 grow items-center gap-1">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
@ -210,105 +251,148 @@ const UpdateSettingPopover = ({
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
)}
|
||||
/>
|
||||
{inputElem}
|
||||
<span className="w-[64px] min-w-[64px] text-center system-sm-medium text-components-button-secondary-text">
|
||||
{inputElem}
|
||||
</span>
|
||||
<div className="system-sm-medium text-components-button-secondary-text">{convertTimezoneToOffsetStr(timezone)}</div>
|
||||
</div>
|
||||
<div className="system-sm-regular text-text-tertiary">{convertTimezoneToOffsetStr(timezone)}</div>
|
||||
</button>
|
||||
)
|
||||
}, [timezone])
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<Popover open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger
|
||||
render={(
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-8 gap-0.5 px-3 system-sm-medium"
|
||||
disabled={disabled}
|
||||
>
|
||||
<span aria-hidden className="i-ri-flashlight-line size-4" />
|
||||
<span className="px-0.5">{t('modelProvider.updateSetting', { ns: 'common' })}</span>
|
||||
<span className="flex min-w-4 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{selectedStrategyLabel}
|
||||
</span>
|
||||
{selectedStrategyLabel && (
|
||||
<span className="flex min-w-4 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{selectedStrategyLabel}
|
||||
</span>
|
||||
)}
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[320px] overflow-hidden rounded-2xl border-t border-components-panel-border bg-components-panel-bg p-0 shadow-xl"
|
||||
>
|
||||
<div className="border-b-[0.5px] border-black/5 py-2">
|
||||
<div className={cn(updateSettingFormClassName, 'w-full')}>
|
||||
<div className={updateSettingFormInputSetClassName}>
|
||||
<div className={updateSettingFormLabelClassName}>
|
||||
{t('autoUpdate.automaticUpdates', { ns: 'plugin' })}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
ariaLabel={t('autoUpdate.automaticUpdates', { ns: 'plugin' })}
|
||||
options={strategyOptions}
|
||||
value={autoUpgrade.strategy_setting}
|
||||
onChange={strategy_setting => updateAutoUpgrade({ strategy_setting })}
|
||||
/>
|
||||
{!disabled && (
|
||||
<PopoverContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
popupClassName="w-[518px] max-w-[calc(100vw-32px)] overflow-hidden rounded-2xl border-t border-components-panel-border bg-components-panel-bg p-0 shadow-xl"
|
||||
>
|
||||
{isSettingsLoading && (
|
||||
<div
|
||||
role="status"
|
||||
className="flex min-h-[96px] items-center justify-center gap-2 px-4 py-6 system-sm-regular text-text-tertiary"
|
||||
>
|
||||
<span aria-hidden className="i-ri-loader-2-line size-4 animate-spin motion-reduce:animate-none" />
|
||||
<span>{t('loading', { ns: 'common' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
{autoUpgrade.strategy_setting !== AUTO_UPDATE_STRATEGY.disabled && (
|
||||
)}
|
||||
{!isSettingsLoading && !hasSettings && (
|
||||
<div className="flex min-h-[96px] items-center justify-center px-4 py-6 text-center system-sm-regular text-text-tertiary">
|
||||
{t('api.actionFailed', { ns: 'common' })}
|
||||
</div>
|
||||
)}
|
||||
{!isSettingsLoading && hasSettings && autoUpgrade && (
|
||||
<>
|
||||
<div className={updateSettingFormClassName}>
|
||||
<div className={updateSettingFormInputSetClassName}>
|
||||
<div className={updateSettingFormLabelClassName}>
|
||||
{t('autoUpdate.updateTime', { ns: 'plugin' })}
|
||||
</div>
|
||||
<div className="flex flex-col items-start">
|
||||
<TimePicker
|
||||
value={updateTimeValue}
|
||||
timezone={timezone}
|
||||
onChange={handleUpdateTimeChange}
|
||||
onClear={() => updateAutoUpgrade({
|
||||
upgrade_time_of_day: convertLocalSecondsToUTCDaySeconds(0, timezone),
|
||||
})}
|
||||
title={t('autoUpdate.updateTime', { ns: 'plugin' })}
|
||||
minuteFilter={minuteFilter}
|
||||
renderTrigger={renderTimePickerTrigger}
|
||||
placement="bottom-end"
|
||||
/>
|
||||
<div className="mt-1 body-xs-regular text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey="autoUpdate.changeTimezone"
|
||||
ns="plugin"
|
||||
components={{
|
||||
setTimezone: <SettingTimeZone />,
|
||||
}}
|
||||
/>
|
||||
<div className="border-b-[0.5px] border-black/5 py-2">
|
||||
<div className={cn(updateSettingFormClassName, 'w-full')}>
|
||||
<div className={cn(updateSettingFormInputSetClassName, 'w-full')}>
|
||||
<div className={updateSettingFormLabelClassName}>
|
||||
{t('autoUpdate.automaticUpdates', { ns: 'plugin' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(updateSettingFormClassName, 'w-full')}>
|
||||
<div className={cn(updateSettingFormInputSetClassName, 'w-full')}>
|
||||
<div className={updateSettingFormLabelClassName}>
|
||||
{t('autoUpdate.scope', { ns: 'plugin' })}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
ariaLabel={t('autoUpdate.scope', { ns: 'plugin' })}
|
||||
options={scopeOptions}
|
||||
value={autoUpgrade.upgrade_mode}
|
||||
onChange={upgrade_mode => updateAutoUpgrade({ upgrade_mode })}
|
||||
/>
|
||||
{autoUpgrade.upgrade_mode !== AUTO_UPDATE_MODE.update_all && (
|
||||
<PluginsPicker
|
||||
value={plugins}
|
||||
onChange={handlePluginsChange}
|
||||
updateMode={autoUpgrade.upgrade_mode}
|
||||
<SegmentedControl
|
||||
ariaLabel={t('autoUpdate.automaticUpdates', { ns: 'plugin' })}
|
||||
className="w-full"
|
||||
options={strategyOptions}
|
||||
value={autoUpgrade.strategy_setting}
|
||||
onChange={strategy_setting => updateAutoUpgrade({ strategy_setting })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{autoUpgrade.strategy_setting !== AUTO_UPDATE_STRATEGY.disabled && (
|
||||
<>
|
||||
<div className={updateSettingFormClassName}>
|
||||
<div className={cn(updateSettingFormInputSetClassName, 'w-full')}>
|
||||
<div className={updateSettingFormLabelClassName}>
|
||||
{t('autoUpdate.updateTime', { ns: 'plugin' })}
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start gap-1">
|
||||
<TimePicker
|
||||
value={updateTimeValue}
|
||||
timezone={timezone}
|
||||
onChange={handleUpdateTimeChange}
|
||||
onClear={() => updateAutoUpgrade({
|
||||
upgrade_time_of_day: convertLocalSecondsToUTCDaySeconds(0, timezone),
|
||||
})}
|
||||
title={t('autoUpdate.updateTime', { ns: 'plugin' })}
|
||||
minuteFilter={minuteFilter}
|
||||
renderTrigger={renderTimePickerTrigger}
|
||||
placement="bottom-end"
|
||||
/>
|
||||
<div className="mt-1 body-xs-regular text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey="autoUpdate.changeTimezone"
|
||||
ns="plugin"
|
||||
components={{
|
||||
setTimezone: <SettingTimeZone />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(updateSettingFormClassName, 'w-full')}>
|
||||
<div className={cn(updateSettingFormInputSetClassName, 'w-full')}>
|
||||
<div className={updateSettingFormLabelClassName}>
|
||||
{t('autoUpdate.scope', { ns: 'plugin' })}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
ariaLabel={t('autoUpdate.scope', { ns: 'plugin' })}
|
||||
className="w-full"
|
||||
options={scopeOptions}
|
||||
value={autoUpgrade.upgrade_mode}
|
||||
onChange={upgrade_mode => updateAutoUpgrade({ upgrade_mode })}
|
||||
/>
|
||||
{autoUpgrade.upgrade_mode !== AUTO_UPDATE_MODE.update_all && (
|
||||
<PluginsPicker
|
||||
value={plugins}
|
||||
onChange={handlePluginsChange}
|
||||
updateMode={autoUpgrade.upgrade_mode}
|
||||
integrationCategory={category}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 px-4 pt-2 pb-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
disabled={isSavePending}
|
||||
>
|
||||
{t('operation.save', { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverContent>
|
||||
)}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@ -10,6 +10,10 @@ import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders, useInvalid
|
||||
import { useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { PluginCategoryEnum } from '../../types'
|
||||
|
||||
type PluginCategoryPayload = {
|
||||
category: PluginCategoryEnum | string
|
||||
}
|
||||
|
||||
const useRefreshPluginList = () => {
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
|
||||
@ -29,7 +33,7 @@ const useRefreshPluginList = () => {
|
||||
|
||||
const invalidateRAGRecommendedPlugins = useInvalidateRAGRecommendedPlugins()
|
||||
return {
|
||||
refreshPluginList: (manifest?: PluginManifestInMarket | Plugin | PluginDeclaration | null, refreshAllType?: boolean) => {
|
||||
refreshPluginList: (manifest?: PluginManifestInMarket | Plugin | PluginDeclaration | PluginCategoryPayload | null, refreshAllType?: boolean) => {
|
||||
// installed list
|
||||
invalidateInstalledPluginList()
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { renderHookWithSystemFeatures as renderHook } from '@/__tests__/utils/mock-system-features'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
|
||||
import { PermissionType } from '../../types'
|
||||
import { useInvalidateReferenceSettings, useMutationPluginPermissionSettings, useMutationReferenceSettings, usePluginAutoUpgradeSettings, usePluginPermissionSettings } from '@/service/use-plugins'
|
||||
import { PermissionType, PluginCategoryEnum } from '../../types'
|
||||
import useReferenceSetting, { useCanInstallPluginFromMarketplace } from '../use-reference-setting'
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
@ -14,7 +14,21 @@ vi.mock('@/context/app-context', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useReferenceSettings: vi.fn(),
|
||||
hasPluginPermission: vi.fn((permission: string | undefined, isAdmin: boolean) => {
|
||||
if (!permission)
|
||||
return false
|
||||
|
||||
if (permission === 'noone')
|
||||
return false
|
||||
|
||||
if (permission === 'everyone')
|
||||
return true
|
||||
|
||||
return isAdmin
|
||||
}),
|
||||
usePluginAutoUpgradeSettings: vi.fn(),
|
||||
usePluginPermissionSettings: vi.fn(),
|
||||
useMutationPluginPermissionSettings: vi.fn(),
|
||||
useMutationReferenceSettings: vi.fn(),
|
||||
useInvalidateReferenceSettings: vi.fn(),
|
||||
}))
|
||||
@ -32,67 +46,77 @@ describe('useReferenceSetting Hook', () => {
|
||||
isCurrentWorkspaceOwner: false,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginAutoUpgradeSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.everyone,
|
||||
category: PluginCategoryEnum.tool,
|
||||
auto_upgrade: {
|
||||
strategy_setting: 'fix_only',
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: 'all',
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
},
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as unknown as ReturnType<typeof usePluginAutoUpgradeSettings>)
|
||||
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.everyone,
|
||||
},
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
vi.mocked(useMutationReferenceSettings).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
} as unknown as ReturnType<typeof useMutationReferenceSettings>)
|
||||
|
||||
vi.mocked(useMutationPluginPermissionSettings).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
isPending: false,
|
||||
} as unknown as ReturnType<typeof useMutationPluginPermissionSettings>)
|
||||
|
||||
vi.mocked(useInvalidateReferenceSettings).mockReturnValue(vi.fn())
|
||||
})
|
||||
|
||||
describe('hasPermission logic', () => {
|
||||
it('should return false when permission is undefined', () => {
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: undefined,
|
||||
debug_permission: undefined,
|
||||
},
|
||||
install_permission: undefined,
|
||||
debug_permission: undefined,
|
||||
},
|
||||
} as unknown as ReturnType<typeof useReferenceSettings>)
|
||||
} as unknown as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canManagement).toBe(false)
|
||||
expect(result.current.canDebugger).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when permission is noOne', () => {
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.noOne,
|
||||
debug_permission: PermissionType.noOne,
|
||||
},
|
||||
install_permission: PermissionType.noOne,
|
||||
debug_permission: PermissionType.noOne,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canManagement).toBe(false)
|
||||
expect(result.current.canDebugger).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when permission is everyone', () => {
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.everyone,
|
||||
},
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.everyone,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canManagement).toBe(true)
|
||||
expect(result.current.canDebugger).toBe(true)
|
||||
@ -104,16 +128,14 @@ describe('useReferenceSetting Hook', () => {
|
||||
isCurrentWorkspaceOwner: false,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
},
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canManagement).toBe(true)
|
||||
expect(result.current.canDebugger).toBe(true)
|
||||
@ -125,16 +147,14 @@ describe('useReferenceSetting Hook', () => {
|
||||
isCurrentWorkspaceOwner: true,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
},
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canManagement).toBe(true)
|
||||
expect(result.current.canDebugger).toBe(true)
|
||||
@ -146,16 +166,14 @@ describe('useReferenceSetting Hook', () => {
|
||||
isCurrentWorkspaceOwner: false,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
},
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canManagement).toBe(false)
|
||||
expect(result.current.canDebugger).toBe(false)
|
||||
@ -169,7 +187,7 @@ describe('useReferenceSetting Hook', () => {
|
||||
isCurrentWorkspaceOwner: false,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canSetPermissions).toBe(true)
|
||||
})
|
||||
@ -180,7 +198,7 @@ describe('useReferenceSetting Hook', () => {
|
||||
isCurrentWorkspaceOwner: true,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canSetPermissions).toBe(true)
|
||||
})
|
||||
@ -191,7 +209,7 @@ describe('useReferenceSetting Hook', () => {
|
||||
isCurrentWorkspaceOwner: false,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canSetPermissions).toBe(false)
|
||||
})
|
||||
@ -211,7 +229,7 @@ describe('useReferenceSetting Hook', () => {
|
||||
} as unknown as ReturnType<typeof useMutationReferenceSettings>
|
||||
})
|
||||
|
||||
renderHook(() => useReferenceSetting())
|
||||
renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
// Trigger the onSuccess callback
|
||||
if (onSuccessCallback)
|
||||
@ -231,12 +249,22 @@ describe('useReferenceSetting Hook', () => {
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.everyone,
|
||||
},
|
||||
auto_upgrade: {
|
||||
strategy_setting: 'fix_only',
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: 'all',
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
},
|
||||
}
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
data: mockData,
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
vi.mocked(usePluginAutoUpgradeSettings).mockReturnValue({
|
||||
data: {
|
||||
category: PluginCategoryEnum.tool,
|
||||
auto_upgrade: mockData.auto_upgrade,
|
||||
},
|
||||
} as unknown as ReturnType<typeof usePluginAutoUpgradeSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.referenceSetting).toEqual(mockData)
|
||||
})
|
||||
@ -247,20 +275,21 @@ describe('useReferenceSetting Hook', () => {
|
||||
isPending: true,
|
||||
} as unknown as ReturnType<typeof useMutationReferenceSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.isUpdatePending).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle null data', () => {
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
data: null,
|
||||
} as unknown as ReturnType<typeof useReferenceSettings>)
|
||||
it('should keep permissions available when reference setting data is still loading', () => {
|
||||
vi.mocked(usePluginAutoUpgradeSettings).mockReturnValue({
|
||||
data: undefined,
|
||||
} as unknown as ReturnType<typeof usePluginAutoUpgradeSettings>)
|
||||
|
||||
const { result } = renderHook(() => useReferenceSetting())
|
||||
const { result } = renderHook(() => useReferenceSetting(PluginCategoryEnum.tool))
|
||||
|
||||
expect(result.current.canManagement).toBe(false)
|
||||
expect(result.current.canDebugger).toBe(false)
|
||||
expect(result.current.referenceSetting).toBeUndefined()
|
||||
expect(result.current.canManagement).toBe(true)
|
||||
expect(result.current.canDebugger).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -274,14 +303,12 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
|
||||
isCurrentWorkspaceOwner: false,
|
||||
} as ReturnType<typeof useAppContext>)
|
||||
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.everyone,
|
||||
},
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.everyone,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
vi.mocked(useMutationReferenceSettings).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
@ -308,14 +335,12 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
|
||||
})
|
||||
|
||||
it('should return false when canManagement is false', () => {
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.noOne,
|
||||
debug_permission: PermissionType.noOne,
|
||||
},
|
||||
install_permission: PermissionType.noOne,
|
||||
debug_permission: PermissionType.noOne,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useCanInstallPluginFromMarketplace(), {
|
||||
systemFeatures: { enable_marketplace: true },
|
||||
@ -325,14 +350,12 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
|
||||
})
|
||||
|
||||
it('should return false when both marketplace is disabled and canManagement is false', () => {
|
||||
vi.mocked(useReferenceSettings).mockReturnValue({
|
||||
vi.mocked(usePluginPermissionSettings).mockReturnValue({
|
||||
data: {
|
||||
permission: {
|
||||
install_permission: PermissionType.noOne,
|
||||
debug_permission: PermissionType.noOne,
|
||||
},
|
||||
install_permission: PermissionType.noOne,
|
||||
debug_permission: PermissionType.noOne,
|
||||
},
|
||||
} as ReturnType<typeof useReferenceSettings>)
|
||||
} as ReturnType<typeof usePluginPermissionSettings>)
|
||||
|
||||
const { result } = renderHook(() => useCanInstallPluginFromMarketplace(), {
|
||||
systemFeatures: { enable_marketplace: false },
|
||||
@ -340,4 +363,13 @@ describe('useCanInstallPluginFromMarketplace Hook', () => {
|
||||
|
||||
expect(result.current.canInstallPluginFromMarketplace).toBe(false)
|
||||
})
|
||||
|
||||
it('should only read plugin permissions and not fetch category auto-upgrade settings', () => {
|
||||
renderHook(() => useCanInstallPluginFromMarketplace(), {
|
||||
systemFeatures: { enable_marketplace: true },
|
||||
})
|
||||
|
||||
expect(usePluginPermissionSettings).toHaveBeenCalled()
|
||||
expect(usePluginAutoUpgradeSettings).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,46 +1,73 @@
|
||||
import type { PluginCategoryEnum } from '../types'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
|
||||
import { PermissionType } from '../types'
|
||||
import { hasPluginPermission, useInvalidateReferenceSettings, useMutationPluginPermissionSettings, useMutationReferenceSettings, usePluginAutoUpgradeSettings, usePluginPermissionSettings } from '@/service/use-plugins'
|
||||
|
||||
const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) => {
|
||||
if (!permission)
|
||||
return false
|
||||
export const useCanSetPluginSettings = () => {
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
|
||||
|
||||
if (permission === PermissionType.noOne)
|
||||
return false
|
||||
|
||||
if (permission === PermissionType.everyone)
|
||||
return true
|
||||
|
||||
return isAdmin
|
||||
return {
|
||||
canSetPermissions: isCurrentWorkspaceManager || isCurrentWorkspaceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
const useReferenceSetting = () => {
|
||||
export const usePluginSettingsAccess = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const { data } = useReferenceSettings()
|
||||
// console.log(data)
|
||||
const { permission: permissions } = data || {}
|
||||
const { canSetPermissions } = useCanSetPluginSettings()
|
||||
const permissionQuery = usePluginPermissionSettings()
|
||||
const { data: permissions } = permissionQuery
|
||||
const { mutate: setPluginPermissionSettings, isPending: isPermissionUpdatePending } = useMutationPluginPermissionSettings({
|
||||
onSuccess: () => {
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
permission: permissions,
|
||||
setPluginPermissionSettings,
|
||||
canManagement: hasPluginPermission(permissions?.install_permission, canSetPermissions),
|
||||
canDebugger: hasPluginPermission(permissions?.debug_permission, canSetPermissions),
|
||||
canSetPermissions,
|
||||
isPermissionLoading: permissionQuery.isLoading || permissionQuery.isFetching,
|
||||
permissionError: permissionQuery.error,
|
||||
isPermissionUpdatePending,
|
||||
}
|
||||
}
|
||||
|
||||
const useReferenceSetting = (category: PluginCategoryEnum) => {
|
||||
const { t } = useTranslation()
|
||||
const permissionAccess = usePluginSettingsAccess()
|
||||
const autoUpgradeQuery = usePluginAutoUpgradeSettings(category)
|
||||
const data = permissionAccess.permission && autoUpgradeQuery.data?.auto_upgrade
|
||||
? {
|
||||
permission: permissionAccess.permission,
|
||||
auto_upgrade: autoUpgradeQuery.data.auto_upgrade,
|
||||
}
|
||||
: undefined
|
||||
const invalidateReferenceSettings = useInvalidateReferenceSettings()
|
||||
const { mutate: updateReferenceSetting, isPending: isUpdatePending } = useMutationReferenceSettings({
|
||||
category,
|
||||
currentReferenceSetting: data,
|
||||
onSuccess: () => {
|
||||
invalidateReferenceSettings()
|
||||
toast.success(t('api.actionSuccess', { ns: 'common' }))
|
||||
},
|
||||
})
|
||||
const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner
|
||||
|
||||
return {
|
||||
referenceSetting: data,
|
||||
setReferenceSettings: updateReferenceSetting,
|
||||
canManagement: hasPermission(permissions?.install_permission, isAdmin),
|
||||
canDebugger: hasPermission(permissions?.debug_permission, isAdmin),
|
||||
canSetPermissions: isAdmin,
|
||||
canManagement: permissionAccess.canManagement,
|
||||
canDebugger: permissionAccess.canDebugger,
|
||||
canSetPermissions: permissionAccess.canSetPermissions,
|
||||
isPermissionLoading: permissionAccess.isPermissionLoading,
|
||||
permissionError: permissionAccess.permissionError,
|
||||
isReferenceSettingLoading: autoUpgradeQuery.isLoading || autoUpgradeQuery.isFetching,
|
||||
referenceSettingError: autoUpgradeQuery.error,
|
||||
isUpdatePending,
|
||||
}
|
||||
}
|
||||
@ -50,7 +77,12 @@ export const useCanInstallPluginFromMarketplace = () => {
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: s => s.enable_marketplace,
|
||||
})
|
||||
const { canManagement } = useReferenceSetting()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const { data: permissions } = usePluginPermissionSettings()
|
||||
const canManagement = hasPluginPermission(
|
||||
permissions?.install_permission,
|
||||
isCurrentWorkspaceManager || isCurrentWorkspaceOwner,
|
||||
)
|
||||
|
||||
const canInstallPluginFromMarketplace = useMemo(() => {
|
||||
return enable_marketplace && canManagement
|
||||
|
||||
@ -80,17 +80,6 @@ vi.mock('../auto-update-setting', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock config default value
|
||||
vi.mock('../auto-update-setting/config', () => ({
|
||||
defaultValue: {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.disabled,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.update_all,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
},
|
||||
}))
|
||||
|
||||
// ================================
|
||||
// Test Data Factories
|
||||
// ================================
|
||||
@ -298,20 +287,6 @@ describe('reference-setting-modal', () => {
|
||||
// Assert
|
||||
expect(screen.getByTestId('auto-update-strategy'))!.toHaveTextContent('latest')
|
||||
})
|
||||
|
||||
it('should use fix-only auto_upgrade when payload.auto_upgrade is undefined', () => {
|
||||
// Arrange
|
||||
const payload = {
|
||||
permission: createMockPermissions(),
|
||||
auto_upgrade: undefined as unknown as AutoUpdateConfig,
|
||||
}
|
||||
|
||||
// Act
|
||||
render(<ReferenceSettingModal {...defaultProps} payload={payload} />)
|
||||
|
||||
// Assert - should use Integrations default value (fix-only)
|
||||
expect(screen.getByTestId('auto-update-strategy'))!.toHaveTextContent('fix_only')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
@ -489,15 +464,6 @@ describe('reference-setting-modal', () => {
|
||||
})
|
||||
|
||||
describe('Edge Cases and Error Handling', () => {
|
||||
it('should handle null payload gracefully', () => {
|
||||
// Arrange
|
||||
const payload = null as unknown as ReferenceSetting
|
||||
|
||||
// Act & Assert - should not crash
|
||||
render(<ReferenceSettingModal {...defaultProps} payload={payload} />)
|
||||
expect(screen.getByText('plugin.privilege.title'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined permission values', () => {
|
||||
// Arrange
|
||||
const payload = {
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { defaultValue } from '../config'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '../types'
|
||||
|
||||
describe('auto-update config', () => {
|
||||
it('provides the expected default auto update value', () => {
|
||||
expect(defaultValue).toEqual({
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.disabled,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.update_all,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -9,7 +9,6 @@ import utc from 'dayjs/plugin/utc'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum, PluginSource } from '../../../types'
|
||||
import { defaultValue } from '../config'
|
||||
import AutoUpdateSetting from '../index'
|
||||
import NoDataPlaceholder from '../no-data-placeholder'
|
||||
import NoPluginSelected from '../no-plugin-selected'
|
||||
@ -352,39 +351,6 @@ describe('auto-update-setting', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('config.ts', () => {
|
||||
describe('defaultValue', () => {
|
||||
it('should have disabled strategy by default', () => {
|
||||
expect(defaultValue.strategy_setting).toBe(AUTO_UPDATE_STRATEGY.disabled)
|
||||
})
|
||||
|
||||
it('should have upgrade_time_of_day as 0', () => {
|
||||
expect(defaultValue.upgrade_time_of_day).toBe(0)
|
||||
})
|
||||
|
||||
it('should have update_all mode by default', () => {
|
||||
expect(defaultValue.upgrade_mode).toBe(AUTO_UPDATE_MODE.update_all)
|
||||
})
|
||||
|
||||
it('should have empty exclude_plugins array', () => {
|
||||
expect(defaultValue.exclude_plugins).toEqual([])
|
||||
})
|
||||
|
||||
it('should have empty include_plugins array', () => {
|
||||
expect(defaultValue.include_plugins).toEqual([])
|
||||
})
|
||||
|
||||
it('should be a complete AutoUpdateConfig object', () => {
|
||||
const keys = Object.keys(defaultValue)
|
||||
expect(keys).toContain('strategy_setting')
|
||||
expect(keys).toContain('upgrade_time_of_day')
|
||||
expect(keys).toContain('upgrade_mode')
|
||||
expect(keys).toContain('exclude_plugins')
|
||||
expect(keys).toContain('include_plugins')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// Utils Tests (Extended coverage beyond utils.spec.ts)
|
||||
// ============================================================
|
||||
|
||||
@ -1,9 +1,32 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import PluginsPicker from '../plugins-picker'
|
||||
import { AUTO_UPDATE_MODE } from '../types'
|
||||
|
||||
const mockToolPicker = vi.fn()
|
||||
const mockInstalledPluginList = vi.hoisted(() => ({
|
||||
data: {
|
||||
plugins: [
|
||||
{
|
||||
plugin_id: 'dify/model-plugin',
|
||||
declaration: { category: 'model' },
|
||||
},
|
||||
{
|
||||
plugin_id: 'dify/tool-plugin',
|
||||
declaration: { category: 'tool' },
|
||||
},
|
||||
{
|
||||
plugin_id: 'dify/datasource-plugin',
|
||||
declaration: { category: 'datasource' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInstalledPluginList: () => mockInstalledPluginList,
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/button', () => ({
|
||||
Button: ({
|
||||
@ -29,6 +52,10 @@ vi.mock('../tool-picker', () => ({
|
||||
}))
|
||||
|
||||
describe('PluginsPicker', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders the empty state when no plugins are selected', () => {
|
||||
render(
|
||||
<PluginsPicker
|
||||
@ -71,12 +98,43 @@ describe('PluginsPicker', () => {
|
||||
updateMode={AUTO_UPDATE_MODE.partial}
|
||||
value={[]}
|
||||
onChange={vi.fn()}
|
||||
integrationCategory={PluginCategoryEnum.model}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('tool-picker')).toBeInTheDocument()
|
||||
expect(mockToolPicker).toHaveBeenCalledWith(expect.objectContaining({
|
||||
trigger: expect.anything(),
|
||||
integrationCategory: PluginCategoryEnum.model,
|
||||
}))
|
||||
})
|
||||
|
||||
it('shows and edits only selected plugins from the provided integration category', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<PluginsPicker
|
||||
updateMode={AUTO_UPDATE_MODE.exclude}
|
||||
value={['dify/model-plugin', 'dify/tool-plugin', 'dify/datasource-plugin']}
|
||||
onChange={onChange}
|
||||
integrationCategory={PluginCategoryEnum.tool}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('plugin.autoUpdate.excludeUpdate:{"num":1}')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('plugins-selected')).toHaveTextContent('dify/tool-plugin')
|
||||
expect(screen.getByTestId('plugins-selected')).not.toHaveTextContent('dify/model-plugin')
|
||||
|
||||
const toolPickerProps = mockToolPicker.mock.lastCall?.[0] as {
|
||||
value: string[]
|
||||
onChange: (value: string[]) => void
|
||||
}
|
||||
expect(toolPickerProps.value).toEqual(['dify/tool-plugin'])
|
||||
|
||||
toolPickerProps.onChange(['dify/new-tool-plugin'])
|
||||
expect(onChange).toHaveBeenCalledWith(['dify/model-plugin', 'dify/datasource-plugin', 'dify/new-tool-plugin'])
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.autoUpdate.operation.clearAll' }))
|
||||
expect(onChange).toHaveBeenLastCalledWith(['dify/model-plugin', 'dify/datasource-plugin'])
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginSource } from '@/app/components/plugins/types'
|
||||
import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types'
|
||||
import ToolPicker from '../tool-picker'
|
||||
|
||||
const mockInstalledPluginList = vi.hoisted(() => ({
|
||||
@ -226,6 +226,35 @@ describe('ToolPicker', () => {
|
||||
expect(screen.getByTestId('search-state')).toHaveTextContent('tool-rag')
|
||||
})
|
||||
|
||||
it('limits selectable integrations to the provided integration category', () => {
|
||||
mockInstalledPluginList.data = {
|
||||
plugins: [
|
||||
createPlugin('model-openai', PluginSource.marketplace, 'model', ['llm']),
|
||||
createPlugin('tool-rag', PluginSource.marketplace, 'tool', ['rag']),
|
||||
createPlugin('datasource-notion', PluginSource.marketplace, 'datasource', ['docs']),
|
||||
],
|
||||
}
|
||||
|
||||
render(
|
||||
<ToolPicker
|
||||
trigger={<span>trigger</span>}
|
||||
value={[]}
|
||||
onChange={vi.fn()}
|
||||
isShow
|
||||
onShowChange={vi.fn()}
|
||||
integrationCategory={PluginCategoryEnum.model}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('plugin.category.models')).toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.category.all')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.category.tools')).not.toBeInTheDocument()
|
||||
expect(screen.getAllByTestId('tool-item')).toHaveLength(1)
|
||||
expect(screen.getByText('model-openai')).toBeInTheDocument()
|
||||
expect(screen.queryByText('tool-rag')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('datasource-notion')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('adds and removes plugin ids from the selection', () => {
|
||||
mockInstalledPluginList.data = {
|
||||
plugins: [
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import type { AutoUpdateConfig } from './types'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from './types'
|
||||
|
||||
export const defaultValue: AutoUpdateConfig = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.disabled,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.update_all,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}
|
||||
@ -14,7 +14,7 @@ const NoPluginSelected: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const text = `${t(`autoUpdate.upgradeModePlaceholder.${updateMode === AUTO_UPDATE_MODE.partial ? 'partial' : 'exclude'}`, { ns: 'plugin' })}`
|
||||
return (
|
||||
<div className="rounded-[10px] border border-components-option-card-option-border bg-background-section p-3 text-center system-xs-regular text-text-tertiary">
|
||||
<div className="text-center system-xs-regular text-text-tertiary">
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { PluginCategoryEnum } from '../../types'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useInstalledPluginList } from '@/service/use-plugins'
|
||||
import NoPluginSelected from './no-plugin-selected'
|
||||
import PluginsSelected from './plugins-selected'
|
||||
import ToolPicker from './tool-picker'
|
||||
@ -16,32 +19,58 @@ type Props = {
|
||||
updateMode: AUTO_UPDATE_MODE
|
||||
value: string[] // plugin ids
|
||||
onChange: (value: string[]) => void
|
||||
integrationCategory?: PluginCategoryEnum
|
||||
}
|
||||
|
||||
const PluginsPicker: FC<Props> = ({
|
||||
updateMode,
|
||||
value,
|
||||
onChange,
|
||||
integrationCategory,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const hasSelected = value.length > 0
|
||||
const { data } = useInstalledPluginList()
|
||||
const pluginCategoryById = useMemo(() => {
|
||||
return new Map(data?.plugins.map(plugin => [plugin.plugin_id, plugin.declaration.category]))
|
||||
}, [data?.plugins])
|
||||
const isCurrentCategoryPlugin = useCallback((pluginId: string) => {
|
||||
if (!integrationCategory)
|
||||
return true
|
||||
|
||||
return pluginCategoryById.get(pluginId) === integrationCategory
|
||||
}, [integrationCategory, pluginCategoryById])
|
||||
const visiblePlugins = useMemo(() => {
|
||||
return value.filter(isCurrentCategoryPlugin)
|
||||
}, [isCurrentCategoryPlugin, value])
|
||||
const hiddenPlugins = useMemo(() => {
|
||||
if (!integrationCategory)
|
||||
return []
|
||||
|
||||
return value.filter(plugin => !isCurrentCategoryPlugin(plugin))
|
||||
}, [integrationCategory, isCurrentCategoryPlugin, value])
|
||||
const hasSelected = visiblePlugins.length > 0
|
||||
const isExcludeMode = updateMode === AUTO_UPDATE_MODE.exclude
|
||||
const handleClear = () => {
|
||||
onChange([])
|
||||
onChange(hiddenPlugins)
|
||||
}
|
||||
const handleVisiblePluginsChange = useCallback((newVisiblePlugins: string[]) => {
|
||||
onChange(integrationCategory
|
||||
? [...hiddenPlugins, ...newVisiblePlugins.filter(plugin => !hiddenPlugins.includes(plugin))]
|
||||
: newVisiblePlugins)
|
||||
}, [hiddenPlugins, integrationCategory, onChange])
|
||||
|
||||
const [isShowToolPicker, {
|
||||
set: setToolPicker,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<div className="mt-2 rounded-[10px] bg-background-section-burn p-2.5">
|
||||
<div className="mt-2 flex w-full flex-col gap-2 rounded-[10px] bg-background-section-burn p-2">
|
||||
{hasSelected
|
||||
? (
|
||||
<div className="flex justify-between text-text-tertiary">
|
||||
<div className="system-xs-medium">{t(`${i18nPrefix}.${isExcludeMode ? 'excludeUpdate' : 'partialUPdate'}`, { ns: 'plugin', num: value.length })}</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="system-xs-regular text-text-tertiary">{t(`${i18nPrefix}.${isExcludeMode ? 'excludeUpdate' : 'partialUPdate'}`, { ns: 'plugin', num: visiblePlugins.length })}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left system-xs-medium text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
className="shrink-0 cursor-pointer border-none bg-transparent p-0 text-left system-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={handleClear}
|
||||
>
|
||||
{t(`${i18nPrefix}.operation.clearAll`, { ns: 'plugin' })}
|
||||
@ -54,22 +83,23 @@ const PluginsPicker: FC<Props> = ({
|
||||
|
||||
{hasSelected && (
|
||||
<PluginsSelected
|
||||
className="mt-2"
|
||||
plugins={value}
|
||||
className="gap-2"
|
||||
plugins={visiblePlugins}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToolPicker
|
||||
trigger={(
|
||||
<Button className="mt-2 w-full" size="small" variant="secondary-accent">
|
||||
<Button className="h-6 w-full gap-1" size="small" variant="secondary-accent">
|
||||
<RiAddLine className="size-3.5" />
|
||||
{t(`${i18nPrefix}.operation.select`, { ns: 'plugin' })}
|
||||
</Button>
|
||||
)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
value={visiblePlugins}
|
||||
onChange={handleVisiblePluginsChange}
|
||||
isShow={isShowToolPicker}
|
||||
onShowChange={setToolPicker}
|
||||
integrationCategory={integrationCategory}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -18,7 +18,7 @@ const PluginsSelected: FC<Props> = ({
|
||||
const isShowAll = plugins.length < MAX_DISPLAY_COUNT
|
||||
const displayPlugins = plugins.slice(0, MAX_DISPLAY_COUNT)
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-1', className)}>
|
||||
<div className={cn('flex items-center', className)}>
|
||||
{displayPlugins.map(plugin => (
|
||||
<Icon key={plugin} size="tiny" src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin}/icon`} />
|
||||
))}
|
||||
|
||||
@ -26,12 +26,12 @@ const ToolItem: FC<Props> = ({
|
||||
return (
|
||||
<div className="p-1">
|
||||
<div
|
||||
className="flex w-full items-center rounded-lg pr-2 select-none hover:bg-state-base-hover"
|
||||
className="flex w-full items-center gap-1 rounded-lg py-1 pr-2 pl-3 select-none hover:bg-state-base-hover"
|
||||
>
|
||||
<div className="flex h-8 grow items-center space-x-2 pr-2 pl-3">
|
||||
<div className="flex min-w-0 grow items-center gap-2 pr-2">
|
||||
<Icon size="tiny" src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin_id}/icon`} />
|
||||
<div className="max-w-[150px] shrink-0 truncate system-sm-medium text-text-primary">{renderI18nObject(label, language)}</div>
|
||||
<div className="max-w-[150px] shrink-0 truncate system-xs-regular text-text-quaternary">{org}</div>
|
||||
<div className="shrink-0 truncate system-sm-medium text-text-secondary">{renderI18nObject(label, language)}</div>
|
||||
<div className="min-w-0 truncate system-xs-regular text-text-quaternary">{org}</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { ActivePluginType } from '../../marketplace/constants'
|
||||
import type { PluginCategoryEnum } from '../../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
Popover,
|
||||
@ -24,6 +25,7 @@ type Props = {
|
||||
onChange: (value: string[]) => void
|
||||
isShow: boolean
|
||||
onShowChange: (isShow: boolean) => void
|
||||
integrationCategory?: PluginCategoryEnum
|
||||
}
|
||||
|
||||
const ToolPicker: FC<Props> = ({
|
||||
@ -32,10 +34,11 @@ const ToolPicker: FC<Props> = ({
|
||||
onChange,
|
||||
isShow,
|
||||
onShowChange,
|
||||
integrationCategory,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const tabs = [
|
||||
const allTabs = [
|
||||
{ key: PLUGIN_TYPE_SEARCH_MAP.all, name: t('category.all', { ns: 'plugin' }) },
|
||||
{ key: PLUGIN_TYPE_SEARCH_MAP.model, name: t('category.models', { ns: 'plugin' }) },
|
||||
{ key: PLUGIN_TYPE_SEARCH_MAP.tool, name: t('category.tools', { ns: 'plugin' }) },
|
||||
@ -45,8 +48,12 @@ const ToolPicker: FC<Props> = ({
|
||||
{ key: PLUGIN_TYPE_SEARCH_MAP.trigger, name: t('category.triggers', { ns: 'plugin' }) },
|
||||
{ key: PLUGIN_TYPE_SEARCH_MAP.bundle, name: t('category.bundles', { ns: 'plugin' }) },
|
||||
]
|
||||
const tabs = integrationCategory
|
||||
? allTabs.filter(tab => tab.key === integrationCategory)
|
||||
: allTabs
|
||||
|
||||
const [pluginType, setPluginType] = useState<ActivePluginType>(PLUGIN_TYPE_SEARCH_MAP.all)
|
||||
const effectivePluginType = integrationCategory ?? pluginType
|
||||
const [query, setQuery] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const { data, isLoading } = useInstalledPluginList()
|
||||
@ -55,12 +62,12 @@ const ToolPicker: FC<Props> = ({
|
||||
return list.filter((plugin) => {
|
||||
const isFromMarketPlace = plugin.source === PluginSource.marketplace
|
||||
return (
|
||||
isFromMarketPlace && (pluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === pluginType)
|
||||
isFromMarketPlace && (effectivePluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === effectivePluginType)
|
||||
&& (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
|
||||
&& (query === '' || plugin.plugin_id.toLowerCase().includes(query.toLowerCase()))
|
||||
)
|
||||
})
|
||||
}, [data, pluginType, query, tags])
|
||||
}, [data, effectivePluginType, query, tags])
|
||||
|
||||
const handleCheckChange = (pluginId: string) => {
|
||||
const newValue = value.includes(pluginId)
|
||||
@ -102,7 +109,7 @@ const ToolPicker: FC<Props> = ({
|
||||
sideOffset={0}
|
||||
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="relative min-h-20 w-full rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-xs">
|
||||
<div className="relative min-h-20 w-[476px] max-w-[calc(100vw-32px)] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs">
|
||||
<div className="p-2 pb-1">
|
||||
<SearchBox
|
||||
search={query}
|
||||
@ -113,14 +120,14 @@ const ToolPicker: FC<Props> = ({
|
||||
inputClassName="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-b-[0.5px] border-divider-subtle bg-background-default-hover px-3 shadow-xs">
|
||||
<div className="flex h-8 items-center space-x-1">
|
||||
<div className="flex items-center justify-between bg-components-panel-bg px-3 pb-2">
|
||||
<div className="flex min-w-0 items-center gap-0.5 overflow-x-auto">
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-6 cursor-pointer items-center rounded-md px-2 hover:bg-state-base-hover',
|
||||
'flex h-6 shrink-0 cursor-pointer items-center rounded-md px-2 hover:bg-state-base-hover',
|
||||
'text-xs font-medium text-text-secondary',
|
||||
pluginType === tab.key && 'bg-state-base-hover-alt',
|
||||
effectivePluginType === tab.key && 'bg-state-base-hover-alt',
|
||||
)}
|
||||
key={tab.key}
|
||||
onClick={() => setPluginType(tab.key)}
|
||||
|
||||
@ -12,23 +12,9 @@ import { PermissionType } from '@/app/components/plugins/types'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import AutoUpdateSetting from './auto-update-setting'
|
||||
import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config'
|
||||
import { AUTO_UPDATE_STRATEGY } from './auto-update-setting/types'
|
||||
import Label from './label'
|
||||
|
||||
const i18nPrefix = 'privilege'
|
||||
const defaultAutoUpdateConfig: AutoUpdateConfig = {
|
||||
...autoUpdateDefaultValue,
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
}
|
||||
|
||||
const getInitialAutoUpdateConfig = (autoUpdateConfig?: AutoUpdateConfig): AutoUpdateConfig => ({
|
||||
...defaultAutoUpdateConfig,
|
||||
...autoUpdateConfig,
|
||||
strategy_setting: autoUpdateConfig?.strategy_setting ?? defaultAutoUpdateConfig.strategy_setting,
|
||||
exclude_plugins: autoUpdateConfig?.exclude_plugins ?? defaultAutoUpdateConfig.exclude_plugins,
|
||||
include_plugins: autoUpdateConfig?.include_plugins ?? defaultAutoUpdateConfig.include_plugins,
|
||||
})
|
||||
|
||||
type Props = {
|
||||
payload: ReferenceSetting
|
||||
@ -42,9 +28,9 @@ const PluginSettingModal: FC<Props> = ({
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {}
|
||||
const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload
|
||||
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege)
|
||||
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(() => getInitialAutoUpdateConfig(autoUpdateConfig))
|
||||
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig)
|
||||
const { data: enable_marketplace } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: s => s.enable_marketplace,
|
||||
|
||||
323
web/service/__tests__/use-plugins.spec.tsx
Normal file
323
web/service/__tests__/use-plugins.spec.tsx
Normal file
@ -0,0 +1,323 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Permissions } from '@/app/components/plugins/types'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import { PermissionType, PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { get, post } from '../base'
|
||||
import {
|
||||
useMutationPluginAutoUpgradeSettings,
|
||||
useMutationPluginPermissionSettings,
|
||||
usePluginAutoUpgradeSettings,
|
||||
} from '../use-plugins'
|
||||
|
||||
vi.mock('../base', () => ({
|
||||
get: vi.fn(),
|
||||
getMarketplace: vi.fn(),
|
||||
post: vi.fn(),
|
||||
postMarketplace: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list', () => ({
|
||||
default: () => ({
|
||||
refreshPluginList: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../use-tools', () => ({
|
||||
useInvalidateAllBuiltInTools: () => vi.fn(),
|
||||
}))
|
||||
|
||||
const createQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const createWrapper = (queryClient: QueryClient) => {
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('use-plugins mutations', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('optimistically updates category auto-upgrade cache before the request finishes', async () => {
|
||||
const queryClient = createQueryClient()
|
||||
const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
|
||||
const previousAutoUpgrade = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.latest,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}
|
||||
const nextAutoUpgrade = {
|
||||
...previousAutoUpgrade,
|
||||
upgrade_time_of_day: 3600,
|
||||
}
|
||||
let resolvePost: (value: unknown) => void = () => {}
|
||||
vi.mocked(post).mockReturnValue(new Promise((resolve) => {
|
||||
resolvePost = resolve
|
||||
}) as ReturnType<typeof post>)
|
||||
queryClient.setQueryData(queryKey, {
|
||||
category: PluginCategoryEnum.model,
|
||||
auto_upgrade: previousAutoUpgrade,
|
||||
})
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
|
||||
{ wrapper: createWrapper(queryClient) },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.mutate(nextAutoUpgrade)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(post).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/change', {
|
||||
body: {
|
||||
category: PluginCategoryEnum.model,
|
||||
auto_upgrade: nextAutoUpgrade,
|
||||
},
|
||||
})
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual({
|
||||
category: PluginCategoryEnum.model,
|
||||
auto_upgrade: nextAutoUpgrade,
|
||||
})
|
||||
})
|
||||
|
||||
resolvePost({})
|
||||
})
|
||||
|
||||
it('optimistically updates plugin permission cache before the request finishes', async () => {
|
||||
const queryClient = createQueryClient()
|
||||
const queryKey = ['plugins', 'referenceSettings', 'permission']
|
||||
const previousPermission: Permissions = {
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
}
|
||||
const nextPermission: Permissions = {
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.admin,
|
||||
}
|
||||
let resolvePost: (value: unknown) => void = () => {}
|
||||
vi.mocked(post).mockReturnValue(new Promise((resolve) => {
|
||||
resolvePost = resolve
|
||||
}) as ReturnType<typeof post>)
|
||||
queryClient.setQueryData(queryKey, previousPermission)
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMutationPluginPermissionSettings(),
|
||||
{ wrapper: createWrapper(queryClient) },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.mutate(nextPermission)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(post).toHaveBeenCalledWith('/workspaces/current/plugin/permission/change', {
|
||||
body: nextPermission,
|
||||
})
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual(nextPermission)
|
||||
})
|
||||
|
||||
resolvePost({})
|
||||
})
|
||||
|
||||
it('rolls back category auto-upgrade cache when the request fails', async () => {
|
||||
const queryClient = createQueryClient()
|
||||
const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
|
||||
const previousAutoUpgrade = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.latest,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}
|
||||
const nextAutoUpgrade = {
|
||||
...previousAutoUpgrade,
|
||||
upgrade_time_of_day: 3600,
|
||||
}
|
||||
let rejectPost: (reason?: unknown) => void = () => {}
|
||||
vi.mocked(post).mockReturnValue(new Promise((_resolve, reject) => {
|
||||
rejectPost = reject
|
||||
}) as ReturnType<typeof post>)
|
||||
queryClient.setQueryData(queryKey, {
|
||||
category: PluginCategoryEnum.model,
|
||||
auto_upgrade: previousAutoUpgrade,
|
||||
})
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
|
||||
{ wrapper: createWrapper(queryClient) },
|
||||
)
|
||||
|
||||
const mutation = result.current.mutateAsync(nextAutoUpgrade).catch(() => undefined)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual({
|
||||
category: PluginCategoryEnum.model,
|
||||
auto_upgrade: nextAutoUpgrade,
|
||||
})
|
||||
})
|
||||
|
||||
rejectPost(new Error('auto-upgrade update failed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual({
|
||||
category: PluginCategoryEnum.model,
|
||||
auto_upgrade: previousAutoUpgrade,
|
||||
})
|
||||
})
|
||||
await mutation
|
||||
})
|
||||
|
||||
it('clears optimistic category auto-upgrade cache when the request fails without previous cache', async () => {
|
||||
const queryClient = createQueryClient()
|
||||
const queryKey = ['plugins', 'referenceSettings', 'autoUpgrade', PluginCategoryEnum.model]
|
||||
const nextAutoUpgrade = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.latest,
|
||||
upgrade_time_of_day: 3600,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}
|
||||
let rejectPost: (reason?: unknown) => void = () => {}
|
||||
vi.mocked(post).mockReturnValue(new Promise((_resolve, reject) => {
|
||||
rejectPost = reject
|
||||
}) as ReturnType<typeof post>)
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMutationPluginAutoUpgradeSettings({ category: PluginCategoryEnum.model }),
|
||||
{ wrapper: createWrapper(queryClient) },
|
||||
)
|
||||
|
||||
const mutation = result.current.mutateAsync(nextAutoUpgrade).catch(() => undefined)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual({
|
||||
category: PluginCategoryEnum.model,
|
||||
auto_upgrade: nextAutoUpgrade,
|
||||
})
|
||||
})
|
||||
|
||||
rejectPost(new Error('auto-upgrade update failed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toBeUndefined()
|
||||
})
|
||||
await mutation
|
||||
})
|
||||
|
||||
it('rolls back plugin permission cache when the request fails', async () => {
|
||||
const queryClient = createQueryClient()
|
||||
const queryKey = ['plugins', 'referenceSettings', 'permission']
|
||||
const previousPermission: Permissions = {
|
||||
install_permission: PermissionType.admin,
|
||||
debug_permission: PermissionType.admin,
|
||||
}
|
||||
const nextPermission: Permissions = {
|
||||
install_permission: PermissionType.everyone,
|
||||
debug_permission: PermissionType.admin,
|
||||
}
|
||||
let rejectPost: (reason?: unknown) => void = () => {}
|
||||
vi.mocked(post).mockReturnValue(new Promise((_resolve, reject) => {
|
||||
rejectPost = reject
|
||||
}) as ReturnType<typeof post>)
|
||||
queryClient.setQueryData(queryKey, previousPermission)
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useMutationPluginPermissionSettings(),
|
||||
{ wrapper: createWrapper(queryClient) },
|
||||
)
|
||||
|
||||
const mutation = result.current.mutateAsync(nextPermission).catch(() => undefined)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual(nextPermission)
|
||||
})
|
||||
|
||||
rejectPost(new Error('permission update failed'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryClient.getQueryData(queryKey)).toEqual(previousPermission)
|
||||
})
|
||||
await mutation
|
||||
})
|
||||
})
|
||||
|
||||
describe('usePluginAutoUpgradeSettings', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('does not expose frontend default settings before backend data resolves', () => {
|
||||
const queryClient = createQueryClient()
|
||||
vi.mocked(get).mockReturnValue(new Promise(() => {}) as ReturnType<typeof get>)
|
||||
|
||||
const { result } = renderHook(
|
||||
() => usePluginAutoUpgradeSettings(PluginCategoryEnum.model),
|
||||
{ wrapper: createWrapper(queryClient) },
|
||||
)
|
||||
|
||||
expect(result.current.data).toBeUndefined()
|
||||
expect(get).toHaveBeenCalledWith('/workspaces/current/plugin/auto-upgrade/fetch', {
|
||||
params: {
|
||||
category: PluginCategoryEnum.model,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('returns backend auto-upgrade settings when the request resolves', async () => {
|
||||
const queryClient = createQueryClient()
|
||||
const backendAutoUpgrade = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.exclude,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}
|
||||
vi.mocked(get).mockResolvedValue({
|
||||
category: PluginCategoryEnum.tool,
|
||||
auto_upgrade: backendAutoUpgrade,
|
||||
})
|
||||
|
||||
const { result } = renderHook(
|
||||
() => usePluginAutoUpgradeSettings(PluginCategoryEnum.tool),
|
||||
{ wrapper: createWrapper(queryClient) },
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.data).toEqual({
|
||||
category: PluginCategoryEnum.tool,
|
||||
auto_upgrade: backendAutoUpgrade,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -4,7 +4,8 @@ import type {
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type {
|
||||
} from '@/app/components/plugins/marketplace/types'
|
||||
AutoUpdateConfig,
|
||||
} from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import type {
|
||||
DebugInfo as DebugInfoTypes,
|
||||
Dependency,
|
||||
@ -13,6 +14,7 @@ import type {
|
||||
InstallPackageResponse,
|
||||
InstallStatusResponse,
|
||||
PackageDependency,
|
||||
Permissions,
|
||||
Plugin,
|
||||
PluginDeclaration,
|
||||
PluginInfoFromMarketPlace,
|
||||
@ -31,11 +33,11 @@ import {
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||
import useReferenceSetting from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import { PluginCategoryEnum, TaskStatus } from '@/app/components/plugins/types'
|
||||
import { PermissionType, PluginCategoryEnum, TaskStatus } from '@/app/components/plugins/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchModelProviderModelList } from '@/service/common'
|
||||
import { fetchPluginInfoFromMarketPlace, uninstallPlugin } from '@/service/plugins'
|
||||
import { get, getMarketplace, post, postMarketplace } from './base'
|
||||
@ -431,14 +433,88 @@ export const useDebugKey = () => {
|
||||
})
|
||||
}
|
||||
|
||||
type PluginAutoUpgradeSettingsResponse = {
|
||||
category: PluginCategoryEnum
|
||||
auto_upgrade: AutoUpdateConfig
|
||||
}
|
||||
|
||||
type MarketplacePluginInfoRequest = {
|
||||
organization?: string
|
||||
plugin?: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
const useReferenceSettingKey = [NAME_SPACE, 'referenceSettings']
|
||||
export const useReferenceSettings = () => {
|
||||
const usePluginPermissionSettingsKey = [...useReferenceSettingKey, 'permission']
|
||||
const usePluginAutoUpgradeSettingsKey = [...useReferenceSettingKey, 'autoUpgrade']
|
||||
const pluginAutoUpgradeSettingsQueryKey = (category: PluginCategoryEnum) => [...usePluginAutoUpgradeSettingsKey, category]
|
||||
|
||||
const areStringArraysEqual = (left: string[], right: string[]) => {
|
||||
return left.length === right.length && left.every((value, index) => value === right[index])
|
||||
}
|
||||
|
||||
const arePermissionsEqual = (left: Permissions | undefined, right: Permissions | undefined) => {
|
||||
return left?.install_permission === right?.install_permission
|
||||
&& left?.debug_permission === right?.debug_permission
|
||||
}
|
||||
|
||||
const areAutoUpgradeSettingsEqual = (left: AutoUpdateConfig | undefined, right: AutoUpdateConfig | undefined) => {
|
||||
return left?.strategy_setting === right?.strategy_setting
|
||||
&& left?.upgrade_time_of_day === right?.upgrade_time_of_day
|
||||
&& left?.upgrade_mode === right?.upgrade_mode
|
||||
&& areStringArraysEqual(left?.exclude_plugins ?? [], right?.exclude_plugins ?? [])
|
||||
&& areStringArraysEqual(left?.include_plugins ?? [], right?.include_plugins ?? [])
|
||||
}
|
||||
|
||||
export const hasPluginPermission = (permission: PermissionType | undefined, isAdmin: boolean) => {
|
||||
if (!permission)
|
||||
return false
|
||||
|
||||
if (permission === PermissionType.noOne)
|
||||
return false
|
||||
|
||||
if (permission === PermissionType.everyone)
|
||||
return true
|
||||
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
export const usePluginPermissionSettings = () => {
|
||||
return useQuery({
|
||||
queryKey: useReferenceSettingKey,
|
||||
queryFn: () => get<ReferenceSetting>('/workspaces/current/plugin/preferences/fetch'),
|
||||
queryKey: usePluginPermissionSettingsKey,
|
||||
queryFn: () => get<Permissions>('/workspaces/current/plugin/permission/fetch'),
|
||||
})
|
||||
}
|
||||
|
||||
export const usePluginAutoUpgradeSettings = (category: PluginCategoryEnum) => {
|
||||
return useQuery({
|
||||
queryKey: pluginAutoUpgradeSettingsQueryKey(category),
|
||||
queryFn: () => get<PluginAutoUpgradeSettingsResponse>(
|
||||
'/workspaces/current/plugin/auto-upgrade/fetch',
|
||||
{ params: { category } },
|
||||
),
|
||||
staleTime: 60 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
export const useReferenceSettings = (category: PluginCategoryEnum) => {
|
||||
const permissionQuery = usePluginPermissionSettings()
|
||||
const autoUpgradeQuery = usePluginAutoUpgradeSettings(category)
|
||||
|
||||
return {
|
||||
...autoUpgradeQuery,
|
||||
data: permissionQuery.data && autoUpgradeQuery.data
|
||||
? {
|
||||
permission: permissionQuery.data,
|
||||
auto_upgrade: autoUpgradeQuery.data.auto_upgrade,
|
||||
} satisfies ReferenceSetting
|
||||
: undefined,
|
||||
error: permissionQuery.error ?? autoUpgradeQuery.error,
|
||||
isLoading: permissionQuery.isLoading || autoUpgradeQuery.isLoading,
|
||||
isFetching: permissionQuery.isFetching || autoUpgradeQuery.isFetching,
|
||||
}
|
||||
}
|
||||
|
||||
export const useInvalidateReferenceSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return () => {
|
||||
@ -450,23 +526,183 @@ export const useInvalidateReferenceSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const useMutationReferenceSettings = ({
|
||||
export const useInvalidatePluginAutoUpgradeSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
return (category: PluginCategoryEnum) => {
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: pluginAutoUpgradeSettingsQueryKey(category),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const useMutationPluginPermissionSettings = ({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess?: () => void
|
||||
} = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: Permissions) => {
|
||||
return post('/workspaces/current/plugin/permission/change', { body: payload })
|
||||
},
|
||||
onMutate: async (payload) => {
|
||||
await queryClient.cancelQueries({ queryKey: usePluginPermissionSettingsKey })
|
||||
const previousPermission = queryClient.getQueryData<Permissions>(usePluginPermissionSettingsKey)
|
||||
const hadPreviousPermission = previousPermission !== undefined
|
||||
|
||||
queryClient.setQueryData(usePluginPermissionSettingsKey, payload)
|
||||
|
||||
return { previousPermission, hadPreviousPermission }
|
||||
},
|
||||
onError: (_error, _payload, context) => {
|
||||
if (context?.hadPreviousPermission)
|
||||
queryClient.setQueryData(usePluginPermissionSettingsKey, context.previousPermission)
|
||||
else
|
||||
queryClient.removeQueries({ queryKey: usePluginPermissionSettingsKey })
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess?.()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useMutationPluginAutoUpgradeSettings = ({
|
||||
category,
|
||||
onSuccess,
|
||||
}: {
|
||||
category: PluginCategoryEnum
|
||||
onSuccess?: () => void
|
||||
}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: AutoUpdateConfig) => {
|
||||
return post('/workspaces/current/plugin/auto-upgrade/change', {
|
||||
body: {
|
||||
category,
|
||||
auto_upgrade: payload,
|
||||
},
|
||||
})
|
||||
},
|
||||
onMutate: async (payload) => {
|
||||
const queryKey = pluginAutoUpgradeSettingsQueryKey(category)
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
const previousAutoUpgrade = queryClient.getQueryData<PluginAutoUpgradeSettingsResponse>(queryKey)
|
||||
const hadPreviousAutoUpgrade = previousAutoUpgrade !== undefined
|
||||
|
||||
queryClient.setQueryData(pluginAutoUpgradeSettingsQueryKey(category), {
|
||||
category,
|
||||
auto_upgrade: payload,
|
||||
} satisfies PluginAutoUpgradeSettingsResponse)
|
||||
|
||||
return { previousAutoUpgrade, hadPreviousAutoUpgrade }
|
||||
},
|
||||
onError: (_error, _payload, context) => {
|
||||
if (context?.hadPreviousAutoUpgrade)
|
||||
queryClient.setQueryData(pluginAutoUpgradeSettingsQueryKey(category), context.previousAutoUpgrade)
|
||||
else
|
||||
queryClient.removeQueries({ queryKey: pluginAutoUpgradeSettingsQueryKey(category) })
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess?.()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useMutationReferenceSettings = ({
|
||||
category,
|
||||
currentReferenceSetting,
|
||||
onSuccess,
|
||||
}: {
|
||||
category: PluginCategoryEnum
|
||||
currentReferenceSetting?: ReferenceSetting
|
||||
onSuccess?: () => void
|
||||
}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: ReferenceSetting) => {
|
||||
return post('/workspaces/current/plugin/preferences/change', { body: payload })
|
||||
const mutations: Array<Promise<unknown>> = []
|
||||
|
||||
if (!arePermissionsEqual(payload.permission, currentReferenceSetting?.permission))
|
||||
mutations.push(post('/workspaces/current/plugin/permission/change', { body: payload.permission }))
|
||||
|
||||
if (!areAutoUpgradeSettingsEqual(payload.auto_upgrade, currentReferenceSetting?.auto_upgrade)) {
|
||||
mutations.push(post('/workspaces/current/plugin/auto-upgrade/change', {
|
||||
body: {
|
||||
category,
|
||||
auto_upgrade: payload.auto_upgrade,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
return Promise.all(mutations)
|
||||
},
|
||||
onMutate: async (payload) => {
|
||||
const shouldUpdatePermission = !arePermissionsEqual(payload.permission, currentReferenceSetting?.permission)
|
||||
const shouldUpdateAutoUpgrade = !areAutoUpgradeSettingsEqual(payload.auto_upgrade, currentReferenceSetting?.auto_upgrade)
|
||||
const autoUpgradeQueryKey = pluginAutoUpgradeSettingsQueryKey(category)
|
||||
|
||||
await Promise.all([
|
||||
shouldUpdatePermission ? queryClient.cancelQueries({ queryKey: usePluginPermissionSettingsKey }) : Promise.resolve(),
|
||||
shouldUpdateAutoUpgrade ? queryClient.cancelQueries({ queryKey: autoUpgradeQueryKey }) : Promise.resolve(),
|
||||
])
|
||||
|
||||
const previousPermission = queryClient.getQueryData<Permissions>(usePluginPermissionSettingsKey)
|
||||
const previousAutoUpgrade = queryClient.getQueryData<PluginAutoUpgradeSettingsResponse>(autoUpgradeQueryKey)
|
||||
const hadPreviousPermission = previousPermission !== undefined
|
||||
const hadPreviousAutoUpgrade = previousAutoUpgrade !== undefined
|
||||
|
||||
if (shouldUpdatePermission)
|
||||
queryClient.setQueryData(usePluginPermissionSettingsKey, payload.permission)
|
||||
|
||||
if (shouldUpdateAutoUpgrade) {
|
||||
queryClient.setQueryData(autoUpgradeQueryKey, {
|
||||
category,
|
||||
auto_upgrade: payload.auto_upgrade,
|
||||
} satisfies PluginAutoUpgradeSettingsResponse)
|
||||
}
|
||||
|
||||
return {
|
||||
previousPermission,
|
||||
previousAutoUpgrade,
|
||||
hadPreviousPermission,
|
||||
hadPreviousAutoUpgrade,
|
||||
shouldUpdatePermission,
|
||||
shouldUpdateAutoUpgrade,
|
||||
}
|
||||
},
|
||||
onError: (_error, _payload, context) => {
|
||||
if (context?.shouldUpdatePermission && context.hadPreviousPermission)
|
||||
queryClient.setQueryData(usePluginPermissionSettingsKey, context.previousPermission)
|
||||
else if (context?.shouldUpdatePermission)
|
||||
queryClient.removeQueries({ queryKey: usePluginPermissionSettingsKey })
|
||||
|
||||
if (context?.shouldUpdateAutoUpgrade && context.hadPreviousAutoUpgrade)
|
||||
queryClient.setQueryData(pluginAutoUpgradeSettingsQueryKey(category), context.previousAutoUpgrade)
|
||||
else if (context?.shouldUpdateAutoUpgrade)
|
||||
queryClient.removeQueries({ queryKey: pluginAutoUpgradeSettingsQueryKey(category) })
|
||||
},
|
||||
onSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
export const useRemoveAutoUpgrade = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (payload: { plugin_id: string }) => {
|
||||
return post('/workspaces/current/plugin/preferences/autoupgrade/exclude', { body: payload })
|
||||
mutationFn: (payload: { plugin_id: string, category: PluginCategoryEnum }) => {
|
||||
return post('/workspaces/current/plugin/auto-upgrade/exclude', { body: payload })
|
||||
},
|
||||
onSuccess: (_data, payload) => {
|
||||
queryClient.invalidateQueries(
|
||||
{
|
||||
queryKey: pluginAutoUpgradeSettingsQueryKey(payload.category),
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -485,7 +721,7 @@ export const useFetchPluginsInMarketPlaceByIds = (unique_identifiers: string[],
|
||||
})
|
||||
}
|
||||
|
||||
export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[]) => {
|
||||
export const useFetchPluginsInMarketPlaceByInfo = (infos: MarketplacePluginInfoRequest[]) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'fetchPluginsInMarketPlaceByInfo', infos],
|
||||
queryFn: () => postMarketplace<{ data: PluginsFromMarketplaceByInfoResponse }>('/plugins/versions/batch', {
|
||||
@ -504,18 +740,15 @@ export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[])
|
||||
|
||||
const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
|
||||
export const usePluginTaskList = (category?: PluginCategoryEnum | string) => {
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
const {
|
||||
canManagement,
|
||||
} = useReferenceSetting()
|
||||
const initializedRef = useRef(false)
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const { data: permissions } = usePluginPermissionSettings()
|
||||
const canManagement = hasPluginPermission(
|
||||
permissions?.install_permission,
|
||||
isCurrentWorkspaceManager || isCurrentWorkspaceOwner,
|
||||
)
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
const {
|
||||
data,
|
||||
isFetched,
|
||||
isRefetching,
|
||||
refetch,
|
||||
...rest
|
||||
} = useQuery({
|
||||
const query = useQuery({
|
||||
enabled: canManagement,
|
||||
queryKey: usePluginTaskListKey,
|
||||
queryFn: () => get<{ tasks: PluginTask[] }>('/workspaces/current/plugin/tasks?page=1&page_size=100'),
|
||||
@ -525,23 +758,25 @@ export const usePluginTaskList = (category?: PluginCategoryEnum | string) => {
|
||||
return taskDone ? false : 5000
|
||||
},
|
||||
})
|
||||
const { data, isFetched, isRefetching, refetch } = query
|
||||
|
||||
useEffect(() => {
|
||||
// After first fetch, refresh plugin list each time all tasks are done
|
||||
// Skip initialization period, because the query cache is not updated yet
|
||||
if (!initialized || isRefetching)
|
||||
if (!initializedRef.current) {
|
||||
initializedRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
if (isRefetching)
|
||||
return
|
||||
|
||||
const lastData = cloneDeep(data)
|
||||
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
|
||||
const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
|
||||
if (taskDone && lastData?.tasks.length && !taskAllFailed)
|
||||
refreshPluginList(category ? { category } as any : undefined, !category)
|
||||
}, [isRefetching])
|
||||
|
||||
useEffect(() => {
|
||||
setInitialized(true)
|
||||
}, [])
|
||||
refreshPluginList(category ? { category } : undefined, !category)
|
||||
}, [category, data, isRefetching, refreshPluginList])
|
||||
|
||||
const handleRefetch = useCallback(() => {
|
||||
refetch()
|
||||
@ -552,7 +787,6 @@ export const usePluginTaskList = (category?: PluginCategoryEnum | string) => {
|
||||
pluginTasks: data?.tasks || [],
|
||||
isFetched,
|
||||
handleRefetch,
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
@ -631,7 +865,7 @@ export const usePluginInfo = (providerName?: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type?: string, extra?: Record<string, any>) => {
|
||||
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type?: string, extra?: Record<string, unknown>) => {
|
||||
return useMutation({
|
||||
mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
|
||||
params: {
|
||||
@ -656,10 +890,11 @@ export const usePluginReadme = ({ plugin_unique_identifier, language }: { plugin
|
||||
}
|
||||
|
||||
export const usePluginReadmeAsset = ({ file_name, plugin_unique_identifier }: { file_name?: string, plugin_unique_identifier?: string }) => {
|
||||
const normalizedFileName = file_name?.replace(/(^\.\/_assets\/|^_assets\/)/, '')
|
||||
const normalizedFileName = file_name?.replace(/^\.\/_assets\//, '').replace(/^_assets\//, '')
|
||||
const isAssetFile = file_name?.startsWith('./_assets') || file_name?.startsWith('_assets')
|
||||
return useQuery({
|
||||
queryKey: ['pluginReadmeAsset', plugin_unique_identifier, normalizedFileName],
|
||||
queryFn: () => get<Blob>('/workspaces/current/plugin/asset', { params: { plugin_unique_identifier, file_name: normalizedFileName } }, { silent: true }),
|
||||
enabled: !!plugin_unique_identifier && !!file_name && /(^\.\/_assets|^_assets)/.test(file_name),
|
||||
enabled: !!plugin_unique_identifier && !!isAssetFile,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user