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 {ArrowClockwise} from "@phosphor-icons/react";
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 {
plugin: PluginDto;
@@ -35,18 +35,26 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
});
}
function getEffectiveConfig(): Record<string, string> {
const effectiveConfig: Record<string, string> = {};
function getEffectiveConfig(): Record<string, any> {
const effectiveConfig: Record<string, any> = {};
if (!plugin.configMetadata) return effectiveConfig;
for (const meta of plugin.configMetadata) {
const key = meta.key;
let value = plugin.config?.[key]?.toString();
if (value == null && meta.default != null) {
value = meta.default.toString();
}
if (value) {
effectiveConfig[key] = value;
let value = plugin.config?.[key] ?? meta.default;
if (value != null) {
switch (meta.type.toLowerCase()) {
case "float":
case "int":
effectiveConfig[key] = Number(value);
break;
case "boolean":
effectiveConfig[key] = value === true || value === "true";
break;
default:
effectiveConfig[key] = value.toString();
}
}
}
return effectiveConfig;
@@ -55,132 +63,138 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
<ModalContent>
{(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>
{(onClose) => {
<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>
)}
async function handleSubmit(values: Record<string, string>): Promise<void> {
await saveConfig(values);
onClose();
}
return (
<Formik initialValues={getEffectiveConfig()}
initialErrors={plugin.configValidation?.errors}
enableReinitialize={true}
onSubmit={handleSubmit}
>
{(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">
<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>
</Modal>
);