mirror of https://github.com/jetkvm/kvm.git
Add enable/disable button
This commit is contained in:
parent
3853b58613
commit
88f3e97011
|
@ -1,5 +1,7 @@
|
||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
type PluginInstall struct {
|
type PluginInstall struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
|
||||||
|
@ -29,3 +31,21 @@ func (p *PluginInstall) GetManifest() (*PluginManifest, error) {
|
||||||
func (p *PluginInstall) GetExtractedFolder() string {
|
func (p *PluginInstall) GetExtractedFolder() string {
|
||||||
return p.ExtractedVersions[p.Version]
|
return p.ExtractedVersions[p.Version]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PluginInstall) GetStatus() (*PluginStatus, error) {
|
||||||
|
manifest, err := p.GetManifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get plugin manifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "stopped"
|
||||||
|
if p.Enabled {
|
||||||
|
status = "running"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PluginStatus{
|
||||||
|
PluginManifest: *manifest,
|
||||||
|
Enabled: p.Enabled,
|
||||||
|
Status: status,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
|
@ -135,25 +135,35 @@ func RpcPluginInstall(name string, version string) error {
|
||||||
func RpcPluginList() ([]PluginStatus, error) {
|
func RpcPluginList() ([]PluginStatus, error) {
|
||||||
plugins := make([]PluginStatus, 0, len(pluginDatabase.Plugins))
|
plugins := make([]PluginStatus, 0, len(pluginDatabase.Plugins))
|
||||||
for pluginName, plugin := range pluginDatabase.Plugins {
|
for pluginName, plugin := range pluginDatabase.Plugins {
|
||||||
manifest, err := plugin.GetManifest()
|
status, err := plugin.GetStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get plugin manifest for %s: %v", pluginName, err)
|
return nil, fmt.Errorf("failed to get plugin status for %s: %v", pluginName, err)
|
||||||
}
|
}
|
||||||
|
plugins = append(plugins, *status)
|
||||||
status := "stopped"
|
|
||||||
if plugin.Enabled {
|
|
||||||
status = "running"
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins = append(plugins, PluginStatus{
|
|
||||||
PluginManifest: *manifest,
|
|
||||||
Enabled: plugin.Enabled,
|
|
||||||
Status: status,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return plugins, nil
|
return plugins, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RpcUpdateConfig(name string, enabled bool) (*PluginStatus, error) {
|
||||||
|
pluginInstall, ok := pluginDatabase.Plugins[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("plugin not found: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginInstall.Enabled = enabled
|
||||||
|
pluginDatabase.Plugins[name] = pluginInstall
|
||||||
|
|
||||||
|
if err := pluginDatabase.Save(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to save plugin database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := pluginInstall.GetStatus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get plugin status for %s: %v", name, err)
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
func readManifest(extractFolder string) (*PluginManifest, error) {
|
func readManifest(extractFolder string) (*PluginManifest, error) {
|
||||||
manifestPath := path.Join(extractFolder, "manifest.json")
|
manifestPath := path.Join(extractFolder, "manifest.json")
|
||||||
manifestFile, err := os.Open(manifestPath)
|
manifestFile, err := os.Open(manifestPath)
|
||||||
|
|
|
@ -559,4 +559,5 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"pluginExtract": {Func: plugin.RpcPluginExtract, Params: []string{"filename"}},
|
"pluginExtract": {Func: plugin.RpcPluginExtract, Params: []string{"filename"}},
|
||||||
"pluginInstall": {Func: plugin.RpcPluginInstall, Params: []string{"name", "version"}},
|
"pluginInstall": {Func: plugin.RpcPluginInstall, Params: []string{"name", "version"}},
|
||||||
"pluginList": {Func: plugin.RpcPluginList},
|
"pluginList": {Func: plugin.RpcPluginList},
|
||||||
|
"pluginUpdateConfig": {Func: plugin.RpcUpdateConfig, Params: []string{"name", "enabled"}},
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { PluginStatus } from "@/hooks/stores";
|
||||||
|
import Modal from "@components/Modal";
|
||||||
|
import AutoHeight from "@components/AutoHeight";
|
||||||
|
import { GridCard } from "@components/Card";
|
||||||
|
import LogoBlueIcon from "@/assets/logo-blue.svg";
|
||||||
|
import LogoWhiteIcon from "@/assets/logo-white.svg";
|
||||||
|
import { ViewHeader } from "./MountMediaDialog";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function PluginConfigureModal({
|
||||||
|
plugin,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
}: {
|
||||||
|
plugin: PluginStatus | null;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Modal open={!!plugin && open} onClose={() => setOpen(false)}>
|
||||||
|
<Dialog plugin={plugin} setOpen={setOpen} />
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dialog({ plugin, setOpen }: { plugin: PluginStatus | null, setOpen: (open: boolean) => void }) {
|
||||||
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, [plugin])
|
||||||
|
|
||||||
|
const updatePlugin = useCallback((enabled: boolean) => {
|
||||||
|
if (!plugin) return;
|
||||||
|
if (!enabled) {
|
||||||
|
if (!window.confirm("Are you sure you want to disable this plugin?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
send("pluginUpdateConfig", { name: plugin.name, enabled }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
setError(resp.error.message);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
}, [send, plugin, setOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoHeight>
|
||||||
|
<div className="mx-auto max-w-4xl px-4 transition-all duration-300 ease-in-out">
|
||||||
|
<GridCard cardClassName="relative w-full text-left pointer-events-auto">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||||
|
<img
|
||||||
|
src={LogoBlueIcon}
|
||||||
|
alt="JetKVM Logo"
|
||||||
|
className="h-[24px] dark:hidden block"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={LogoWhiteIcon}
|
||||||
|
alt="JetKVM Logo"
|
||||||
|
className="h-[24px] dark:block hidden dark:!mt-0"
|
||||||
|
/>
|
||||||
|
<div className="w-full space-y-4">
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<ViewHeader title="Plugin Configuration" description={`Configure the ${plugin?.name} plugin`} />
|
||||||
|
<div>
|
||||||
|
{/* Enable/Disable toggle */}
|
||||||
|
<Button
|
||||||
|
size="MD"
|
||||||
|
theme={plugin?.enabled ? "danger" : "light"}
|
||||||
|
text={plugin?.enabled ? "Disable Plugin" : "Enable Plugin"}
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => {
|
||||||
|
updatePlugin(!plugin?.enabled);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="space-y-2 opacity-0 animate-fadeIn"
|
||||||
|
style={{
|
||||||
|
animationDuration: "0.7s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error && <p className="text-red-500 dark:text-red-400">{error}</p>}
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 py-10">
|
||||||
|
TODO: Plugin configuration goes here
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex items-end w-full opacity-0 animate-fadeIn"
|
||||||
|
style={{
|
||||||
|
animationDuration: "0.7s",
|
||||||
|
animationDelay: "0.1s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex justify-end w-full space-x-2">
|
||||||
|
<Button
|
||||||
|
size="MD"
|
||||||
|
theme="light"
|
||||||
|
text="Back"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
</div>
|
||||||
|
</AutoHeight>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { PluginStatus, usePluginStore, useUiStore } from "@/hooks/stores";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import UploadPluginModal from "@components/UploadPluginDialog";
|
import UploadPluginModal from "@components/UploadPluginDialog";
|
||||||
|
import PluginConfigureModal from "./PluginConfigureDialog";
|
||||||
|
|
||||||
function PluginListStatusIcon({ plugin }: { plugin: PluginStatus }) {
|
function PluginListStatusIcon({ plugin }: { plugin: PluginStatus }) {
|
||||||
let classNames = "bg-slate-500 border-slate-600";
|
let classNames = "bg-slate-500 border-slate-600";
|
||||||
|
@ -29,7 +30,11 @@ export default function PluginList() {
|
||||||
setIsPluginUploadModalOpen,
|
setIsPluginUploadModalOpen,
|
||||||
setPluginUploadModalView,
|
setPluginUploadModalView,
|
||||||
plugins,
|
plugins,
|
||||||
setPlugins
|
setPlugins,
|
||||||
|
pluginConfigureModalOpen,
|
||||||
|
setPluginConfigureModalOpen,
|
||||||
|
configuringPlugin,
|
||||||
|
setConfiguringPlugin,
|
||||||
} = usePluginStore();
|
} = usePluginStore();
|
||||||
const sidebarView = useUiStore(state => state.sidebarView);
|
const sidebarView = useUiStore(state => state.sidebarView);
|
||||||
|
|
||||||
|
@ -74,7 +79,10 @@ export default function PluginList() {
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Settings"
|
text="Settings"
|
||||||
onClick={() => console.log("Settings clicked")}
|
onClick={() => {
|
||||||
|
setConfiguringPlugin(plugin);
|
||||||
|
setPluginConfigureModalOpen(true);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -82,6 +90,17 @@ export default function PluginList() {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PluginConfigureModal
|
||||||
|
open={pluginConfigureModalOpen}
|
||||||
|
setOpen={(open) => {
|
||||||
|
setPluginConfigureModalOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
updatePlugins();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
plugin={configuringPlugin}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
|
|
|
@ -557,6 +557,12 @@ interface PluginState {
|
||||||
|
|
||||||
plugins: PluginStatus[];
|
plugins: PluginStatus[];
|
||||||
setPlugins: (plugins: PluginStatus[]) => void;
|
setPlugins: (plugins: PluginStatus[]) => void;
|
||||||
|
|
||||||
|
pluginConfigureModalOpen: boolean;
|
||||||
|
setPluginConfigureModalOpen: (isOpen: boolean) => void;
|
||||||
|
|
||||||
|
configuringPlugin: PluginStatus | null;
|
||||||
|
setConfiguringPlugin: (plugin: PluginStatus | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePluginStore = create<PluginState>(set => ({
|
export const usePluginStore = create<PluginState>(set => ({
|
||||||
|
@ -574,4 +580,10 @@ export const usePluginStore = create<PluginState>(set => ({
|
||||||
|
|
||||||
plugins: [],
|
plugins: [],
|
||||||
setPlugins: plugins => set({ plugins }),
|
setPlugins: plugins => set({ plugins }),
|
||||||
|
|
||||||
|
pluginConfigureModalOpen: false,
|
||||||
|
setPluginConfigureModalOpen: isOpen => set({ pluginConfigureModalOpen: isOpen }),
|
||||||
|
|
||||||
|
configuringPlugin: null,
|
||||||
|
setConfiguringPlugin: plugin => set({ configuringPlugin: plugin }),
|
||||||
}));
|
}));
|
||||||
|
|
Loading…
Reference in New Issue