feat: plugin auto upgrade strategy (#19758)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Novice <novice12185727@gmail.com>
This commit is contained in:

committed by
GitHub

parent
e6913744ae
commit
eaae79a581
@@ -10,7 +10,7 @@ import type { ExposeRefs } from './install-multi'
|
||||
import InstallMulti from './install-multi'
|
||||
import { useInstallOrUpdate } from '@/service/use-plugins'
|
||||
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
|
||||
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
|
||||
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import { useMittContextSelector } from '@/context/mitt-context'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
const i18nPrefix = 'plugin.installModal'
|
||||
|
@@ -40,6 +40,11 @@ import { PluginAuth } from '@/app/components/plugins/plugin-auth'
|
||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import DeprecationNotice from '../base/deprecation-notice'
|
||||
import { AutoUpdateLine } from '../../base/icons/src/vender/system'
|
||||
import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../reference-setting-modal/auto-update-setting/utils'
|
||||
import useReferenceSetting from '../plugin-page/use-reference-setting'
|
||||
import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
const i18nPrefix = 'plugin.action'
|
||||
|
||||
@@ -55,6 +60,8 @@ const DetailHeader = ({
|
||||
onUpdate,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
|
||||
const { theme } = useTheme()
|
||||
const locale = useGetLanguage()
|
||||
const { locale: currentLocale } = useI18N()
|
||||
@@ -112,8 +119,24 @@ const DetailHeader = ({
|
||||
setFalse: hideUpdateModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleUpdate = async () => {
|
||||
const { referenceSetting } = useReferenceSetting()
|
||||
const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {}
|
||||
const isAutoUpgradeEnabled = useMemo(() => {
|
||||
if (!autoUpgradeInfo || !isFromMarketplace)
|
||||
return false
|
||||
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.update_all)
|
||||
return true
|
||||
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.partial && autoUpgradeInfo.include_plugins.includes(plugin_id))
|
||||
return true
|
||||
if(autoUpgradeInfo.upgrade_mode === AUTO_UPDATE_MODE.exclude && !autoUpgradeInfo.exclude_plugins.includes(plugin_id))
|
||||
return true
|
||||
return false
|
||||
}, [autoUpgradeInfo, plugin_id, isFromMarketplace])
|
||||
|
||||
const [isDowngrade, setIsDowngrade] = useState(false)
|
||||
const handleUpdate = async (isDowngrade?: boolean) => {
|
||||
if (isFromMarketplace) {
|
||||
setIsDowngrade(!!isDowngrade)
|
||||
showUpdateModal()
|
||||
return
|
||||
}
|
||||
@@ -180,9 +203,6 @@ const DetailHeader = ({
|
||||
}
|
||||
}, [showDeleting, installation_id, hideDeleting, hideDeleteConfirm, onUpdate, category, refreshModelProviders, invalidateAllToolProviders])
|
||||
|
||||
// #plugin TODO# used in apps
|
||||
// const usedInApps = 3
|
||||
|
||||
return (
|
||||
<div className={cn('shrink-0 border-b border-divider-subtle bg-components-panel-bg p-4 pb-3')}>
|
||||
<div className="flex">
|
||||
@@ -201,7 +221,7 @@ const DetailHeader = ({
|
||||
currentVersion={version}
|
||||
onSelect={(state) => {
|
||||
setTargetVersion(state)
|
||||
handleUpdate()
|
||||
handleUpdate(state.isDowngrade)
|
||||
}}
|
||||
trigger={
|
||||
<Badge
|
||||
@@ -221,6 +241,18 @@ const DetailHeader = ({
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* Auto update info */}
|
||||
{isAutoUpgradeEnabled && (
|
||||
<Tooltip popupContent={t('plugin.autoUpdate.nextUpdateTime', { time: timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(autoUpgradeInfo?.upgrade_time_of_day || 0, timezone!)).format('hh:mm A') })}>
|
||||
{/* add a a div to fix tooltip hover not show problem */}
|
||||
<div>
|
||||
<Badge className='mr-1 cursor-pointer px-1'>
|
||||
<AutoUpdateLine className='size-3' />
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{(hasNewVersion || isFromGitHub) && (
|
||||
<Button variant='secondary-accent' size='small' className='!h-5' onClick={() => {
|
||||
if (isFromMarketplace) {
|
||||
@@ -324,6 +356,7 @@ const DetailHeader = ({
|
||||
{
|
||||
isShowUpdateModal && (
|
||||
<UpdateFromMarketplace
|
||||
pluginId={plugin_id}
|
||||
payload={{
|
||||
category: detail.declaration.category,
|
||||
originalPackageInfo: {
|
||||
@@ -337,6 +370,7 @@ const DetailHeader = ({
|
||||
}}
|
||||
onCancel={hideUpdateModal}
|
||||
onSave={handleUpdatedFromMarketplace}
|
||||
isShowDowngradeWarningModal={isDowngrade && isAutoUpgradeEnabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@@ -17,14 +17,14 @@ import {
|
||||
} from './context'
|
||||
import InstallPluginDropdown from './install-plugin-dropdown'
|
||||
import { useUploader } from './use-uploader'
|
||||
import usePermission from './use-permission'
|
||||
import useReferenceSetting from './use-reference-setting'
|
||||
import DebugInfo from './debug-info'
|
||||
import PluginTasks from './plugin-tasks'
|
||||
import Button from '@/app/components/base/button'
|
||||
import TabSlider from '@/app/components/base/tab-slider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import PermissionSetModal from '@/app/components/plugins/permission-setting-modal/modal'
|
||||
import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal/modal'
|
||||
import InstallFromMarketplace from '../install-plugin/install-from-marketplace'
|
||||
import {
|
||||
useRouter,
|
||||
@@ -121,16 +121,16 @@ const PluginPage = ({
|
||||
}, [packageId, bundleInfo])
|
||||
|
||||
const {
|
||||
referenceSetting,
|
||||
canManagement,
|
||||
canDebugger,
|
||||
canSetPermissions,
|
||||
permissions,
|
||||
setPermissions,
|
||||
} = usePermission()
|
||||
setReferenceSettings,
|
||||
} = useReferenceSetting()
|
||||
const [showPluginSettingModal, {
|
||||
setTrue: setShowPluginSettingModal,
|
||||
setFalse: setHidePluginSettingModal,
|
||||
}] = useBoolean()
|
||||
}] = useBoolean(false)
|
||||
const [currentFile, setCurrentFile] = useState<File | null>(null)
|
||||
const containerRef = usePluginPageContext(v => v.containerRef)
|
||||
const options = usePluginPageContext(v => v.options)
|
||||
@@ -276,10 +276,10 @@ const PluginPage = ({
|
||||
}
|
||||
|
||||
{showPluginSettingModal && (
|
||||
<PermissionSetModal
|
||||
payload={permissions!}
|
||||
<ReferenceSettingModal
|
||||
payload={referenceSetting!}
|
||||
onHide={setHidePluginSettingModal}
|
||||
onSave={setPermissions}
|
||||
onSave={setReferenceSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import { PermissionType } from '../types'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Toast from '../../base/toast'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins'
|
||||
import { useInvalidateReferenceSettings, useMutationReferenceSettings, useReferenceSettings } from '@/service/use-plugins'
|
||||
import { useMemo } from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
@@ -19,14 +19,16 @@ const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean)
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
const usePermission = () => {
|
||||
const useReferenceSetting = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const { data: permissions } = usePermissions()
|
||||
const invalidatePermissions = useInvalidatePermissions()
|
||||
const { mutate: updatePermission, isPending: isUpdatePending } = useMutationPermissions({
|
||||
const { data } = useReferenceSettings()
|
||||
// console.log(data)
|
||||
const { permission: permissions } = data || {}
|
||||
const invalidateReferenceSettings = useInvalidateReferenceSettings()
|
||||
const { mutate: updateReferenceSetting, isPending: isUpdatePending } = useMutationReferenceSettings({
|
||||
onSuccess: () => {
|
||||
invalidatePermissions()
|
||||
invalidateReferenceSettings()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
@@ -36,18 +38,18 @@ const usePermission = () => {
|
||||
const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner
|
||||
|
||||
return {
|
||||
referenceSetting: data,
|
||||
setReferenceSettings: updateReferenceSetting,
|
||||
canManagement: hasPermission(permissions?.install_permission, isAdmin),
|
||||
canDebugger: hasPermission(permissions?.debug_permission, isAdmin),
|
||||
canSetPermissions: isAdmin,
|
||||
permissions,
|
||||
setPermissions: updatePermission,
|
||||
isUpdatePending,
|
||||
}
|
||||
}
|
||||
|
||||
export const useCanInstallPluginFromMarketplace = () => {
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { canManagement } = usePermission()
|
||||
const { canManagement } = useReferenceSetting()
|
||||
|
||||
const canInstallPluginFromMarketplace = useMemo(() => {
|
||||
return enable_marketplace && canManagement
|
||||
@@ -58,4 +60,4 @@ export const useCanInstallPluginFromMarketplace = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export default usePermission
|
||||
export default useReferenceSetting
|
@@ -0,0 +1,9 @@
|
||||
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: [],
|
||||
}
|
@@ -0,0 +1,185 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { AUTO_UPDATE_MODE, AUTO_UPDATE_STRATEGY, type AutoUpdateConfig } from './types'
|
||||
import Label from '../label'
|
||||
import StrategyPicker from './strategy-picker'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import PluginsPicker from './plugins-picker'
|
||||
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds, dayjsToTimeOfDay, timeOfDayToDayjs } from './utils'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
|
||||
import { RiTimeLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
|
||||
const i18nPrefix = 'plugin.autoUpdate'
|
||||
|
||||
type Props = {
|
||||
payload: AutoUpdateConfig
|
||||
onChange: (payload: AutoUpdateConfig) => void
|
||||
}
|
||||
|
||||
const SettingTimeZone: FC<{
|
||||
children?: React.ReactNode
|
||||
}> = ({
|
||||
children,
|
||||
}) => {
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
return (
|
||||
<span className='body-xs-regular cursor-pointer text-text-accent' onClick={() => setShowAccountSettingModal({ payload: 'language' })} >{children}</span>
|
||||
)
|
||||
}
|
||||
const AutoUpdateSetting: FC<Props> = ({
|
||||
payload,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile: { timezone } } = useAppContext()
|
||||
|
||||
const {
|
||||
strategy_setting,
|
||||
upgrade_time_of_day,
|
||||
upgrade_mode,
|
||||
exclude_plugins,
|
||||
include_plugins,
|
||||
} = payload
|
||||
|
||||
const minuteFilter = useCallback((minutes: string[]) => {
|
||||
return minutes.filter((m) => {
|
||||
const time = Number.parseInt(m, 10)
|
||||
return time % 15 === 0
|
||||
})
|
||||
}, [])
|
||||
const strategyDescription = useMemo(() => {
|
||||
switch (strategy_setting) {
|
||||
case AUTO_UPDATE_STRATEGY.fixOnly:
|
||||
return t(`${i18nPrefix}.strategy.fixOnly.selectedDescription`)
|
||||
case AUTO_UPDATE_STRATEGY.latest:
|
||||
return t(`${i18nPrefix}.strategy.latest.selectedDescription`)
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}, [strategy_setting, t])
|
||||
|
||||
const plugins = useMemo(() => {
|
||||
switch (upgrade_mode) {
|
||||
case AUTO_UPDATE_MODE.partial:
|
||||
return include_plugins
|
||||
case AUTO_UPDATE_MODE.exclude:
|
||||
return exclude_plugins
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}, [upgrade_mode, exclude_plugins, include_plugins])
|
||||
|
||||
const handlePluginsChange = useCallback((newPlugins: string[]) => {
|
||||
if (upgrade_mode === AUTO_UPDATE_MODE.partial) {
|
||||
onChange({
|
||||
...payload,
|
||||
include_plugins: newPlugins,
|
||||
})
|
||||
}
|
||||
else if (upgrade_mode === AUTO_UPDATE_MODE.exclude) {
|
||||
onChange({
|
||||
...payload,
|
||||
exclude_plugins: newPlugins,
|
||||
})
|
||||
}
|
||||
}, [payload, upgrade_mode, onChange])
|
||||
const handleChange = useCallback((key: keyof AutoUpdateConfig) => {
|
||||
return (value: AutoUpdateConfig[keyof AutoUpdateConfig]) => {
|
||||
onChange({
|
||||
...payload,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
}, [payload, onChange])
|
||||
|
||||
const renderTimePickerTrigger = useCallback(({ inputElem, onClick, isOpen }: TriggerParams) => {
|
||||
return (
|
||||
<div
|
||||
className='group float-right flex h-8 w-[160px] cursor-pointer items-center justify-between rounded-lg bg-components-input-bg-normal px-2 hover:bg-state-base-hover-alt'
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex w-0 grow items-center gap-x-1'>
|
||||
<RiTimeLine className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-tertiary',
|
||||
isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary',
|
||||
)} />
|
||||
{inputElem}
|
||||
</div>
|
||||
<div className='system-sm-regular text-text-tertiary'>{convertTimezoneToOffsetStr(timezone)}</div>
|
||||
</div>
|
||||
)
|
||||
}, [timezone])
|
||||
|
||||
return (
|
||||
<div className='self-stretch px-6'>
|
||||
<div className='my-3 flex items-center'>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>{t(`${i18nPrefix}.updateSettings`)}</div>
|
||||
<div className='ml-2 h-px grow bg-divider-subtle'></div>
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label label={t(`${i18nPrefix}.automaticUpdates`)} description={strategyDescription} />
|
||||
<StrategyPicker value={strategy_setting} onChange={handleChange('strategy_setting')} />
|
||||
</div>
|
||||
{strategy_setting !== AUTO_UPDATE_STRATEGY.disabled && (
|
||||
<>
|
||||
<div className='flex items-center justify-between'>
|
||||
<Label label={t(`${i18nPrefix}.updateTime`)} />
|
||||
<div className='flex flex-col justify-start'>
|
||||
<TimePicker
|
||||
value={timeOfDayToDayjs(convertUTCDaySecondsToLocalSeconds(upgrade_time_of_day, timezone!))}
|
||||
timezone={timezone}
|
||||
onChange={v => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(dayjsToTimeOfDay(v), timezone!))}
|
||||
onClear={() => handleChange('upgrade_time_of_day')(convertLocalSecondsToUTCDaySeconds(0, timezone!))}
|
||||
popupClassName='z-[99]'
|
||||
title={t(`${i18nPrefix}.updateTime`)}
|
||||
minuteFilter={minuteFilter}
|
||||
renderTrigger={renderTimePickerTrigger}
|
||||
/>
|
||||
<div className='body-xs-regular mt-1 text-right text-text-tertiary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.changeTimezone`}
|
||||
components={{
|
||||
setTimezone: <SettingTimeZone />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label label={t(`${i18nPrefix}.specifyPluginsToUpdate`)} />
|
||||
<div className='mt-1 flex w-full items-start justify-between gap-2'>
|
||||
{[AUTO_UPDATE_MODE.update_all, AUTO_UPDATE_MODE.exclude, AUTO_UPDATE_MODE.partial].map(option => (
|
||||
<OptionCard
|
||||
key={option}
|
||||
title={t(`${i18nPrefix}.upgradeMode.${option}`)}
|
||||
onSelect={() => handleChange('upgrade_mode')(option)}
|
||||
selected={upgrade_mode === option}
|
||||
className="flex-1"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{upgrade_mode !== AUTO_UPDATE_MODE.update_all && (
|
||||
<PluginsPicker
|
||||
value={plugins}
|
||||
onChange={handlePluginsChange}
|
||||
updateMode={upgrade_mode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AutoUpdateSetting)
|
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Group } from '@/app/components/base/icons/src/vender/other'
|
||||
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
className: string
|
||||
noPlugins?: boolean
|
||||
}
|
||||
|
||||
const NoDataPlaceholder: FC<Props> = ({
|
||||
className,
|
||||
noPlugins,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const icon = noPlugins ? (<Group className='size-6 text-text-quaternary' />) : (<SearchMenu className='size-8 text-text-tertiary' />)
|
||||
const text = t(`plugin.autoUpdate.noPluginPlaceholder.${noPlugins ? 'noInstalled' : 'noFound'}`)
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center', className)}>
|
||||
<div className='flex flex-col items-center'>
|
||||
{icon}
|
||||
<div className='system-sm-regular mt-2 text-text-tertiary'>{text}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NoDataPlaceholder)
|
@@ -0,0 +1,22 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { AUTO_UPDATE_MODE } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
updateMode: AUTO_UPDATE_MODE
|
||||
}
|
||||
|
||||
const NoPluginSelected: FC<Props> = ({
|
||||
updateMode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const text = `${t(`plugin.autoUpdate.upgradeModePlaceholder.${updateMode === AUTO_UPDATE_MODE.partial ? 'partial' : 'exclude'}`)}`
|
||||
return (
|
||||
<div className='system-xs-regular rounded-[10px] border border-[divider-subtle] bg-background-section p-3 text-center text-text-tertiary'>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoPluginSelected)
|
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import NoPluginSelected from './no-plugin-selected'
|
||||
import { AUTO_UPDATE_MODE } from './types'
|
||||
import PluginsSelected from './plugins-selected'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import ToolPicker from './tool-picker'
|
||||
|
||||
const i18nPrefix = 'plugin.autoUpdate'
|
||||
|
||||
type Props = {
|
||||
updateMode: AUTO_UPDATE_MODE
|
||||
value: string[] // plugin ids
|
||||
onChange: (value: string[]) => void
|
||||
}
|
||||
|
||||
const PluginsPicker: FC<Props> = ({
|
||||
updateMode,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const hasSelected = value.length > 0
|
||||
const isExcludeMode = updateMode === AUTO_UPDATE_MODE.exclude
|
||||
const handleClear = () => {
|
||||
onChange([])
|
||||
}
|
||||
|
||||
const [isShowToolPicker, {
|
||||
set: setToolPicker,
|
||||
}] = useBoolean(false)
|
||||
return (
|
||||
<div className='mt-2 rounded-[10px] bg-background-section-burn p-2.5'>
|
||||
{hasSelected ? (
|
||||
<div className='flex justify-between text-text-tertiary'>
|
||||
<div className='system-xs-medium'>{t(`${i18nPrefix}.${isExcludeMode ? 'excludeUpdate' : 'partialUPdate'}`, { num: value.length })}</div>
|
||||
<div className='system-xs-medium cursor-pointer' onClick={handleClear}>{t(`${i18nPrefix}.operation.clearAll`)}</div>
|
||||
</div>
|
||||
) : (
|
||||
<NoPluginSelected updateMode={updateMode} />
|
||||
)}
|
||||
|
||||
{hasSelected && (
|
||||
<PluginsSelected
|
||||
className='mt-2'
|
||||
plugins={value}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToolPicker
|
||||
trigger={
|
||||
<Button className='mt-2 w-[412px]' size='small' variant='secondary-accent'>
|
||||
<RiAddLine className='size-3.5' />
|
||||
{t(`${i18nPrefix}.operation.select`)}
|
||||
</Button>
|
||||
}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isShow={isShowToolPicker}
|
||||
onShowChange={setToolPicker}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PluginsPicker)
|
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
|
||||
const MAX_DISPLAY_COUNT = 14
|
||||
type Props = {
|
||||
className?: string
|
||||
plugins: string[]
|
||||
}
|
||||
|
||||
const PluginsSelected: FC<Props> = ({
|
||||
className,
|
||||
plugins,
|
||||
}) => {
|
||||
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)}>
|
||||
{displayPlugins.map(plugin => (
|
||||
<Icon key={plugin} size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin}/icon`} />
|
||||
))}
|
||||
{!isShowAll && <div className='system-xs-medium text-text-tertiary'>+{plugins.length - MAX_DISPLAY_COUNT}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PluginsSelected)
|
@@ -0,0 +1,98 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiCheckLine,
|
||||
} from '@remixicon/react'
|
||||
import { AUTO_UPDATE_STRATEGY } from './types'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
const i18nPrefix = 'plugin.autoUpdate.strategy'
|
||||
|
||||
type Props = {
|
||||
value: AUTO_UPDATE_STRATEGY
|
||||
onChange: (value: AUTO_UPDATE_STRATEGY) => void
|
||||
}
|
||||
const StrategyPicker = ({
|
||||
value,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const options = [
|
||||
{
|
||||
value: AUTO_UPDATE_STRATEGY.disabled,
|
||||
label: t(`${i18nPrefix}.disabled.name`),
|
||||
description: t(`${i18nPrefix}.disabled.description`),
|
||||
},
|
||||
{
|
||||
value: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
label: t(`${i18nPrefix}.fixOnly.name`),
|
||||
description: t(`${i18nPrefix}.fixOnly.description`),
|
||||
},
|
||||
{
|
||||
value: AUTO_UPDATE_STRATEGY.latest,
|
||||
label: t(`${i18nPrefix}.latest.name`),
|
||||
description: t(`${i18nPrefix}.latest.description`),
|
||||
},
|
||||
]
|
||||
const selectedOption = options.find(option => option.value === value)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='top-end'
|
||||
offset={4}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setOpen(v => !v)
|
||||
}}>
|
||||
<Button
|
||||
size='small'
|
||||
>
|
||||
{selectedOption?.label}
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[99]'>
|
||||
<div className='w-[280px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className='flex cursor-pointer rounded-lg p-2 pr-3 hover:bg-state-base-hover'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
onChange(option.value)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className='mr-1 w-4 shrink-0'>
|
||||
{
|
||||
value === option.value && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='system-sm-semibold mb-0.5 text-text-secondary'>{option.label}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{option.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default StrategyPicker
|
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import Icon from '@/app/components/plugins/card/base/card-icon'
|
||||
import { renderI18nObject } from '@/i18n'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { MARKETPLACE_API_PREFIX } from '@/config'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
|
||||
type Props = {
|
||||
payload: PluginDetail
|
||||
isChecked?: boolean
|
||||
onCheckChange: () => void
|
||||
}
|
||||
|
||||
const ToolItem: FC<Props> = ({
|
||||
payload,
|
||||
isChecked,
|
||||
onCheckChange,
|
||||
}) => {
|
||||
const language = useGetLanguage()
|
||||
|
||||
const { plugin_id, declaration } = payload
|
||||
const { label, author: org } = declaration
|
||||
return (
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex w-full select-none items-center rounded-lg pr-2 hover:bg-state-base-hover'
|
||||
>
|
||||
<div className='flex h-8 grow items-center space-x-2 pl-3 pr-2'>
|
||||
<Icon size='tiny' src={`${MARKETPLACE_API_PREFIX}/plugins/${plugin_id}/icon`} />
|
||||
<div className='system-sm-medium max-w-[150px] shrink-0 truncate text-text-primary'>{renderI18nObject(label, language)}</div>
|
||||
<div className='system-xs-regular max-w-[150px] shrink-0 truncate text-text-quaternary'>{org}</div>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={isChecked}
|
||||
onCheck={onCheckChange}
|
||||
className='shrink-0'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ToolItem)
|
@@ -0,0 +1,167 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useInstalledPluginList } from '@/service/use-plugins'
|
||||
import { PLUGIN_TYPE_SEARCH_MAP } from '../../marketplace/plugin-type-switch'
|
||||
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
import ToolItem from './tool-item'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import NoDataPlaceholder from './no-data-placeholder'
|
||||
import { PluginSource } from '../../types'
|
||||
|
||||
type Props = {
|
||||
trigger: React.ReactNode
|
||||
value: string[]
|
||||
onChange: (value: string[]) => void
|
||||
isShow: boolean
|
||||
onShowChange: (isShow: boolean) => void
|
||||
|
||||
}
|
||||
|
||||
const ToolPicker: FC<Props> = ({
|
||||
trigger,
|
||||
value,
|
||||
onChange,
|
||||
isShow,
|
||||
onShowChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const toggleShowPopup = useCallback(() => {
|
||||
onShowChange(!isShow)
|
||||
}, [onShowChange, isShow])
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.all,
|
||||
name: t('plugin.category.all'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.model,
|
||||
name: t('plugin.category.models'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.tool,
|
||||
name: t('plugin.category.tools'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.agent,
|
||||
name: t('plugin.category.agents'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.extension,
|
||||
name: t('plugin.category.extensions'),
|
||||
},
|
||||
{
|
||||
key: PLUGIN_TYPE_SEARCH_MAP.bundle,
|
||||
name: t('plugin.category.bundles'),
|
||||
},
|
||||
]
|
||||
|
||||
const [pluginType, setPluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all)
|
||||
const [query, setQuery] = useState('')
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const { data, isLoading } = useInstalledPluginList()
|
||||
const filteredList = useMemo(() => {
|
||||
const list = data ? data.plugins : []
|
||||
return list.filter((plugin) => {
|
||||
const isFromMarketPlace = plugin.source === PluginSource.marketplace
|
||||
return (
|
||||
isFromMarketPlace && (pluginType === PLUGIN_TYPE_SEARCH_MAP.all || plugin.declaration.category === pluginType)
|
||||
&& (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
|
||||
&& (query === '' || plugin.plugin_id.toLowerCase().includes(query.toLowerCase()))
|
||||
)
|
||||
})
|
||||
}, [data, pluginType, query, tags])
|
||||
const handleCheckChange = useCallback((pluginId: string) => {
|
||||
return () => {
|
||||
const newValue = value.includes(pluginId)
|
||||
? value.filter(id => id !== pluginId)
|
||||
: [...value, pluginId]
|
||||
onChange(newValue)
|
||||
}
|
||||
}, [onChange, value])
|
||||
|
||||
const listContent = (
|
||||
<div className='max-h-[396px] overflow-y-auto'>
|
||||
{filteredList.map(item => (
|
||||
<ToolItem
|
||||
key={item.plugin_id}
|
||||
payload={item}
|
||||
isChecked={value.includes(item.plugin_id)}
|
||||
onCheckChange={handleCheckChange(item.plugin_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const loadingContent = (
|
||||
<div className='flex h-[396px] items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
|
||||
const noData = (
|
||||
<NoDataPlaceholder className='h-[396px]' noPlugins={!query} />
|
||||
)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='top'
|
||||
offset={0}
|
||||
open={isShow}
|
||||
onOpenChange={onShowChange}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={toggleShowPopup}
|
||||
>
|
||||
{trigger}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<div className={cn('relative min-h-20 w-[436px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-2 shadow-lg backdrop-blur-sm')}>
|
||||
<div className='p-2 pb-1'>
|
||||
<SearchBox
|
||||
search={query}
|
||||
onSearchChange={setQuery}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
size='small'
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
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'>
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-6 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',
|
||||
)}
|
||||
key={tab.key}
|
||||
onClick={() => setPluginType(tab.key)}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && filteredList.length > 0 && listContent}
|
||||
{!isLoading && filteredList.length === 0 && noData}
|
||||
{isLoading && loadingContent}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ToolPicker)
|
@@ -0,0 +1,19 @@
|
||||
export enum AUTO_UPDATE_STRATEGY {
|
||||
fixOnly = 'fix_only',
|
||||
disabled = 'disabled',
|
||||
latest = 'latest',
|
||||
}
|
||||
|
||||
export enum AUTO_UPDATE_MODE {
|
||||
partial = 'partial',
|
||||
exclude = 'exclude',
|
||||
update_all = 'all',
|
||||
}
|
||||
|
||||
export type AutoUpdateConfig = {
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY
|
||||
upgrade_time_of_day: number
|
||||
upgrade_mode: AUTO_UPDATE_MODE
|
||||
exclude_plugins: string[]
|
||||
include_plugins: string[]
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
import { convertLocalSecondsToUTCDaySeconds, convertUTCDaySecondsToLocalSeconds } from './utils'
|
||||
|
||||
describe('convertLocalSecondsToUTCDaySeconds', () => {
|
||||
it('should convert local seconds to UTC day seconds correctly', () => {
|
||||
const localTimezone = 'Asia/Shanghai'
|
||||
const utcSeconds = convertLocalSecondsToUTCDaySeconds(0, localTimezone)
|
||||
expect(utcSeconds).toBe((24 - 8) * 3600)
|
||||
})
|
||||
|
||||
it('should convert local seconds to UTC day seconds for a specific time', () => {
|
||||
const localTimezone = 'Asia/Shanghai'
|
||||
expect(convertUTCDaySecondsToLocalSeconds(convertLocalSecondsToUTCDaySeconds(0, localTimezone), localTimezone)).toBe(0)
|
||||
})
|
||||
})
|
@@ -0,0 +1,37 @@
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export const timeOfDayToDayjs = (timeOfDay: number): Dayjs => {
|
||||
const hours = Math.floor(timeOfDay / 3600)
|
||||
const minutes = (timeOfDay - hours * 3600) / 60
|
||||
const res = dayjs().startOf('day').hour(hours).minute(minutes)
|
||||
return res
|
||||
}
|
||||
|
||||
export const convertLocalSecondsToUTCDaySeconds = (secondsInDay: number, localTimezone: string): number => {
|
||||
const localDayStart = dayjs().tz(localTimezone).startOf('day')
|
||||
const localTargetTime = localDayStart.add(secondsInDay, 'second')
|
||||
const utcTargetTime = localTargetTime.utc()
|
||||
const utcDayStart = utcTargetTime.startOf('day')
|
||||
const secondsFromUTCMidnight = utcTargetTime.diff(utcDayStart, 'second')
|
||||
return secondsFromUTCMidnight
|
||||
}
|
||||
|
||||
export const dayjsToTimeOfDay = (date?: Dayjs): number => {
|
||||
if (!date) return 0
|
||||
return date.hour() * 3600 + date.minute() * 60
|
||||
}
|
||||
|
||||
export const convertUTCDaySecondsToLocalSeconds = (utcDaySeconds: number, localTimezone: string): number => {
|
||||
const utcDayStart = dayjs().utc().startOf('day')
|
||||
const utcTargetTime = utcDayStart.add(utcDaySeconds, 'second')
|
||||
const localTargetTime = utcTargetTime.tz(localTimezone)
|
||||
const localDayStart = localTargetTime.startOf('day')
|
||||
const secondsInLocalDay = localTargetTime.diff(localDayStart, 'second')
|
||||
return secondsInLocalDay
|
||||
}
|
28
web/app/components/plugins/reference-setting-modal/label.tsx
Normal file
28
web/app/components/plugins/reference-setting-modal/label.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
label: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
const Label: FC<Props> = ({
|
||||
label,
|
||||
description,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={cn('flex h-6 items-center', description && 'h-4')}>
|
||||
<span className='system-sm-semibold text-text-secondary'>{label}</span>
|
||||
</div>
|
||||
{description && (
|
||||
<div className='body-xs-regular mt-1 text-text-tertiary'>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Label)
|
@@ -5,14 +5,18 @@ import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { Permissions } from '@/app/components/plugins/types'
|
||||
import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types'
|
||||
import { PermissionType } from '@/app/components/plugins/types'
|
||||
import type { AutoUpdateConfig } from './auto-update-setting/types'
|
||||
import AutoUpdateSetting from './auto-update-setting'
|
||||
import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config'
|
||||
import Label from './label'
|
||||
|
||||
const i18nPrefix = 'plugin.privilege'
|
||||
type Props = {
|
||||
payload: Permissions
|
||||
payload: ReferenceSetting
|
||||
onHide: () => void
|
||||
onSave: (payload: Permissions) => void
|
||||
onSave: (payload: ReferenceSetting) => void
|
||||
}
|
||||
|
||||
const PluginSettingModal: FC<Props> = ({
|
||||
@@ -21,7 +25,9 @@ const PluginSettingModal: FC<Props> = ({
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(payload)
|
||||
const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {}
|
||||
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege)
|
||||
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue)
|
||||
const handlePrivilegeChange = useCallback((key: string) => {
|
||||
return (value: PermissionType) => {
|
||||
setTempPrivilege({
|
||||
@@ -32,18 +38,21 @@ const PluginSettingModal: FC<Props> = ({
|
||||
}, [tempPrivilege])
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
await onSave(tempPrivilege)
|
||||
await onSave({
|
||||
permission: tempPrivilege,
|
||||
auto_upgrade: tempAutoUpdateConfig,
|
||||
})
|
||||
onHide()
|
||||
}, [onHide, onSave, tempPrivilege])
|
||||
}, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onHide}
|
||||
closable
|
||||
className='w-[420px] !p-0'
|
||||
className='w-[480px] !p-0'
|
||||
>
|
||||
<div className='shadows-shadow-xl flex w-[420px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'>
|
||||
<div className='shadows-shadow-xl flex w-[480px] flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg'>
|
||||
<div className='flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6'>
|
||||
<span className='title-2xl-semi-bold self-stretch text-text-primary'>{t(`${i18nPrefix}.title`)}</span>
|
||||
</div>
|
||||
@@ -53,9 +62,7 @@ const PluginSettingModal: FC<Props> = ({
|
||||
{ title: t(`${i18nPrefix}.whoCanDebug`), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
|
||||
].map(({ title, key, value }) => (
|
||||
<div key={key} className='flex flex-col items-start gap-1 self-stretch'>
|
||||
<div className='flex h-6 items-center gap-0.5'>
|
||||
<span className='system-sm-semibold text-text-secondary'>{title}</span>
|
||||
</div>
|
||||
<Label label={title} />
|
||||
<div className='flex w-full items-start justify-between gap-2'>
|
||||
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
|
||||
<OptionCard
|
||||
@@ -70,6 +77,8 @@ const PluginSettingModal: FC<Props> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
|
||||
<div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'>
|
||||
<Button
|
||||
className='min-w-[72px]'
|
@@ -2,6 +2,7 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p
|
||||
import type { ToolCredential } from '@/app/components/tools/types'
|
||||
import type { Locale } from '@/i18n'
|
||||
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
|
||||
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
|
||||
export enum PluginType {
|
||||
tool = 'tool',
|
||||
model = 'model',
|
||||
@@ -170,6 +171,11 @@ export type Permissions = {
|
||||
debug_permission: PermissionType
|
||||
}
|
||||
|
||||
export type ReferenceSetting = {
|
||||
permission: Permissions
|
||||
auto_upgrade: AutoUpdateConfig
|
||||
}
|
||||
|
||||
export type UpdateFromMarketPlacePayload = {
|
||||
category: PluginType
|
||||
originalPackageInfo: {
|
||||
|
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
const i18nPrefix = 'plugin.autoUpdate.pluginDowngradeWarning'
|
||||
|
||||
type Props = {
|
||||
onCancel: () => void
|
||||
onJustDowngrade: () => void
|
||||
onExcludeAndDowngrade: () => void
|
||||
}
|
||||
const DowngradeWarningModal = ({
|
||||
onCancel,
|
||||
onJustDowngrade,
|
||||
onExcludeAndDowngrade,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.title`)}</div>
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-9 flex items-start justify-end space-x-2 self-stretch'>
|
||||
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='secondary' destructive onClick={onJustDowngrade}>{t(`${i18nPrefix}.downgrade`)}</Button>
|
||||
<Button variant='primary' onClick={onExcludeAndDowngrade}>{t(`${i18nPrefix}.exclude`)}</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DowngradeWarningModal
|
@@ -13,13 +13,18 @@ import { updateFromMarketPlace } from '@/service/plugins'
|
||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
import { usePluginTaskList } from '@/service/use-plugins'
|
||||
import Toast from '../../base/toast'
|
||||
import DowngradeWarningModal from './downgrade-warning'
|
||||
import { useInvalidateReferenceSettings, useRemoveAutoUpgrade } from '@/service/use-plugins'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'plugin.upgrade'
|
||||
|
||||
type Props = {
|
||||
payload: UpdateFromMarketPlacePayload
|
||||
pluginId: string
|
||||
onSave: () => void
|
||||
onCancel: () => void
|
||||
isShowDowngradeWarningModal?: boolean
|
||||
}
|
||||
|
||||
enum UploadStep {
|
||||
@@ -30,8 +35,10 @@ enum UploadStep {
|
||||
|
||||
const UpdatePluginModal: FC<Props> = ({
|
||||
payload,
|
||||
pluginId,
|
||||
onSave,
|
||||
onCancel,
|
||||
isShowDowngradeWarningModal,
|
||||
}) => {
|
||||
const {
|
||||
originalPackageInfo,
|
||||
@@ -103,51 +110,74 @@ const UpdatePluginModal: FC<Props> = ({
|
||||
onSave()
|
||||
}, [onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
|
||||
|
||||
const { mutateAsync } = useRemoveAutoUpgrade()
|
||||
const invalidateReferenceSettings = useInvalidateReferenceSettings()
|
||||
const handleExcludeAndDownload = async () => {
|
||||
await mutateAsync({
|
||||
plugin_id: pluginId,
|
||||
})
|
||||
invalidateReferenceSettings()
|
||||
handleConfirm()
|
||||
}
|
||||
const doShowDowngradeWarningModal = isShowDowngradeWarningModal && uploadStep === UploadStep.notStarted
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={onCancel}
|
||||
className='min-w-[560px]'
|
||||
className={cn('min-w-[560px]', doShowDowngradeWarningModal && 'min-w-[640px]')}
|
||||
closable
|
||||
title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)}
|
||||
title={!doShowDowngradeWarningModal && t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)}
|
||||
>
|
||||
<div className='system-md-regular mb-2 mt-3 text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<Card
|
||||
installed={uploadStep === UploadStep.installed}
|
||||
payload={pluginManifestToCardPluginProps({
|
||||
...originalPackageInfo.payload,
|
||||
icon: icon!,
|
||||
})}
|
||||
className='w-full'
|
||||
titleLeft={
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
{doShowDowngradeWarningModal && (
|
||||
<DowngradeWarningModal
|
||||
onCancel={onCancel}
|
||||
onJustDowngrade={handleConfirm}
|
||||
onExcludeAndDowngrade={handleExcludeAndDownload}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-2 self-stretch pt-5'>
|
||||
{uploadStep === UploadStep.notStarted && (
|
||||
)}
|
||||
{!doShowDowngradeWarningModal && (
|
||||
<>
|
||||
<div className='system-md-regular mb-2 mt-3 text-text-secondary'>
|
||||
{t(`${i18nPrefix}.description`)}
|
||||
</div>
|
||||
<div className='flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2'>
|
||||
<Card
|
||||
installed={uploadStep === UploadStep.installed}
|
||||
payload={pluginManifestToCardPluginProps({
|
||||
...originalPackageInfo.payload,
|
||||
icon: icon!,
|
||||
})}
|
||||
className='w-full'
|
||||
titleLeft={
|
||||
<>
|
||||
<Badge className='mx-1' size="s" state={BadgeState.Warning}>
|
||||
{`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`}
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-2 self-stretch pt-5'>
|
||||
{uploadStep === UploadStep.notStarted && (
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
variant='primary'
|
||||
loading={uploadStep === UploadStep.upgrading}
|
||||
onClick={handleConfirm}
|
||||
disabled={uploadStep === UploadStep.upgrading}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
{configBtnText}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
loading={uploadStep === UploadStep.upgrading}
|
||||
onClick={handleConfirm}
|
||||
disabled={uploadStep === UploadStep.upgrading}
|
||||
>
|
||||
{configBtnText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import type {
|
||||
import { useVersionListOfPlugin } from '@/service/use-plugins'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import cn from '@/utils/classnames'
|
||||
import { lt } from 'semver'
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
@@ -28,9 +29,11 @@ type Props = {
|
||||
onSelect: ({
|
||||
version,
|
||||
unique_identifier,
|
||||
isDowngrade,
|
||||
}: {
|
||||
version: string
|
||||
unique_identifier: string
|
||||
isDowngrade: boolean
|
||||
}) => void
|
||||
}
|
||||
|
||||
@@ -59,13 +62,14 @@ const PluginVersionPicker: FC<Props> = ({
|
||||
|
||||
const { data: res } = useVersionListOfPlugin(pluginID)
|
||||
|
||||
const handleSelect = useCallback(({ version, unique_identifier }: {
|
||||
const handleSelect = useCallback(({ version, unique_identifier, isDowngrade }: {
|
||||
version: string
|
||||
unique_identifier: string
|
||||
isDowngrade: boolean
|
||||
}) => {
|
||||
if (currentVersion === version)
|
||||
return
|
||||
onSelect({ version, unique_identifier })
|
||||
onSelect({ version, unique_identifier, isDowngrade })
|
||||
onShowChange(false)
|
||||
}, [currentVersion, onSelect, onShowChange])
|
||||
|
||||
@@ -99,6 +103,7 @@ const PluginVersionPicker: FC<Props> = ({
|
||||
onClick={() => handleSelect({
|
||||
version: version.version,
|
||||
unique_identifier: version.unique_identifier,
|
||||
isDowngrade: lt(version.version, currentVersion),
|
||||
})}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
|
Reference in New Issue
Block a user