Cast plugin config values in frontend

This commit is contained in:
grimsi
2025-06-05 22:53:41 +02:00
parent 748a75b675
commit 2717c4deda
@@ -8,7 +8,7 @@ import {PluginEndpoint} from "Frontend/generated/endpoints";
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto"; import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
import {ArrowClockwise} from "@phosphor-icons/react"; import {ArrowClockwise} from "@phosphor-icons/react";
import PluginConfigMetadataDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto"; import PluginConfigMetadataDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto";
import PluginConfigFormField from "Frontend/components/general/input/PluginConfigFormField"; import PluginConfigFormField from "Frontend/components/general/plugin/PluginConfigFormField";
interface PluginDetailsModalProps { interface PluginDetailsModalProps {
plugin: PluginDto; plugin: PluginDto;
@@ -35,18 +35,26 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
}); });
} }
function getEffectiveConfig(): Record<string, string> { function getEffectiveConfig(): Record<string, any> {
const effectiveConfig: Record<string, string> = {}; const effectiveConfig: Record<string, any> = {};
if (!plugin.configMetadata) return effectiveConfig; if (!plugin.configMetadata) return effectiveConfig;
for (const meta of plugin.configMetadata) { for (const meta of plugin.configMetadata) {
const key = meta.key; const key = meta.key;
let value = plugin.config?.[key]?.toString(); let value = plugin.config?.[key] ?? meta.default;
if (value == null && meta.default != null) {
value = meta.default.toString(); if (value != null) {
} switch (meta.type.toLowerCase()) {
if (value) { case "float":
effectiveConfig[key] = value; case "int":
effectiveConfig[key] = Number(value);
break;
case "boolean":
effectiveConfig[key] = value === true || value === "true";
break;
default:
effectiveConfig[key] = value.toString();
}
} }
} }
return effectiveConfig; return effectiveConfig;
@@ -55,132 +63,138 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg"> <Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
<ModalContent> <ModalContent>
{(onClose) => ( {(onClose) => {
<Formik initialValues={getEffectiveConfig()}
initialErrors={plugin.configValidation?.errors}
enableReinitialize={true}
onSubmit={async (values: any) => {
await saveConfig(values);
onClose();
}}
>
{(formik: any) => (
<Form>
<ModalHeader className="flex flex-col gap-1">
Plugin configuration for {plugin.name}
</ModalHeader>
<ModalBody>
<div className="flex flex-col text-sm">
<div className="flex flex-row items-center gap-8 mb-4">
<PluginLogo plugin={plugin}/>
<table className="text-left table-auto">
<tbody>
{Object.entries({
"Author": plugin.author,
"Version": plugin.version,
"License": plugin.license,
"URL": <Link isExternal
showAnchorIcon
color="foreground"
size="sm"
href={plugin.url}>
{plugin.url}
</Link>,
}).map(([key, value]) => {
if (!value) return;
return (
<tr key={key}>
<td className="text-default-500 w-0 min-w-20">{key}</td>
<td className="flex flex-row gap-1">{value}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<p className="text-default-500">Description</p>
<Markdown
remarkPlugins={[remarkBreaks]}
components={{
a(props) {
return <Link isExternal
showAnchorIcon
color="foreground"
underline="always"
href={props.href}
size="sm">
{props.children}
</Link>
}
}}
>{plugin.description}</Markdown>
</div>
<div className="flex flex-row items-center mt-4 gap-2"> async function handleSubmit(values: Record<string, string>): Promise<void> {
<h4 className="text-l font-bold">Configuration</h4> await saveConfig(values);
{(plugin.configMetadata && plugin.configMetadata.length > 0) && <> onClose();
<div className="flex-1"/> }
{(() => {
switch (configValidated) { return (
case ValidationState.VALID: <Formik initialValues={getEffectiveConfig()}
return <p className="text-small text-success"> initialErrors={plugin.configValidation?.errors}
Configuration valid enableReinitialize={true}
</p>; onSubmit={handleSubmit}
case ValidationState.INVALID: >
return <p className="text-small text-danger"> {(formik: any) => (
Configuration invalid <Form>
</p>; <ModalHeader className="flex flex-col gap-1">
default: Plugin configuration for {plugin.name}
return null; </ModalHeader>
} <ModalBody>
})()} <div className="flex flex-col text-sm">
<Tooltip content="Re-validate configuration" placement="bottom" <div className="flex flex-row items-center gap-8 mb-4">
color="foreground"> <PluginLogo plugin={plugin}/>
<Button isIconOnly variant="light" size="sm" <table className="text-left table-auto">
isLoading={configValidated === ValidationState.IN_PROGRESS} <tbody>
onPress={async () => { {Object.entries({
setConfigValidated(ValidationState.IN_PROGRESS); "Author": plugin.author,
let result = await PluginEndpoint.validateNewConfig(plugin.id, formik.values) "Version": plugin.version,
if (result.errors) { "License": plugin.license,
formik.setErrors(result.errors); "URL": <Link isExternal
setConfigValidated(ValidationState.INVALID); showAnchorIcon
} else { color="foreground"
setConfigValidated(ValidationState.VALID); size="sm"
} href={plugin.url}>
setTimeout(() => setConfigValidated(ValidationState.UNCHECKED), 5000); {plugin.url}
}}> </Link>,
<ArrowClockwise/> }).map(([key, value]) => {
</Button> if (!value) return;
</Tooltip> return (
</>} <tr key={key}>
</div> <td className="text-default-500 w-0 min-w-20">{key}</td>
{(plugin.configMetadata && plugin.configMetadata.length > 0) ? <td className="flex flex-row gap-1">{value}</td>
plugin.configMetadata.map((entry: PluginConfigMetadataDto) => ( </tr>
<PluginConfigFormField )
key={entry.key} })}
pluginConfigMetadata={entry} </tbody>
showErrorUntouched={true}/> </table>
)) : "This plugin has no configuration options." </div>
} <p className="text-default-500">Description</p>
</ModalBody> <Markdown
<ModalFooter> remarkPlugins={[remarkBreaks]}
<Button variant="light" onPress={onClose}> components={{
Cancel a(props) {
</Button> return <Link isExternal
{(plugin.configMetadata && plugin.configMetadata?.length > 0) ? showAnchorIcon
<Button color="foreground"
color="primary" underline="always"
isLoading={formik.isSubmitting} href={props.href}
isDisabled={formik.isSubmitting || !formik.dirty} size="sm">
type="submit" {props.children}
> </Link>
{formik.isSubmitting ? "" : "Save"} }
</Button> : ""} }}
</ModalFooter> >{plugin.description}</Markdown>
</Form> </div>
)}
</Formik> <div className="flex flex-row items-center mt-4 gap-2">
)} <h4 className="text-l font-bold">Configuration</h4>
{(plugin.configMetadata && plugin.configMetadata.length > 0) && <>
<div className="flex-1"/>
{(() => {
switch (configValidated) {
case ValidationState.VALID:
return <p className="text-small text-success">
Configuration valid
</p>;
case ValidationState.INVALID:
return <p className="text-small text-danger">
Configuration invalid
</p>;
default:
return null;
}
})()}
<Tooltip content="Re-validate configuration" placement="bottom"
color="foreground">
<Button isIconOnly variant="light" size="sm"
isLoading={configValidated === ValidationState.IN_PROGRESS}
onPress={async () => {
setConfigValidated(ValidationState.IN_PROGRESS);
let result = await PluginEndpoint.validateNewConfig(plugin.id, formik.values)
if (result.errors) {
formik.setErrors(result.errors);
setConfigValidated(ValidationState.INVALID);
} else {
setConfigValidated(ValidationState.VALID);
}
setTimeout(() => setConfigValidated(ValidationState.UNCHECKED), 5000);
}}>
<ArrowClockwise/>
</Button>
</Tooltip>
</>}
</div>
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
plugin.configMetadata.map((entry: PluginConfigMetadataDto) => (
<PluginConfigFormField
key={entry.key}
pluginConfigMetadata={entry}
showErrorUntouched={true}/>
)) : "This plugin has no configuration options."
}
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
Cancel
</Button>
{(plugin.configMetadata && plugin.configMetadata?.length > 0) ?
<Button
color="primary"
isLoading={formik.isSubmitting}
isDisabled={formik.isSubmitting || !formik.dirty}
type="submit"
>
{formik.isSubmitting ? "" : "Save"}
</Button> : ""}
</ModalFooter>
</Form>
)
}
</Formik>
)
}}
</ModalContent> </ModalContent>
</Modal> </Modal>
); );