mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-11 00:06:04 +00:00
perf: add component list in plugin detail dialog
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { PluginComponent } from '@/app/infra/entities/plugin';
|
||||
|
||||
export interface IPluginCardVO {
|
||||
author: string;
|
||||
label: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
install_source: string;
|
||||
install_info: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
status: string;
|
||||
components: PluginComponent[];
|
||||
debug: boolean;
|
||||
}
|
||||
|
||||
export class PluginCardVO implements IPluginCardVO {
|
||||
author: string;
|
||||
label: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
debug: boolean;
|
||||
install_source: string;
|
||||
install_info: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
status: string;
|
||||
components: PluginComponent[];
|
||||
|
||||
constructor(prop: IPluginCardVO) {
|
||||
this.author = prop.author;
|
||||
this.label = prop.label;
|
||||
this.description = prop.description;
|
||||
this.enabled = prop.enabled;
|
||||
this.components = prop.components;
|
||||
this.name = prop.name;
|
||||
this.priority = prop.priority;
|
||||
this.status = prop.status;
|
||||
this.version = prop.version;
|
||||
this.debug = prop.debug;
|
||||
this.install_source = prop.install_source;
|
||||
this.install_info = prop.install_info;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { PluginComponent } from '@/app/infra/entities/plugin';
|
||||
import { TFunction } from 'i18next';
|
||||
import { Wrench, AudioWaveform, Hash } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export default function PluginComponentList({
|
||||
components,
|
||||
showComponentName,
|
||||
showTitle,
|
||||
useBadge,
|
||||
t,
|
||||
}: {
|
||||
components: PluginComponent[];
|
||||
showComponentName: boolean;
|
||||
showTitle: boolean;
|
||||
useBadge: boolean;
|
||||
t: TFunction;
|
||||
}) {
|
||||
const componentKindCount: Record<string, number> = {};
|
||||
|
||||
for (const component of components) {
|
||||
const kind = component.manifest.manifest.kind;
|
||||
if (componentKindCount[kind]) {
|
||||
componentKindCount[kind]++;
|
||||
} else {
|
||||
componentKindCount[kind] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const kindIconMap: Record<string, React.ReactNode> = {
|
||||
Tool: <Wrench className="w-5 h-5" />,
|
||||
EventListener: <AudioWaveform className="w-5 h-5" />,
|
||||
Command: <Hash className="w-5 h-5" />,
|
||||
};
|
||||
|
||||
const componentKindList = Object.keys(componentKindCount);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showTitle && <div>{t('plugins.componentsList')}</div>}
|
||||
{componentKindList.length > 0 && (
|
||||
<>
|
||||
{componentKindList.map((kind) => {
|
||||
return (
|
||||
<>
|
||||
{useBadge && (
|
||||
<Badge variant="outline">
|
||||
{kindIconMap[kind]}
|
||||
{showComponentName &&
|
||||
t('plugins.componentName.' + kind) + ' '}
|
||||
{componentKindCount[kind]}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{!useBadge && (
|
||||
<div
|
||||
key={kind}
|
||||
className="flex flex-row items-center justify-start gap-[0.2rem]"
|
||||
>
|
||||
{kindIconMap[kind]}
|
||||
{showComponentName &&
|
||||
t('plugins.componentName.' + kind) + ' '}
|
||||
{componentKindCount[kind]}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{componentKindList.length === 0 && <div>{t('plugins.noComponents')}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
|
||||
import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
|
||||
import PluginCardComponent from '@/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent';
|
||||
import PluginForm from '@/app/home/plugins/components/plugin-installed/plugin-form/PluginForm';
|
||||
import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { toast } from 'sonner';
|
||||
import { useAsyncTask, AsyncTaskStatus } from '@/hooks/useAsyncTask';
|
||||
|
||||
export interface PluginInstalledComponentRef {
|
||||
refreshPluginList: () => void;
|
||||
}
|
||||
|
||||
enum PluginOperationType {
|
||||
DELETE = 'DELETE',
|
||||
UPDATE = 'UPDATE',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [pluginList, setPluginList] = useState<PluginCardVO[]>([]);
|
||||
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
||||
const [selectedPlugin, setSelectedPlugin] = useState<PluginCardVO | null>(
|
||||
null,
|
||||
);
|
||||
const [showOperationModal, setShowOperationModal] = useState(false);
|
||||
const [operationType, setOperationType] = useState<PluginOperationType>(
|
||||
PluginOperationType.DELETE,
|
||||
);
|
||||
const [targetPlugin, setTargetPlugin] = useState<PluginCardVO | null>(null);
|
||||
|
||||
const asyncTask = useAsyncTask({
|
||||
onSuccess: () => {
|
||||
const successMessage =
|
||||
operationType === PluginOperationType.DELETE
|
||||
? t('plugins.deleteSuccess')
|
||||
: t('plugins.updateSuccess');
|
||||
toast.success(successMessage);
|
||||
setShowOperationModal(false);
|
||||
getPluginList();
|
||||
},
|
||||
onError: () => {
|
||||
// Error is already handled in the hook state
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
}, []);
|
||||
|
||||
function initData() {
|
||||
getPluginList();
|
||||
}
|
||||
|
||||
function getPluginList() {
|
||||
httpClient.getPlugins().then((value) => {
|
||||
setPluginList(
|
||||
value.plugins.map((plugin) => {
|
||||
return new PluginCardVO({
|
||||
author: plugin.manifest.manifest.metadata.author ?? '',
|
||||
label: extractI18nObject(plugin.manifest.manifest.metadata.label),
|
||||
description: extractI18nObject(
|
||||
plugin.manifest.manifest.metadata.description ?? {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
),
|
||||
debug: plugin.debug,
|
||||
enabled: plugin.enabled,
|
||||
name: plugin.manifest.manifest.metadata.name,
|
||||
version: plugin.manifest.manifest.metadata.version ?? '',
|
||||
status: plugin.status,
|
||||
components: plugin.components,
|
||||
priority: plugin.priority,
|
||||
install_source: plugin.install_source,
|
||||
install_info: plugin.install_info,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
refreshPluginList: getPluginList,
|
||||
}));
|
||||
|
||||
function handlePluginClick(plugin: PluginCardVO) {
|
||||
setSelectedPlugin(plugin);
|
||||
setModalOpen(true);
|
||||
}
|
||||
|
||||
function handlePluginDelete(plugin: PluginCardVO) {
|
||||
setTargetPlugin(plugin);
|
||||
setOperationType(PluginOperationType.DELETE);
|
||||
setShowOperationModal(true);
|
||||
asyncTask.reset();
|
||||
}
|
||||
|
||||
function handlePluginUpdate(plugin: PluginCardVO) {
|
||||
setTargetPlugin(plugin);
|
||||
setOperationType(PluginOperationType.UPDATE);
|
||||
setShowOperationModal(true);
|
||||
asyncTask.reset();
|
||||
}
|
||||
|
||||
function executeOperation() {
|
||||
if (!targetPlugin) return;
|
||||
|
||||
const apiCall =
|
||||
operationType === PluginOperationType.DELETE
|
||||
? httpClient.removePlugin(targetPlugin.author, targetPlugin.name)
|
||||
: httpClient.upgradePlugin(targetPlugin.author, targetPlugin.name);
|
||||
|
||||
apiCall
|
||||
.then((res) => {
|
||||
asyncTask.startTask(res.task_id);
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
operationType === PluginOperationType.DELETE
|
||||
? t('plugins.deleteError') + error.message
|
||||
: t('plugins.updateError') + error.message;
|
||||
toast.error(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={showOperationModal}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowOperationModal(false);
|
||||
setTargetPlugin(null);
|
||||
asyncTask.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.deleteConfirm')
|
||||
: t('plugins.updateConfirm')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
||||
<div>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.confirmDeletePlugin', {
|
||||
author: targetPlugin?.author ?? '',
|
||||
name: targetPlugin?.name ?? '',
|
||||
})
|
||||
: t('plugins.confirmUpdatePlugin', {
|
||||
author: targetPlugin?.author ?? '',
|
||||
name: targetPlugin?.name ?? '',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.RUNNING && (
|
||||
<div>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.deleting')
|
||||
: t('plugins.updating')}
|
||||
</div>
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.ERROR && (
|
||||
<div>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.deleteError')
|
||||
: t('plugins.updateError')}
|
||||
<div className="text-red-500">{asyncTask.error}</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowOperationModal(false);
|
||||
setTargetPlugin(null);
|
||||
asyncTask.reset();
|
||||
}}
|
||||
>
|
||||
{t('plugins.cancel')}
|
||||
</Button>
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.WAIT_INPUT && (
|
||||
<Button
|
||||
variant={
|
||||
operationType === PluginOperationType.DELETE
|
||||
? 'destructive'
|
||||
: 'default'
|
||||
}
|
||||
onClick={() => {
|
||||
executeOperation();
|
||||
}}
|
||||
>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.confirmDelete')
|
||||
: t('plugins.confirmUpdate')}
|
||||
</Button>
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.RUNNING && (
|
||||
<Button
|
||||
variant={
|
||||
operationType === PluginOperationType.DELETE
|
||||
? 'destructive'
|
||||
: 'default'
|
||||
}
|
||||
disabled
|
||||
>
|
||||
{operationType === PluginOperationType.DELETE
|
||||
? t('plugins.deleting')
|
||||
: t('plugins.updating')}
|
||||
</Button>
|
||||
)}
|
||||
{asyncTask.status === AsyncTaskStatus.ERROR && (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setShowOperationModal(false);
|
||||
asyncTask.reset();
|
||||
}}
|
||||
>
|
||||
{t('plugins.close')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{pluginList.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center text-gray-500 h-[calc(100vh-16rem)] w-full gap-2">
|
||||
<svg
|
||||
className="h-[3rem] w-[3rem]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M7 5C7 2.79086 8.79086 1 11 1C13.2091 1 15 2.79086 15 5H20C20.5523 5 21 5.44772 21 6V10.1707C21 10.4953 20.8424 10.7997 20.5774 10.9872C20.3123 11.1746 19.9728 11.2217 19.6668 11.1135C19.4595 11.0403 19.2355 11 19 11C17.8954 11 17 11.8954 17 13C17 14.1046 17.8954 15 19 15C19.2355 15 19.4595 14.9597 19.6668 14.8865C19.9728 14.7783 20.3123 14.8254 20.5774 15.0128C20.8424 15.2003 21 15.5047 21 15.8293V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H7ZM11 3C9.89543 3 9 3.89543 9 5C9 5.23554 9.0403 5.45952 9.11355 5.66675C9.22172 5.97282 9.17461 6.31235 8.98718 6.57739C8.79974 6.84243 8.49532 7 8.17071 7H5V19H19V17C16.7909 17 15 15.2091 15 13C15 10.7909 16.7909 9 19 9V7H13.8293C13.5047 7 13.2003 6.84243 13.0128 6.57739C12.8254 6.31235 12.7783 5.97282 12.8865 5.66675C12.9597 5.45952 13 5.23555 13 5C13 3.89543 12.1046 3 11 3Z"></path>
|
||||
</svg>
|
||||
<div className="text-lg mb-2">{t('plugins.noPluginInstalled')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
|
||||
<DialogHeader className="px-6 pt-6 pb-2">
|
||||
<DialogTitle>{t('plugins.pluginConfig')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6">
|
||||
{selectedPlugin && (
|
||||
<PluginForm
|
||||
pluginAuthor={selectedPlugin.author}
|
||||
pluginName={selectedPlugin.name}
|
||||
onFormSubmit={(timeout?: number) => {
|
||||
setModalOpen(false);
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
getPluginList();
|
||||
}, timeout);
|
||||
} else {
|
||||
getPluginList();
|
||||
}
|
||||
}}
|
||||
onFormCancel={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{pluginList.map((vo, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<PluginCardComponent
|
||||
cardVO={vo}
|
||||
onCardClick={() => handlePluginClick(vo)}
|
||||
onDeleteClick={() => handlePluginDelete(vo)}
|
||||
onUpgradeClick={() => handlePluginUpdate(vo)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default PluginInstalledComponent;
|
||||
@@ -0,0 +1,184 @@
|
||||
import { PluginCardVO } from '@/app/home/plugins/components/plugin-installed/PluginCardVO';
|
||||
import { useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BugIcon, ExternalLink, Ellipsis, Trash, ArrowUp } from 'lucide-react';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
||||
|
||||
export default function PluginCardComponent({
|
||||
cardVO,
|
||||
onCardClick,
|
||||
onDeleteClick,
|
||||
onUpgradeClick,
|
||||
}: {
|
||||
cardVO: PluginCardVO;
|
||||
onCardClick: () => void;
|
||||
onDeleteClick: (cardVO: PluginCardVO) => void;
|
||||
onUpgradeClick: (cardVO: PluginCardVO) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="w-[100%] h-[10rem] bg-white rounded-[10px] shadow-[0px_2px_2px_0_rgba(0,0,0,0.2)] p-[1.2rem] cursor-pointer dark:bg-[#1f1f22]"
|
||||
onClick={onCardClick}
|
||||
>
|
||||
<div className="w-full h-full flex flex-row items-start justify-start gap-[1.2rem]">
|
||||
{/* <svg
|
||||
className="w-16 h-16 text-[#2288ee]"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M8 4C8 2.34315 9.34315 1 11 1C12.6569 1 14 2.34315 14 4C14 4.35064 13.9398 4.68722 13.8293 5H18C18.5523 5 19 5.44772 19 6V10.1707C19.3128 10.0602 19.6494 10 20 10C21.6569 10 23 11.3431 23 13C23 14.6569 21.6569 16 20 16C19.6494 16 19.3128 15.9398 19 15.8293V20C19 20.5523 18.5523 21 18 21H4C3.44772 21 3 20.5523 3 20V6C3 5.44772 3.44772 5 4 5H8.17071C8.06015 4.68722 8 4.35064 8 4Z"></path>
|
||||
</svg> */}
|
||||
<img
|
||||
src={httpClient.getPluginIconURL(cardVO.author, cardVO.name)}
|
||||
alt="plugin icon"
|
||||
className="w-16 h-16"
|
||||
/>
|
||||
|
||||
<div className="w-full h-full flex flex-col items-start justify-between gap-[0.6rem]">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="flex flex-col items-start justify-start">
|
||||
<div className="text-[0.7rem] text-[#666] dark:text-[#999]">
|
||||
{cardVO.author} / {cardVO.name}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<div className="text-[1.2rem] text-black dark:text-[#f0f0f0]">
|
||||
{cardVO.label}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[0.7rem]">
|
||||
v{cardVO.version}
|
||||
</Badge>
|
||||
{cardVO.debug && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-orange-400 text-orange-400"
|
||||
>
|
||||
<BugIcon className="w-4 h-4" />
|
||||
{t('plugins.debugging')}
|
||||
</Badge>
|
||||
)}
|
||||
{!cardVO.debug && (
|
||||
<>
|
||||
{cardVO.install_source === 'github' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-blue-400 text-blue-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
cardVO.install_info.github_url,
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('plugins.fromGithub')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.install_source === 'local' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-green-400 text-green-400"
|
||||
>
|
||||
{t('plugins.fromLocal')}
|
||||
</Badge>
|
||||
)}
|
||||
{cardVO.install_source === 'marketplace' && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[0.7rem] border-purple-400 text-purple-400"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(
|
||||
getCloudServiceClientSync().getPluginMarketplaceURL(
|
||||
cardVO.author,
|
||||
cardVO.name,
|
||||
),
|
||||
'_blank',
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('plugins.fromMarketplace')}
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[0.8rem] text-[#666] line-clamp-2 dark:text-[#999]">
|
||||
{cardVO.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full flex flex-row items-start justify-start gap-[0.6rem]">
|
||||
<PluginComponentList
|
||||
components={cardVO.components}
|
||||
showComponentName={false}
|
||||
showTitle={true}
|
||||
useBadge={false}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center justify-between h-full">
|
||||
<div className="flex items-center justify-center"></div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{/**upgrade */}
|
||||
{cardVO.install_source === 'marketplace' && (
|
||||
<DropdownMenuItem
|
||||
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUpgradeClick(cardVO);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
<span>{t('plugins.update')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="flex flex-row items-center justify-start gap-[0.4rem] cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteClick(cardVO);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
<span>{t('plugins.delete')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ApiRespPluginConfig } from '@/app/infra/entities/api';
|
||||
import { Plugin } from '@/app/infra/entities/plugin';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
||||
|
||||
export default function PluginForm({
|
||||
pluginAuthor,
|
||||
pluginName,
|
||||
onFormSubmit,
|
||||
onFormCancel,
|
||||
}: {
|
||||
pluginAuthor: string;
|
||||
pluginName: string;
|
||||
onFormSubmit: (timeout?: number) => void;
|
||||
onFormCancel: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [pluginInfo, setPluginInfo] = useState<Plugin>();
|
||||
const [pluginConfig, setPluginConfig] = useState<ApiRespPluginConfig>();
|
||||
const [isSaving, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 获取插件信息
|
||||
httpClient.getPlugin(pluginAuthor, pluginName).then((res) => {
|
||||
setPluginInfo(res.plugin);
|
||||
});
|
||||
// 获取插件配置
|
||||
httpClient.getPluginConfig(pluginAuthor, pluginName).then((res) => {
|
||||
setPluginConfig(res);
|
||||
});
|
||||
}, [pluginAuthor, pluginName]);
|
||||
|
||||
const handleSubmit = async (values: object) => {
|
||||
setIsLoading(true);
|
||||
const isDebugPlugin = pluginInfo?.debug;
|
||||
httpClient
|
||||
.updatePluginConfig(pluginAuthor, pluginName, values)
|
||||
.then(() => {
|
||||
toast.success(
|
||||
isDebugPlugin
|
||||
? t('plugins.saveConfigSuccessDebugPlugin')
|
||||
: t('plugins.saveConfigSuccessNormal'),
|
||||
);
|
||||
onFormSubmit(1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(t('plugins.saveConfigError') + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
if (!pluginInfo || !pluginConfig) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full mb-[2rem]">
|
||||
{t('plugins.loading')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-lg font-medium">
|
||||
{extractI18nObject(pluginInfo.manifest.manifest.metadata.label)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 pb-2">
|
||||
{extractI18nObject(
|
||||
pluginInfo.manifest.manifest.metadata.description ?? {
|
||||
en_US: '',
|
||||
zh_Hans: '',
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex flex-row items-center justify-start gap-[0.4rem]">
|
||||
<PluginComponentList
|
||||
components={pluginInfo.components}
|
||||
showComponentName={true}
|
||||
showTitle={false}
|
||||
useBadge={true}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{pluginInfo.manifest.manifest.spec.config.length > 0 && (
|
||||
<DynamicFormComponent
|
||||
itemConfigList={pluginInfo.manifest.manifest.spec.config}
|
||||
initialValues={pluginConfig.config as Record<string, object>}
|
||||
onSubmit={(values) => {
|
||||
let config = pluginConfig.config;
|
||||
config = {
|
||||
...config,
|
||||
...values,
|
||||
};
|
||||
setPluginConfig({
|
||||
config: config,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{pluginInfo.manifest.manifest.spec.config.length === 0 && (
|
||||
<div className="text-sm text-gray-500">
|
||||
{t('plugins.pluginNoConfig')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 left-0 right-0 bg-background border-t p-4 mt-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => handleSubmit(pluginConfig.config)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? t('plugins.saving') : t('plugins.saveConfig')}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={onFormCancel}>
|
||||
{t('plugins.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user