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