Finish plugin support implementation

Small refactoring
This commit is contained in:
grimsi
2024-10-31 14:33:35 +01:00
parent 5ebd074d3b
commit 34f00c091f
21 changed files with 305 additions and 163 deletions
@@ -1,69 +1,29 @@
import React, {useEffect, useState} from "react";
import Section from "Frontend/components/general/Section";
import {PluginConfigEndpoint} from "Frontend/generated/endpoints";
import {Form, Formik} from "formik";
import {Check} from "@phosphor-icons/react";
import {Button} from "@nextui-org/react";
import Input from "Frontend/components/general/Input";
import {PluginManagementEndpoint} from "Frontend/generated/endpoints";
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
import {PluginManagementCard} from "Frontend/components/general/PluginManagementCard";
import {Divider} from "@nextui-org/react";
export default function PluginManagement() {
const [configSaved, setConfigSaved] = useState(false);
const [igdbConfigMeta, setIgdbConfigMeta] = useState<any>();
const [igdbConfig, setIgdbConfig] = useState<any>();
const [plugins, setPlugins] = useState<PluginDto[]>([]);
useEffect(() => {
PluginConfigEndpoint.getConfigMetadata("igdb").then(setIgdbConfigMeta);
PluginConfigEndpoint.getConfig("igdb").then(setIgdbConfig);
PluginManagementEndpoint.getPlugins().then((response) => {
if (response === undefined) return;
setPlugins(response as PluginDto[]);
});
}, []);
useEffect(() => {
if (configSaved) {
setTimeout(() => setConfigSaved(false), 2000);
}
}, [configSaved])
async function handleSubmit(values: any) {
await PluginConfigEndpoint.setConfigEntries("igdb", values);
setConfigSaved(true);
}
return (
<>
<Formik
initialValues={{
clientId: igdbConfig?.clientId,
clientSecret: igdbConfig?.clientSecret
}}
enableReinitialize={true}
onSubmit={handleSubmit}
>
{(formik: { values: any; isSubmitting: any; }) => (
<Form>
<div className="flex flex-row flex-grow justify-between mb-8">
<h2 className="text-2xl font-bold">Plugins</h2>
<div className="flex flex-row items-center gap-4">
<Button
color="primary"
isLoading={formik.isSubmitting}
disabled={formik.isSubmitting || configSaved}
type="submit"
>
{formik.isSubmitting ? "" : configSaved ? <Check/> : "Save"}
</Button>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row flex-grow justify-between mb-8">
<h2 className="text-2xl font-bold">Plugins</h2>
</div>
<div className="flex flex-row flex-1 justify-between gap-16">
<div className="flex flex-col flex-grow">
<Section title="IGDB"/>
{igdbConfigMeta && igdbConfigMeta.map((entry: any) => (
<Input key={entry.key} name={entry.key} label={entry.name} type="text"/>
))}
</div>
</div>
</Form>
)}
</Formik>
</>
<Divider className="mb-4"/>
<div className="grid grid-cols-300px gap-4">
{plugins.map((plugin) => <PluginManagementCard plugin={plugin} key={plugin.name}/>)}
</div>
</div>
);
}
@@ -26,11 +26,7 @@ interface Role {
id: string;
}
export default function AssignRolesModal({
isOpen,
onOpenChange,
user
}: AssignRolesModalProps) {
export default function AssignRolesModal({isOpen, onOpenChange, user}: AssignRolesModalProps) {
const [availableRoles, setAvailableRoles] = useState<Role[]>([]);
const [selectedRole, setSelectedRole] = useState<Selection>();
const [error, setError] = useState<string>();
@@ -9,11 +9,7 @@ interface ConfirmUserDeletionModalProps {
user: UserInfoDto;
}
export default function ConfirmUserDeletionModal({
isOpen,
onOpenChange,
user
}: ConfirmUserDeletionModalProps) {
export default function ConfirmUserDeletionModal({isOpen, onOpenChange, user}: ConfirmUserDeletionModalProps) {
const [confirmUsername, setConfirmUsername] = useState<string>("");
useEffect(() => {
@@ -8,10 +8,7 @@ interface InviteUserModalProps {
onOpenChange: () => void;
}
export default function InviteUserModal({
isOpen,
onOpenChange
}: InviteUserModalProps) {
export default function InviteUserModal({isOpen, onOpenChange}: InviteUserModalProps) {
const [email, setEmail] = useState<string | null>();
const [error, setError] = useState<string | null>();
@@ -9,11 +9,7 @@ interface PasswordResetTokenModalProps {
token: TokenDto;
}
export default function PasswordResetTokenModal({
isOpen,
onOpenChange,
token
}: PasswordResetTokenModalProps) {
export default function PasswordResetTokenModal({isOpen, onOpenChange, token}: PasswordResetTokenModalProps) {
const [timeUntilExpiry, setTimeUntilExpiry] = useState<string>("");
const timeoutRefresh = setInterval(updateTimeUntilExpiry, 1000);
@@ -0,0 +1,75 @@
import React, {useEffect, useState} from "react";
import {Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@nextui-org/react";
import {toast} from "sonner";
import {Form, Formik} from "formik";
import {PluginConfigEndpoint} from "Frontend/generated/endpoints";
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
import PluginConfigElement from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement";
import Input from "Frontend/components/general/Input";
interface PluginConfigurationModalProps {
plugin: PluginDto;
isOpen: boolean;
onOpenChange: () => void;
}
export default function PluginConfigurationModal({plugin, isOpen, onOpenChange}: PluginConfigurationModalProps) {
const [pluginConfigMeta, setPluginConfigMeta] = useState<(PluginConfigElement)[]>();
const [pluginConfig, setPluginConfig] = useState<Record<string, string>>();
useEffect(() => {
PluginConfigEndpoint.getConfigMetadata(plugin.id).then(response => {
if (response === undefined) return;
setPluginConfigMeta(response as PluginConfigElement[]);
});
PluginConfigEndpoint.getConfig(plugin.id).then(response => {
if (response === undefined) return;
setPluginConfig(response as Record<string, string>);
});
}, []);
async function saveConfig(values: Record<string, string>) {
await PluginConfigEndpoint.setConfigEntries(plugin.id, values);
toast.success(`Configuration for ${plugin.name} saved!`);
}
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
<ModalContent>
{(onClose) => (
<Formik initialValues={pluginConfig}
enableReinitialize={true}
onSubmit={async (values: any) => {
await saveConfig(values);
onClose();
}}
>
{(formik: { isSubmitting: any; }) => (
<Form>
<ModalHeader className="flex flex-col gap-1">{plugin.name} configuration</ModalHeader>
<ModalBody>
{pluginConfigMeta && pluginConfigMeta.map((entry: any) => (
<Input key={entry.key} name={entry.key} label={entry.name} type="text"/>
))}
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
Cancel
</Button>
<Button
color="primary"
isLoading={formik.isSubmitting}
disabled={formik.isSubmitting}
type="submit"
>
{formik.isSubmitting ? "" : "Save"}
</Button>
</ModalFooter>
</Form>
)}
</Formik>
)}
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,50 @@
import {Card, Chip, Tooltip, useDisclosure} from "@nextui-org/react";
import {PuzzlePiece} from "@phosphor-icons/react";
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
import PluginState from "Frontend/generated/org/pf4j/PluginState";
import React from "react";
import PluginConfigurationModal from "Frontend/components/general/PluginConfigurationModal";
export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
const pluginConfigurationModal = useDisclosure();
function stateToColor(state: PluginState | undefined): string {
switch (state) {
case PluginState.STARTED:
return "success";
case PluginState.DISABLED:
return "warning";
case PluginState.STOPPED:
return "danger";
default:
return "";
}
}
return (
<>
<Card className="flex flex-row justify-between p-2"
isPressable={true} onPress={pluginConfigurationModal.onOpen}>
<div className="flex flex-row items-center gap-4">
<Tooltip placement="right" content={`Plugin ${plugin.state!.toLowerCase()}`}>
<PuzzlePiece size={64} weight="duotone" className={`text-${stateToColor(plugin.state)}`}/>
</Tooltip>
<div className="flex flex-col items-start gap-1">
<div className="flex flex-row gap-2">
<p className="font-semibold">{plugin.name}</p>
<div className="text-sm">
<Chip size="sm" radius="sm" className="text-xs">{plugin.version}</Chip>
</div>
</div>
<p className="text-sm">Author: {plugin.author}</p>
</div>
</div>
</Card>
<PluginConfigurationModal plugin={plugin}
isOpen={pluginConfigurationModal.isOpen}
onOpenChange={pluginConfigurationModal.onOpenChange}
/>
</>
)
}
@@ -1,4 +1,4 @@
package de.grimsi.gameyfin.core.plugins
package de.grimsi.gameyfin.core.plugins.config
import com.vaadin.hilla.Endpoint
import de.grimsi.gameyfin.core.Role
@@ -1,4 +1,4 @@
package de.grimsi.gameyfin.core.plugins
package de.grimsi.gameyfin.core.plugins.config
import de.grimsi.gameyfin.core.security.EncryptionConverter
import jakarta.persistence.Column
@@ -1,4 +1,4 @@
package de.grimsi.gameyfin.core.plugins
package de.grimsi.gameyfin.core.plugins.config
import org.springframework.data.jpa.repository.JpaRepository
@@ -1,7 +1,9 @@
package de.grimsi.gameyfin.core.plugins
package de.grimsi.gameyfin.core.plugins.config
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service
@Service
@@ -10,22 +12,28 @@ class PluginConfigService(
private val pluginManager: GameyfinPluginManager
) {
private val log = KotlinLogging.logger {}
fun getConfigMetadata(pluginId: String): List<PluginConfigElement> {
log.info { "Getting config metadata for plugin $pluginId" }
val plugin = pluginManager.getPlugin(pluginId).plugin as GameyfinPlugin
return plugin.configMetadata
}
fun getConfig(pluginId: String): Map<String, String?> {
log.info { "Getting config for plugin $pluginId" }
return pluginConfigRepository.findAllById_PluginId(pluginId).associate { it.id.key to it.value }
}
fun setConfigEntries(pluginId: String, config: Map<String, String>) {
log.info { "Setting config entries for plugin $pluginId" }
val entries = config.map { PluginConfigEntry(PluginConfigEntryKey(pluginId, it.key), it.value) }
pluginConfigRepository.saveAll(entries)
pluginManager.restart(pluginId)
}
fun setConfigEntry(pluginId: String, key: String, value: String) {
log.info { "Setting config entry $key for plugin $pluginId" }
val entry = PluginConfigEntry(PluginConfigEntryKey(pluginId, key), value)
pluginConfigRepository.save(entry)
pluginManager.restart(pluginId)
@@ -1,4 +1,4 @@
package de.grimsi.gameyfin.core.plugins
package de.grimsi.gameyfin.core.plugins.management
import org.pf4j.DevelopmentPluginLoader
import org.pf4j.PluginClassLoader
@@ -1,5 +1,6 @@
package de.grimsi.gameyfin.core.plugins
package de.grimsi.gameyfin.core.plugins.management
import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
import io.github.oshai.kotlinlogging.KotlinLogging
import org.pf4j.CompoundPluginLoader
@@ -0,0 +1,11 @@
package de.grimsi.gameyfin.core.plugins.management
import org.pf4j.PluginState
data class PluginDto(
val id: String,
val name: String,
val version: String,
val author: String,
val state: PluginState
)
@@ -0,0 +1,19 @@
package de.grimsi.gameyfin.core.plugins.management
import com.vaadin.hilla.Endpoint
import de.grimsi.gameyfin.core.Role
import jakarta.annotation.security.RolesAllowed
@Endpoint
@RolesAllowed(Role.Names.ADMIN)
class PluginManagementEndpoint(
private val pluginManagementService: PluginManagementService
) {
fun getPlugins() = pluginManagementService.getPlugins()
fun startPlugin(pluginId: String) = pluginManagementService.startPlugin(pluginId)
fun stopPlugin(pluginId: String) = pluginManagementService.stopPlugin(pluginId)
fun restartPlugin(pluginId: String) = pluginManagementService.restartPlugin(pluginId)
}
@@ -0,0 +1,32 @@
package de.grimsi.gameyfin.core.plugins.management
import org.springframework.stereotype.Service
@Service
class PluginManagementService(
private val pluginManager: GameyfinPluginManager
) {
fun getPlugins(): List<PluginDto> {
return pluginManager.plugins.map {
PluginDto(
it.pluginId,
it.descriptor.pluginDescription,
it.descriptor.version,
it.descriptor.provider,
it.pluginState
)
}
}
fun startPlugin(pluginId: String) {
pluginManager.startPlugin(pluginId)
}
fun stopPlugin(pluginId: String) {
pluginManager.stopPlugin(pluginId)
}
fun restartPlugin(pluginId: String) {
pluginManager.restart(pluginId)
}
}
@@ -1,5 +1,6 @@
package de.grimsi.gameyfin.core.plugins
package de.grimsi.gameyfin.core.plugins.management
import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.annotation.Bean