mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 08:15:37 +00:00
Enable real-time UI starting with config pages
Implement client side state caching and updating via websocket Simplify config REST endpoints
This commit is contained in:
Generated
+31
@@ -53,6 +53,7 @@
|
||||
"react-router": "7.5.2",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"swiper": "^11.2.6",
|
||||
"valtio": "^2.1.5",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -15816,6 +15817,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
|
||||
"integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
@@ -18159,6 +18166,30 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/valtio": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/valtio/-/valtio-2.1.5.tgz",
|
||||
"integrity": "sha512-vsh1Ixu5mT0pJFZm+Jspvhga5GzHUTYv0/+Th203pLfh3/wbHwxhu/Z2OkZDXIgHfjnjBns7SN9HNcbDvPmaGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"proxy-compare": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"react": ">=18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vfile": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"react-router": "7.5.2",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"swiper": "^11.2.6",
|
||||
"valtio": "^2.1.5",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -136,7 +137,8 @@
|
||||
"swiper": "$swiper",
|
||||
"react-player": "$react-player",
|
||||
"react-markdown": "$react-markdown",
|
||||
"remark-breaks": "$remark-breaks"
|
||||
"remark-breaks": "$remark-breaks",
|
||||
"valtio": "$valtio"
|
||||
},
|
||||
"vaadin": {
|
||||
"dependencies": {
|
||||
@@ -197,6 +199,6 @@
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-precaching": "7.3.0"
|
||||
},
|
||||
"hash": "f66bddf01ef8dd09a431102050ddd561b4587fdd43bcbff127b93872febbee92"
|
||||
"hash": "c57af53043f6c6a9f0b03c75c28c3fde0bbfd828f9ce0e179263959c61ec888d"
|
||||
}
|
||||
}
|
||||
@@ -119,4 +119,4 @@ const validationSchema = Yup.object({
|
||||
})
|
||||
});
|
||||
|
||||
export const LibraryManagement = withConfigPage(LibraryManagementLayout, "Library Management", "library", validationSchema);
|
||||
export const LibraryManagement = withConfigPage(LibraryManagementLayout, "Library Management", validationSchema);
|
||||
@@ -94,4 +94,4 @@ const validationSchema = Yup.object({
|
||||
})
|
||||
});
|
||||
|
||||
export const LogManagement = withConfigPage(LogManagementLayout, "Logging", "logs", validationSchema);
|
||||
export const LogManagement = withConfigPage(LogManagementLayout, "Logging", validationSchema);
|
||||
@@ -9,7 +9,7 @@ import MessageTemplateDto from "Frontend/generated/de/grimsi/gameyfin/messages/t
|
||||
import SendTestNotificationModal from "Frontend/components/administration/messages/SendTestNotificationModal";
|
||||
import EditTemplateModal from "Frontend/components/administration/messages/EditTemplateModel";
|
||||
|
||||
function MessageManagementLayout({getConfig, getConfigs, formik}: any) {
|
||||
function MessageManagementLayout({getConfig, formik}: any) {
|
||||
|
||||
const editorModal = useDisclosure();
|
||||
const testNotificationModal = useDisclosure();
|
||||
|
||||
@@ -121,4 +121,4 @@ const validationSchema = Yup.object({
|
||||
})
|
||||
});
|
||||
|
||||
export const SsoManagement = withConfigPage(SsoManagementLayout, "Single Sign-On", "sso", validationSchema);
|
||||
export const SsoManagement = withConfigPage(SsoManagementLayout, "Single Sign-On", validationSchema);
|
||||
@@ -26,4 +26,4 @@ function SystemManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export const SystemManagement = withConfigPage(SystemManagementLayout, "System", "system", null);
|
||||
export const SystemManagement = withConfigPage(SystemManagementLayout, "System");
|
||||
@@ -2,27 +2,25 @@ import React, {useEffect, useState} from "react";
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
import Section from "Frontend/components/general/Section";
|
||||
import {ConfigEndpoint, UserEndpoint} from "Frontend/generated/endpoints";
|
||||
import {UserEndpoint} from "Frontend/generated/endpoints";
|
||||
import UserInfoDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserInfoDto";
|
||||
import {UserManagementCard} from "Frontend/components/general/cards/UserManagementCard";
|
||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
||||
import {Info, UserPlus} from "@phosphor-icons/react";
|
||||
import {Button, Divider, Tooltip, useDisclosure} from "@heroui/react";
|
||||
import InviteUserModal from "Frontend/components/general/modals/InviteUserModal";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {configState} from "Frontend/state/ConfigState";
|
||||
|
||||
function UserManagementLayout({getConfig, formik}: any) {
|
||||
const inviteUserModal = useDisclosure();
|
||||
const [users, setUsers] = useState<UserInfoDto[]>([]);
|
||||
const [autoRegisterNewUsers, setAutoRegisterNewUsers] = useState(true);
|
||||
const autoRegisterNewUsers = useSnapshot(configState);
|
||||
|
||||
useEffect(() => {
|
||||
UserEndpoint.getAllUsers().then(
|
||||
(response) => setUsers(response)
|
||||
);
|
||||
|
||||
ConfigEndpoint.get("sso.oidc.auto-register-new-users").then(
|
||||
(response) => setAutoRegisterNewUsers(response as boolean)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -56,4 +54,4 @@ function UserManagementLayout({getConfig, formik}: any) {
|
||||
);
|
||||
}
|
||||
|
||||
export const UserManagement = withConfigPage(UserManagementLayout, "User Management", "users");
|
||||
export const UserManagement = withConfigPage(UserManagementLayout, "User Management");
|
||||
@@ -1,30 +1,22 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {ConfigEndpoint} from "Frontend/generated/endpoints";
|
||||
import ConfigEntryDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigEntryDto";
|
||||
import {Form, Formik} from "formik";
|
||||
import {Button, Skeleton} from "@heroui/react";
|
||||
import {Check, Info} from "@phosphor-icons/react";
|
||||
import ConfigValuePairDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigValuePairDto";
|
||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
||||
import {configState, initializeConfig, NestedConfig} from "Frontend/state/ConfigState";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
|
||||
type NestedConfig = {
|
||||
[field: string]: any;
|
||||
}
|
||||
|
||||
export default function withConfigPage(WrappedComponent: React.ComponentType<any>, title: String, configPrefix: string, validationSchema?: any) {
|
||||
export default function withConfigPage(WrappedComponent: React.ComponentType<any>, title: String, validationSchema?: any) {
|
||||
return function ConfigPage(props: any) {
|
||||
const isInitialized = useRef(false);
|
||||
const [configSaved, setConfigSaved] = useState(false);
|
||||
const [configDtos, setConfigDtos] = useState<ConfigEntryDto[]>([]);
|
||||
const [nestedConfigDtos, setNestedConfigDtos] = useState<NestedConfig>({});
|
||||
const [saveMessage, setSaveMessage] = useState<string>();
|
||||
|
||||
const state = useSnapshot(configState);
|
||||
|
||||
useEffect(() => {
|
||||
ConfigEndpoint.getAll(configPrefix).then((response: any) => {
|
||||
setConfigDtos(response as ConfigEntryDto[]);
|
||||
setNestedConfigDtos(toNestedConfig(response as ConfigEntryDto[]));
|
||||
isInitialized.current = true;
|
||||
});
|
||||
initializeConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,149 +26,111 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
}, [configSaved])
|
||||
|
||||
async function handleSubmit(values: NestedConfig): Promise<void> {
|
||||
const configValues = toConfigValuePair(values);
|
||||
await ConfigEndpoint.setAll(configValues);
|
||||
setNestedConfigDtos(values);
|
||||
const changed = getChangedValues(state.configNested, values);
|
||||
await ConfigEndpoint.update({updates: changed});
|
||||
setConfigSaved(true);
|
||||
}
|
||||
|
||||
function getConfig(key: string): ConfigEntryDto | undefined {
|
||||
return configDtos.find((configDto: ConfigEntryDto) => configDto.key === key);
|
||||
// @ts-ignore
|
||||
return state.configEntries[key];
|
||||
}
|
||||
|
||||
function getConfigs(prefix: string): ConfigEntryDto[] {
|
||||
return configDtos.filter((configDto: ConfigEntryDto) => configDto.key?.startsWith(prefix));
|
||||
}
|
||||
|
||||
function toNestedConfig(configArray: ConfigEntryDto[]): NestedConfig {
|
||||
const nestedConfig: NestedConfig = {};
|
||||
|
||||
configArray.forEach(item => {
|
||||
const keys = item.key!.split('.');
|
||||
let currentLevel = nestedConfig;
|
||||
|
||||
// Traverse the nested structure and create objects as needed
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
// Convert value to the appropriate type
|
||||
let value: any;
|
||||
switch (item.type) {
|
||||
case 'Boolean':
|
||||
value = typeof item.value == 'boolean' ? item.value : item.value === 'true';
|
||||
break;
|
||||
case 'Int':
|
||||
value = typeof item.value == 'number' ? item.value : 0;
|
||||
break;
|
||||
case 'Float':
|
||||
value = typeof item.value == 'number' ? item.value : 0.0;
|
||||
break;
|
||||
case 'Array':
|
||||
if (Array.isArray(item.value)) {
|
||||
switch (item.elementType) {
|
||||
case 'Boolean':
|
||||
value = item.value.map(v => typeof v === 'boolean' ? v : v === 'true');
|
||||
break;
|
||||
case 'Int':
|
||||
case 'Integer':
|
||||
value = item.value.map(v => typeof v == 'number' ? v : 0);
|
||||
break;
|
||||
case 'Float':
|
||||
value = item.value.map(v => typeof v == 'number' ? v : 0.0);
|
||||
break;
|
||||
case 'String':
|
||||
default:
|
||||
value = item.value.map(v => v.toString());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
value = [];
|
||||
}
|
||||
break;
|
||||
case 'String':
|
||||
default:
|
||||
value = item.value;
|
||||
break;
|
||||
function getChangedValues(initial: NestedConfig, current: NestedConfig): Record<string, any> {
|
||||
const flatten = (obj: NestedConfig, parentKey = ''): Record<string, any> => {
|
||||
let result: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||
Object.assign(result, flatten(obj[key], newKey));
|
||||
} else {
|
||||
result[newKey] = obj[key];
|
||||
}
|
||||
currentLevel[key] = value;
|
||||
} else {
|
||||
if (!currentLevel[key]) {
|
||||
currentLevel[key] = {};
|
||||
}
|
||||
currentLevel = currentLevel[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
return nestedConfig;
|
||||
}
|
||||
|
||||
function toConfigValuePair(obj: NestedConfig, parentKey: string = ''): ConfigValuePairDto[] {
|
||||
let result: ConfigValuePairDto[] = [];
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||
result = result.concat(toConfigValuePair(obj[key], newKey));
|
||||
} else {
|
||||
result.push({key: newKey, value: obj[key]});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const arraysEqual = (a: any[], b: any[]): boolean => {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (Array.isArray(a[i]) && Array.isArray(b[i])) {
|
||||
if (!arraysEqual(a[i], b[i])) return false;
|
||||
} else if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const flatInitial = flatten(initial);
|
||||
const flatCurrent = flatten(current);
|
||||
|
||||
const changed: Record<string, any> = {};
|
||||
for (const key in flatCurrent) {
|
||||
const valA = flatCurrent[key];
|
||||
const valB = flatInitial[key];
|
||||
if (Array.isArray(valA) && Array.isArray(valB)) {
|
||||
if (!arraysEqual(valA, valB)) {
|
||||
changed[key] = valA;
|
||||
}
|
||||
} else if (valA !== valB) {
|
||||
changed[key] = valA;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!isInitialized.current) {
|
||||
return (
|
||||
[...Array(4)].map((_e, i) =>
|
||||
<div className="flex flex-col flex-grow gap-8 mb-12" key={i}>
|
||||
<Skeleton className="h-10 w-full rounded-md"/>
|
||||
<Skeleton className="h-12 flex w-1/3 rounded-md"/>
|
||||
<div className="flex flex-row gap-8">
|
||||
<Skeleton className="h-12 flex w-1/3 rounded-md"/>
|
||||
<Skeleton className="h-12 flex w-1/3 rounded-md"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
return changed;
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={nestedConfigDtos}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validationSchema}
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<div className="flex flex-row flex-grow justify-between">
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
<>
|
||||
{state.isLoaded ?
|
||||
<Formik
|
||||
initialValues={state.configNested}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validationSchema}
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<div className="flex flex-row flex-grow justify-between">
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{saveMessage && <SmallInfoField icon={Info}
|
||||
message={saveMessage}
|
||||
className="text-warning"/>}
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{saveMessage && <SmallInfoField icon={Info}
|
||||
message={saveMessage}
|
||||
className="text-warning"/>}
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
isDisabled={formik.isSubmitting || configSaved || !formik.dirty}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : configSaved ? <Check/> : "Save"}
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
isDisabled={formik.isSubmitting || configSaved || !formik.dirty}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : configSaved ? <Check/> : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WrappedComponent {...props}
|
||||
getConfig={getConfig}
|
||||
formik={formik}
|
||||
setSaveMessage={setSaveMessage}/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik> :
|
||||
[...Array(4)].map((_e, i) =>
|
||||
<div className="flex flex-col flex-grow gap-8 mb-12" key={i}>
|
||||
<Skeleton className="h-10 w-full rounded-md"/>
|
||||
<Skeleton className="h-12 flex w-1/3 rounded-md"/>
|
||||
<div className="flex flex-row gap-8">
|
||||
<Skeleton className="h-12 flex w-1/3 rounded-md"/>
|
||||
<Skeleton className="h-12 flex w-1/3 rounded-md"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WrappedComponent {...props}
|
||||
getConfig={getConfig}
|
||||
getConfigs={getConfigs}
|
||||
formik={formik}
|
||||
setSaveMessage={setSaveMessage}/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ const SelectInput = ({label, values, ...props}) => {
|
||||
{...props}
|
||||
id={field.name}
|
||||
label={label}
|
||||
defaultSelectedKeys={[field.value]}
|
||||
selectedKeys={[field.value]}
|
||||
disallowEmptySelection
|
||||
>
|
||||
{values.map((value: string) => (
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import {proxy} from 'valtio';
|
||||
import ConfigEntryDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigEntryDto";
|
||||
import {ConfigEndpoint} from "Frontend/generated/endpoints";
|
||||
import ConfigUpdateDto from "Frontend/generated/de/grimsi/gameyfin/config/dto/ConfigUpdateDto";
|
||||
|
||||
type ConfigState = {
|
||||
isLoaded: boolean;
|
||||
configEntries: Record<string, ConfigEntryDto>;
|
||||
configNested: NestedConfig;
|
||||
autoRegisterNewUsers: boolean;
|
||||
};
|
||||
|
||||
export const configState = proxy<ConfigState>({
|
||||
configEntries: {},
|
||||
isLoaded: false,
|
||||
get configNested() {
|
||||
return toNestedConfig(Object.values(this.configEntries));
|
||||
},
|
||||
get autoRegisterNewUsers() {
|
||||
return this.configNested["sso.oidc.auto-register-new-users"] as boolean;
|
||||
}
|
||||
});
|
||||
|
||||
/** Subscribe to and process state updates from backend **/
|
||||
export async function initializeConfig() {
|
||||
if (configState.isLoaded) return;
|
||||
|
||||
// Fetch initial configuration data
|
||||
const initialEntries = await ConfigEndpoint.getAll();
|
||||
initialEntries.forEach((entry) => {
|
||||
configState.configEntries[entry.key] = entry;
|
||||
});
|
||||
configState.isLoaded = true;
|
||||
|
||||
// Subscribe to real-time updates
|
||||
ConfigEndpoint.subscribe().onNext((updateDto: ConfigUpdateDto) => {
|
||||
Object.entries(updateDto.updates).forEach(([key, value]) => {
|
||||
if (configState.configEntries[key]) {
|
||||
configState.configEntries[key].value = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Computed properties **/
|
||||
|
||||
export type NestedConfig = {
|
||||
[field: string]: any;
|
||||
}
|
||||
|
||||
function toNestedConfig(configArray: ConfigEntryDto[]): NestedConfig {
|
||||
const nestedConfig: NestedConfig = {};
|
||||
|
||||
configArray.forEach(item => {
|
||||
const keys = item.key!.split('.');
|
||||
let currentLevel = nestedConfig;
|
||||
|
||||
// Traverse the nested structure and create objects as needed
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
// Convert value to the appropriate type
|
||||
let value: any;
|
||||
switch (item.type) {
|
||||
case 'Boolean':
|
||||
value = typeof item.value == 'boolean' ? item.value : item.value === 'true';
|
||||
break;
|
||||
case 'Int':
|
||||
value = typeof item.value == 'number' ? item.value : 0;
|
||||
break;
|
||||
case 'Float':
|
||||
value = typeof item.value == 'number' ? item.value : 0.0;
|
||||
break;
|
||||
case 'Array':
|
||||
if (Array.isArray(item.value)) {
|
||||
switch (item.elementType) {
|
||||
case 'Boolean':
|
||||
value = item.value.map(v => typeof v === 'boolean' ? v : v === 'true');
|
||||
break;
|
||||
case 'Int':
|
||||
case 'Integer':
|
||||
value = item.value.map(v => typeof v == 'number' ? v : 0);
|
||||
break;
|
||||
case 'Float':
|
||||
value = item.value.map(v => typeof v == 'number' ? v : 0.0);
|
||||
break;
|
||||
case 'String':
|
||||
default:
|
||||
value = item.value.map(v => v.toString());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
value = [];
|
||||
}
|
||||
break;
|
||||
case 'String':
|
||||
default:
|
||||
value = item.value;
|
||||
break;
|
||||
}
|
||||
currentLevel[key] = value;
|
||||
} else {
|
||||
if (!currentLevel[key]) {
|
||||
currentLevel[key] = {};
|
||||
}
|
||||
currentLevel = currentLevel[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
return nestedConfig;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import com.vaadin.flow.server.auth.AnonymousAllowed
|
||||
import com.vaadin.hilla.Endpoint
|
||||
import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
||||
import de.grimsi.gameyfin.config.dto.ConfigValuePairDto
|
||||
import de.grimsi.gameyfin.config.dto.ConfigUpdateDto
|
||||
import de.grimsi.gameyfin.core.Role
|
||||
import jakarta.annotation.security.PermitAll
|
||||
import jakarta.annotation.security.RolesAllowed
|
||||
import java.io.Serializable
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Sinks
|
||||
|
||||
@Endpoint
|
||||
@RolesAllowed(Role.Names.ADMIN)
|
||||
@@ -15,29 +17,19 @@ class ConfigEndpoint(
|
||||
) {
|
||||
|
||||
/** CRUD endpoints for admins **/
|
||||
private val configUpdates = Sinks.many().multicast().onBackpressureBuffer<ConfigUpdateDto>()
|
||||
|
||||
fun getAll(prefix: String?): List<ConfigEntryDto> {
|
||||
return config.getAll(prefix)
|
||||
fun getAll(): List<ConfigEntryDto> {
|
||||
return config.getAll(null)
|
||||
}
|
||||
|
||||
fun get(key: String): Serializable? {
|
||||
return config.get(key)
|
||||
}
|
||||
// FIXME
|
||||
@AnonymousAllowed
|
||||
fun subscribe(): Flux<ConfigUpdateDto> = configUpdates.asFlux()
|
||||
|
||||
fun set(key: String, value: String) {
|
||||
config.set(key, value)
|
||||
}
|
||||
|
||||
fun setAll(configs: List<ConfigValuePairDto>) {
|
||||
config.setAll(configs)
|
||||
}
|
||||
|
||||
fun resetConfig(key: String) {
|
||||
config.deleteConfig(key)
|
||||
}
|
||||
|
||||
fun deleteConfig(key: String) {
|
||||
config.deleteConfig(key)
|
||||
fun update(update: ConfigUpdateDto) {
|
||||
config.update(update.updates)
|
||||
configUpdates.tryEmitNext(update)
|
||||
}
|
||||
|
||||
/** Specific read-only endpoint for all users **/
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.grimsi.gameyfin.config
|
||||
|
||||
import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
||||
import de.grimsi.gameyfin.config.dto.ConfigValuePairDto
|
||||
import de.grimsi.gameyfin.config.entities.ConfigEntry
|
||||
import de.grimsi.gameyfin.config.persistence.ConfigRepository
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
@@ -116,11 +115,15 @@ class ConfigService(
|
||||
* Set multiple config values at once.
|
||||
* Configs with a null value will be deleted.
|
||||
*
|
||||
* @param configs: A map of key-value pairs to set
|
||||
* @param updates: A map of key-value pairs to set
|
||||
*/
|
||||
fun setAll(configs: List<ConfigValuePairDto>) {
|
||||
configs.forEach {
|
||||
it.value?.let { value -> set(it.key, value) } ?: deleteConfig(it.key)
|
||||
fun update(updates: Map<String, Serializable?>) {
|
||||
updates.forEach { (key, value) ->
|
||||
if (value == null) {
|
||||
delete(key)
|
||||
} else {
|
||||
set(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +144,7 @@ class ConfigService(
|
||||
*
|
||||
* @param key: Key of the config property
|
||||
*/
|
||||
fun deleteConfig(key: String) {
|
||||
fun delete(key: String) {
|
||||
|
||||
log.debug { "Delete config value '$key'" }
|
||||
|
||||
|
||||
+3
-5
@@ -4,9 +4,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import de.grimsi.gameyfin.core.serialization.ArrayDeserializer
|
||||
import java.io.Serializable
|
||||
|
||||
data class ConfigValuePairDto(
|
||||
val key: String,
|
||||
|
||||
@field:JsonDeserialize(using = ArrayDeserializer::class)
|
||||
val value: Serializable?
|
||||
data class ConfigUpdateDto(
|
||||
@get:JsonDeserialize(contentUsing = ArrayDeserializer::class)
|
||||
val updates: Map<String, Serializable?>
|
||||
)
|
||||
Reference in New Issue
Block a user