mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 08:15:37 +00:00
Optimized config management in FE and BE
Implemented standardized Avatar component Added optional "Save message" to config pages
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {GearFine, Question, SignOut, User} from "@phosphor-icons/react";
|
||||
import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@nextui-org/react";
|
||||
import {Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@nextui-org/react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {ConfigController} from "Frontend/generated/endpoints";
|
||||
import {ConfigEndpoint} from "Frontend/generated/endpoints";
|
||||
import Avatar from "Frontend/components/general/Avatar";
|
||||
|
||||
export default function ProfileMenu() {
|
||||
const auth = useAuth();
|
||||
@@ -10,7 +11,7 @@ export default function ProfileMenu() {
|
||||
|
||||
async function logout() {
|
||||
if (auth.state.user?.managedBySso) {
|
||||
window.location.href = await ConfigController.getLogoutUrl() || "/";
|
||||
window.location.href = await ConfigEndpoint.getLogoutUrl() || "/";
|
||||
} else {
|
||||
await auth.logout();
|
||||
}
|
||||
@@ -44,16 +45,17 @@ export default function ProfileMenu() {
|
||||
return (
|
||||
<Dropdown placement="bottom-end">
|
||||
<DropdownTrigger>
|
||||
<Avatar showFallback
|
||||
src={`/images/avatar?username=${auth.state.user?.username}`}
|
||||
radius="full"
|
||||
as="button"
|
||||
className="transition-transform size-8"
|
||||
classNames={{
|
||||
base: "gradient-primary",
|
||||
icon: "text-background/80"
|
||||
}}
|
||||
/>
|
||||
{/* div is necessary so dropdown menu will appear in the correct place */}
|
||||
<div>
|
||||
<Avatar radius="full"
|
||||
as="button"
|
||||
className="transition-transform size-8"
|
||||
classNames={{
|
||||
base: "gradient-primary",
|
||||
icon: "text-background/80"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
{/* @ts-ignore */}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Section from "Frontend/components/general/Section";
|
||||
import Input from "Frontend/components/general/Input";
|
||||
import {Avatar, Button, Input as NextUiInput, Tooltip} from "@nextui-org/react";
|
||||
import {Button, Input as NextUiInput, Tooltip} from "@nextui-org/react";
|
||||
import {Form, Formik} from "formik";
|
||||
import {Check, Info, Trash} from "@phosphor-icons/react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
@@ -11,6 +11,7 @@ import {UserEndpoint} from "Frontend/generated/endpoints";
|
||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
||||
import {toast} from "sonner";
|
||||
import {removeAvatar, uploadAvatar} from "Frontend/endpoints/AvatarEndpoint";
|
||||
import Avatar from "Frontend/components/general/Avatar";
|
||||
|
||||
export default function ProfileManagement() {
|
||||
const [configSaved, setConfigSaved] = useState(false);
|
||||
@@ -101,10 +102,7 @@ export default function ProfileManagement() {
|
||||
<div className="flex flex-row flex-1 justify-between gap-16">
|
||||
<div className="flex flex-col basis-1/4 mt-8 gap-4">
|
||||
<div className="flex flex-row justify-center">
|
||||
<Avatar showFallback
|
||||
src={`/images/avatar?username=${auth.state.user?.username}`}
|
||||
className="size-40 m-4 flex flex-row">
|
||||
</Avatar>
|
||||
<Avatar className="size-40 m-4 flex flex-row"/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<NextUiInput type="file" accept="image/*" onChange={onFileSelected}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, {useEffect} from "react";
|
||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
import * as Yup from 'yup';
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
@@ -7,7 +7,15 @@ import {Button} from "@nextui-org/react";
|
||||
import {MagicWand} from "@phosphor-icons/react";
|
||||
import {toast} from "sonner";
|
||||
|
||||
function SsoMangementLayout({getConfig, formik}: any) {
|
||||
function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.dirty) {
|
||||
setSaveMessage("Gameyfin must be restarted for the changes to take effect");
|
||||
} else {
|
||||
setSaveMessage(null);
|
||||
}
|
||||
}, [formik.dirty]);
|
||||
|
||||
function isAutoPopulateDisabled() {
|
||||
return !formik.values.sso.oidc.enabled || !formik.values.sso.oidc["issuer-url"];
|
||||
@@ -109,4 +117,4 @@ const validationSchema = Yup.object({
|
||||
})
|
||||
});
|
||||
|
||||
export const SsoManagement = withConfigPage(SsoMangementLayout, "Single Sign-On", "sso", validationSchema);
|
||||
export const SsoManagement = withConfigPage(SsoManagementLayout, "Single Sign-On", "sso", validationSchema);
|
||||
@@ -2,7 +2,7 @@ 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 {ConfigController, UserEndpoint} from "Frontend/generated/endpoints";
|
||||
import {ConfigEndpoint, UserEndpoint} from "Frontend/generated/endpoints";
|
||||
import UserInfoDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserInfoDto";
|
||||
import {UserManagementCard} from "Frontend/components/general/UserManagementCard";
|
||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
||||
@@ -17,7 +17,7 @@ function UserManagementLayout({getConfig, formik}: any) {
|
||||
(response) => setUsers(response as UserInfoDto[])
|
||||
);
|
||||
|
||||
ConfigController.getConfig("sso.oidc.auto-register-new-users").then(
|
||||
ConfigEndpoint.get("sso.oidc.auto-register-new-users").then(
|
||||
(response) => setAutoRegisterNewUsers(response === "true")
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {ConfigController} from "Frontend/generated/endpoints";
|
||||
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 "@nextui-org/react";
|
||||
import {Check} from "@phosphor-icons/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";
|
||||
|
||||
type NestedConfig = {
|
||||
[field: string]: any;
|
||||
}
|
||||
|
||||
type ConfigValuePair = {
|
||||
key: string;
|
||||
value: string | number | boolean | null | undefined;
|
||||
}
|
||||
|
||||
export default function withConfigPage(WrappedComponent: React.ComponentType<any>, title: String, configPrefix: string, validationSchema?: any) {
|
||||
return function ConfigPage(props: any) {
|
||||
const isInitialized = useRef(false);
|
||||
const [configSaved, setConfigSaved] = useState(false);
|
||||
const [configDtos, setConfigDtos] = useState<ConfigEntryDto[]>([]);
|
||||
const [saveMessage, setSaveMessage] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
ConfigController.getConfigs(configPrefix).then((response: any) => {
|
||||
ConfigEndpoint.getAll(configPrefix).then((response: any) => {
|
||||
setConfigDtos(response as ConfigEntryDto[]);
|
||||
isInitialized.current = true;
|
||||
});
|
||||
@@ -35,15 +33,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
|
||||
async function handleSubmit(values: NestedConfig) {
|
||||
const configValues = toConfigValuePair(values);
|
||||
await Promise.all(configValues.map(async (c: ConfigValuePair) => {
|
||||
if (c.value === null || c.value === undefined) {
|
||||
await ConfigController.deleteConfig(c.key);
|
||||
return;
|
||||
}
|
||||
|
||||
await ConfigController.setConfig(c.key, c.value.toString());
|
||||
}));
|
||||
|
||||
await ConfigEndpoint.setAll(configValues);
|
||||
setConfigSaved(true);
|
||||
}
|
||||
|
||||
@@ -90,8 +80,8 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
return nestedConfig;
|
||||
}
|
||||
|
||||
function toConfigValuePair(obj: NestedConfig, parentKey: string = ''): ConfigValuePair[] {
|
||||
let result: ConfigValuePair[] = [];
|
||||
function toConfigValuePair(obj: NestedConfig, parentKey: string = ''): ConfigValuePairDto[] {
|
||||
let result: ConfigValuePairDto[] = [];
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
@@ -133,17 +123,24 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting || configSaved}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : configSaved ? <Check/> : "Save"}
|
||||
</Button>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{saveMessage && <SmallInfoField icon={Info}
|
||||
message={saveMessage}
|
||||
className="text-warning"/>}
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting || configSaved}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : configSaved ? <Check/> : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WrappedComponent {...props} getConfig={getConfig} formik={formik}/>
|
||||
<WrappedComponent {...props} getConfig={getConfig} formik={formik}
|
||||
setSaveMessage={setSaveMessage}/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {useAuth} from "Frontend/util/auth";
|
||||
import {Avatar as NextUiAvatar} from "@nextui-org/react";
|
||||
|
||||
// @ts-ignore
|
||||
const Avatar = ({...props}) => {
|
||||
const auth = useAuth();
|
||||
const username = getUsername();
|
||||
|
||||
function getUsername() {
|
||||
if (props.username === undefined || props.username === null || props.username == "") {
|
||||
return auth.state.user?.username;
|
||||
}
|
||||
|
||||
return props.username;
|
||||
}
|
||||
|
||||
// TODO: Check if avatar can be loaded from SSO
|
||||
return (
|
||||
<NextUiAvatar
|
||||
showFallback
|
||||
src={`/images/avatar?username=${username}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Avatar;
|
||||
@@ -1,6 +1,5 @@
|
||||
import UserInfoDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserInfoDto";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
@@ -23,6 +22,7 @@ import {useAuth} from "Frontend/util/auth";
|
||||
import {useEffect, useState} from "react";
|
||||
import {UserEndpoint} from "Frontend/generated/endpoints";
|
||||
import {AvatarEndpoint} from "Frontend/endpoints/endpoints";
|
||||
import Avatar from "Frontend/components/general/Avatar";
|
||||
|
||||
export function UserManagementCard({user}: { user: UserInfoDto }) {
|
||||
const {isOpen, onOpen, onOpenChange} = useDisclosure();
|
||||
@@ -62,14 +62,13 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {
|
||||
return (
|
||||
<Card className="flex flex-row justify-between p-2">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<Avatar showFallback
|
||||
<Avatar username={user.username}
|
||||
name={user.username?.charAt(0)}
|
||||
src={`/images/avatar?username=${user?.username}`}
|
||||
classNames={{
|
||||
base: "gradient-primary size-20",
|
||||
icon: "text-background/80",
|
||||
name: "text-background/80 text-5xl -mt-1",
|
||||
}}></Avatar>
|
||||
name: "text-background/80 text-5xl",
|
||||
}}/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="font-semibold">{user.username}</p>
|
||||
<p className="text-sm">{user.email}</p>
|
||||
|
||||
Reference in New Issue
Block a user