mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Implement plugin config validation
Refactor plugin management card
This commit is contained in:
@@ -19,11 +19,19 @@ export default function PluginManagement() {
|
||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||
<h2 className="text-2xl font-bold">Plugins</h2>
|
||||
</div>
|
||||
|
||||
<Divider className="mb-4"/>
|
||||
|
||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||
<h2 className="text-xl font-bold">Metadata</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-300px gap-4">
|
||||
{plugins.map((plugin) => <PluginManagementCard plugin={plugin} key={plugin.name}/>)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row flex-grow justify-between my-8">
|
||||
<h2 className="text-xl font-bold">Notifications</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+31
-14
@@ -6,14 +6,15 @@ 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";
|
||||
import {PuzzlePiece} from "@phosphor-icons/react";
|
||||
|
||||
interface PluginConfigurationModalProps {
|
||||
interface PluginDetailsModalProps {
|
||||
plugin: PluginDto;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
}
|
||||
|
||||
export default function PluginConfigurationModal({plugin, isOpen, onOpenChange}: PluginConfigurationModalProps) {
|
||||
export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) {
|
||||
const [pluginConfigMeta, setPluginConfigMeta] = useState<(PluginConfigElement)[]>();
|
||||
const [pluginConfig, setPluginConfig] = useState<Record<string, string>>();
|
||||
|
||||
@@ -46,24 +47,40 @@ export default function PluginConfigurationModal({plugin, isOpen, onOpenChange}:
|
||||
>
|
||||
{(formik: { isSubmitting: any; }) => (
|
||||
<Form>
|
||||
<ModalHeader className="flex flex-col gap-1">{plugin.name} configuration</ModalHeader>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Plugin configuration for {plugin.name}</ModalHeader>
|
||||
<ModalBody>
|
||||
{pluginConfigMeta && pluginConfigMeta.map((entry: any) => (
|
||||
<Input key={entry.key} name={entry.key} label={entry.name} type="text"/>
|
||||
))}
|
||||
<h4 className="text-l font-bold">Details</h4>
|
||||
<div className="flex flex-row gap-8">
|
||||
<PuzzlePiece size={64} weight="fill"/>
|
||||
<div className="grid grid-cols-2">
|
||||
<p>Author: {plugin.author}</p>
|
||||
<p>Version: {plugin.version}</p>
|
||||
<p>Plugin ID: {plugin.id}</p>
|
||||
<p>Status: {plugin.state?.toLowerCase()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 className="text-l font-bold mt-6">Configuration</h4>
|
||||
{(pluginConfigMeta && pluginConfigMeta.length > 0) ?
|
||||
pluginConfigMeta.map((entry: any) => (
|
||||
<Input key={entry.key} name={entry.key} label={entry.name} type="text"/>
|
||||
)) : "This plugin has no configuration options."
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : "Save"}
|
||||
</Button>
|
||||
{(pluginConfigMeta && pluginConfigMeta?.length > 0) ?
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : "Save"}
|
||||
</Button> : ""}
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
)}
|
||||
@@ -1,14 +1,29 @@
|
||||
import {Card, Chip, Tooltip, useDisclosure} from "@nextui-org/react";
|
||||
import {Card, Chip, Skeleton, useDisclosure} from "@nextui-org/react";
|
||||
import {PuzzlePiece} from "@phosphor-icons/react";
|
||||
import {PluginManagementEndpoint} from "Frontend/generated/endpoints";
|
||||
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";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import PluginDetailsModal from "Frontend/components/general/PluginDetailsModal";
|
||||
|
||||
export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
||||
const pluginConfigurationModal = useDisclosure();
|
||||
const pluginDetailsModal = useDisclosure();
|
||||
const [configValid, setConfigValid] = useState<boolean | undefined>(undefined);
|
||||
|
||||
function stateToColor(state: PluginState | undefined): string {
|
||||
useEffect(() => {
|
||||
PluginManagementEndpoint.validatePluginConfig(plugin.id).then((response: boolean) => {
|
||||
if (response === undefined) return;
|
||||
setConfigValid(response);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function borderColor(state: PluginState | undefined): "success" | "warning" | "danger" | "default" {
|
||||
if (configValid === undefined) return "default";
|
||||
if (!configValid) return "danger";
|
||||
return stateToColor(state);
|
||||
}
|
||||
|
||||
function stateToColor(state: PluginState | undefined): "success" | "warning" | "danger" | "default" {
|
||||
switch (state) {
|
||||
case PluginState.STARTED:
|
||||
return "success";
|
||||
@@ -17,32 +32,33 @@ export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
||||
case PluginState.STOPPED:
|
||||
return "danger";
|
||||
default:
|
||||
return "";
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<Card className={`flex flex-row justify-between p-2 border-2 border-${borderColor(plugin.state)}`}
|
||||
isPressable={true} onPress={pluginDetailsModal.onOpen}>
|
||||
<div className="flex flex-1 flex-col items-center gap-1">
|
||||
<PuzzlePiece size={64} weight="fill"/>
|
||||
<p className="font-semibold">{plugin.name}</p>
|
||||
<div className="flex flex-row gap-2">
|
||||
<Chip size="sm" radius="sm" className="text-xs">{plugin.version}</Chip>
|
||||
<Chip size="sm" radius="sm" className="text-xs"
|
||||
color={stateToColor(plugin.state)}>{plugin.state?.toLowerCase()}</Chip>
|
||||
{configValid === undefined ?
|
||||
<Skeleton className="rounded-md h-6 w-20"></Skeleton>
|
||||
: configValid ?
|
||||
<Chip size="sm" radius="sm" className="text-xs" color="success">config valid</Chip> :
|
||||
<Chip size="sm" radius="sm" className="text-xs" color="danger">config invalid</Chip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<PluginConfigurationModal plugin={plugin}
|
||||
isOpen={pluginConfigurationModal.isOpen}
|
||||
onOpenChange={pluginConfigurationModal.onOpenChange}
|
||||
<PluginDetailsModal plugin={plugin}
|
||||
isOpen={pluginDetailsModal.isOpen}
|
||||
onOpenChange={pluginDetailsModal.onOpenChange}
|
||||
/>
|
||||
</>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import TokenDto from "Frontend/generated/de/grimsi/gameyfin/shared/token/TokenDt
|
||||
import UserInfoDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserInfoDto";
|
||||
import RoleChip from "Frontend/components/general/RoleChip";
|
||||
import AssignRolesModal from "Frontend/components/general/AssignRolesModal";
|
||||
import Role from "Frontend/generated/de/grimsi/gameyfin/core/Role";
|
||||
|
||||
export function UserManagementCard({user}: { user: UserInfoDto }) {
|
||||
const userDeletionConfirmationModal = useDisclosure();
|
||||
@@ -123,7 +122,7 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {
|
||||
<p className="font-semibold">{user.username}</p>
|
||||
<p className="text-sm">{user.email}</p>
|
||||
{user.roles?.map((role) => (
|
||||
<RoleChip role={role as Role}/>
|
||||
<RoleChip role={role as string}/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function MainLayout() {
|
||||
const [isExploding, setIsExploding] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let newTitle = `Gameyfin - ${routeMetadata?.title}` ?? 'Gameyfin';
|
||||
let newTitle = `Gameyfin - ${routeMetadata?.title}`;
|
||||
window.addEventListener('popstate', () => document.title = newTitle);
|
||||
loadUserTheme().catch(console.error);
|
||||
}, []);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Link} from "react-router-dom";
|
||||
import {Button, Input} from "@nextui-org/react";
|
||||
import {toast} from "sonner";
|
||||
import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints.js";
|
||||
import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints";
|
||||
import {useState} from "react";
|
||||
import Game from "Frontend/generated/de/grimsi/gameyfin/games/Game";
|
||||
|
||||
|
||||
+8
@@ -59,6 +59,14 @@ class GameyfinPluginManager(
|
||||
plugin.start()
|
||||
}
|
||||
|
||||
fun validatePluginConfig(pluginId: String): Boolean {
|
||||
val plugin = getPlugin(pluginId)?.plugin ?: return false
|
||||
if (plugin is GameyfinPlugin) {
|
||||
return plugin.validateConfig()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun configurePlugin(pluginWrapper: PluginWrapper) {
|
||||
val plugin = pluginWrapper.plugin
|
||||
if (plugin is GameyfinPlugin) {
|
||||
|
||||
+2
@@ -20,4 +20,6 @@ class PluginManagementEndpoint(
|
||||
fun enablePlugin(pluginId: String) = pluginManagementService.enablePlugin(pluginId)
|
||||
|
||||
fun disablePlugin(pluginId: String) = pluginManagementService.disablePlugin(pluginId)
|
||||
|
||||
fun validatePluginConfig(pluginId: String): Boolean = pluginManagementService.validatePluginConfig(pluginId)
|
||||
}
|
||||
+4
@@ -37,4 +37,8 @@ class PluginManagementService(
|
||||
fun disablePlugin(pluginId: String) {
|
||||
pluginManager.disablePlugin(pluginId)
|
||||
}
|
||||
|
||||
fun validatePluginConfig(pluginId: String): Boolean {
|
||||
return pluginManager.validatePluginConfig(pluginId)
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,10 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
|
||||
open fun loadConfig(config: Map<String, String?>) {
|
||||
this.config = config
|
||||
}
|
||||
|
||||
open fun validateConfig(): Boolean {
|
||||
return validateConfig(config)
|
||||
}
|
||||
|
||||
abstract fun validateConfig(config: Map<String, String?>): Boolean
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import org.pf4j.Extension
|
||||
import org.pf4j.PluginWrapper
|
||||
import java.time.Instant
|
||||
import kotlin.collections.filter
|
||||
|
||||
class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||
|
||||
@@ -22,6 +21,16 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||
PluginConfigElement("clientSecret", "Twitch client secret", "Your Twitch Client Secret")
|
||||
)
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): Boolean {
|
||||
try {
|
||||
authenticate()
|
||||
return true
|
||||
} catch (e: PluginConfigError) {
|
||||
log.error(e.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
try {
|
||||
authenticate()
|
||||
|
||||
@@ -4,19 +4,15 @@ import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.*
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import org.pf4j.Extension
|
||||
import org.pf4j.PluginWrapper
|
||||
@@ -26,7 +22,7 @@ import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
data class SteamSearchResult(
|
||||
@@ -64,6 +60,10 @@ data class Platforms(
|
||||
class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||
override val configMetadata: List<PluginConfigElement> = emptyList()
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@Extension
|
||||
class SteamMetadataProvider : GameMetadataProvider {
|
||||
val client = HttpClient(CIO) {
|
||||
|
||||
Reference in New Issue
Block a user