mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Finish plugin support implementation
Small refactoring
This commit is contained in:
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package de.grimsi.gameyfin.core.plugins
|
||||
package de.grimsi.gameyfin.core.plugins.config
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
+9
-1
@@ -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
-1
@@ -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
|
||||
+2
-1
@@ -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
|
||||
)
|
||||
+19
@@ -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)
|
||||
}
|
||||
+32
@@ -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)
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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
|
||||
Reference in New Issue
Block a user