mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 08:15:37 +00:00
Implement push-based log view
Various layout fixes
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {LogEndpoint} from "Frontend/generated/endpoints";
|
||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||
import * as Yup from 'yup';
|
||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||
import {toast} from "sonner";
|
||||
import {Button, Code, Divider, Tooltip} from "@nextui-org/react";
|
||||
import {ArrowUDownLeft, SortAscending} from "@phosphor-icons/react";
|
||||
|
||||
function LogManagementLayout({getConfig, formik}: any) {
|
||||
|
||||
const [logEntries, setLogEntries] = useState<string[]>([]);
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const [softWrap, setSoftWrap] = useState(false);
|
||||
const subscribed = useRef(false);
|
||||
const logEndRef = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (subscribed.current) return;
|
||||
LogEndpoint.getApplicationLogs().onNext((newEntry: string | undefined) =>
|
||||
setLogEntries((currentEntries) => [...currentEntries, newEntry as string])
|
||||
);
|
||||
subscribed.current = true;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (formik.isSubmitting == false && formik.submitCount > 0) {
|
||||
LogEndpoint.reloadLogConfig()
|
||||
.catch(() => toast.error("Failed to apply log configuration"));
|
||||
}
|
||||
}, [formik.isSubmitting]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoScroll) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [logEntries, autoScroll, softWrap]);
|
||||
|
||||
function scrollToBottom() {
|
||||
logEndRef.current?.scrollIntoView();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row gap-4">
|
||||
<ConfigFormField configElement={getConfig("logs.folder")}/>
|
||||
<ConfigFormField configElement={getConfig("logs.max-history-days")}/>
|
||||
<ConfigFormField configElement={getConfig("logs.level")}/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row flex-grow justify-between items-baseline">
|
||||
<h2 className={"text-xl font-bold mt-8 mb-1"}>Application logs</h2>
|
||||
<div className="flex flex-row gap-1">
|
||||
<Tooltip content="Soft-wrap" placement="bottom">
|
||||
<Button isIconOnly
|
||||
onPress={() => setSoftWrap(!softWrap)}
|
||||
variant={softWrap ? "solid" : "ghost"}
|
||||
>
|
||||
<ArrowUDownLeft/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Auto-scroll" placement="bottom">
|
||||
<Button isIconOnly
|
||||
onPress={() => setAutoScroll(!autoScroll)}
|
||||
variant={autoScroll ? "solid" : "ghost"}
|
||||
>
|
||||
<SortAscending/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className="mb-4"/>
|
||||
</div>
|
||||
<Code size="sm" radius="none"
|
||||
className={`flex flex-col h-[50vh] max-h-[50vh] text-sm overflow-auto ${softWrap ? "whitespace-normal" : "whitespace-nowrap"}`}>
|
||||
{logEntries.map((entry, index) => <p key={index}>{entry}</p>)}
|
||||
<div ref={logEndRef}/>
|
||||
</Code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
logs: Yup.object({
|
||||
folder: Yup.string().required("Required"),
|
||||
"max-history-days": Yup.number().required("Required"),
|
||||
level: Yup.string().required("Required")
|
||||
})
|
||||
});
|
||||
|
||||
export const LogManagement = withConfigPage(LogManagementLayout, "Logging", "logs", validationSchema);
|
||||
@@ -16,11 +16,13 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<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>();
|
||||
|
||||
useEffect(() => {
|
||||
ConfigEndpoint.getAll(configPrefix).then((response: any) => {
|
||||
setConfigDtos(response as ConfigEntryDto[]);
|
||||
setNestedConfigDtos(toNestedConfig(response as ConfigEntryDto[]));
|
||||
isInitialized.current = true;
|
||||
});
|
||||
}, []);
|
||||
@@ -34,6 +36,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
async function handleSubmit(values: NestedConfig) {
|
||||
const configValues = toConfigValuePair(values);
|
||||
await ConfigEndpoint.setAll(configValues);
|
||||
setNestedConfigDtos(values);
|
||||
setConfigSaved(true);
|
||||
}
|
||||
|
||||
@@ -114,11 +117,12 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={toNestedConfig(configDtos)}
|
||||
initialValues={nestedConfigDtos}
|
||||
onSubmit={handleSubmit}
|
||||
validationSchema={validationSchema}
|
||||
enableReinitialize={true}
|
||||
>
|
||||
{(formik: { values: any; isSubmitting: any; }) => (
|
||||
{(formik) => (
|
||||
<Form>
|
||||
<div className="flex flex-row flex-grow justify-between mb-8">
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
@@ -131,7 +135,7 @@ export default function withConfigPage(WrappedComponent: React.ComponentType<any
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={formik.isSubmitting || configSaved}
|
||||
disabled={formik.isSubmitting || configSaved || !formik.dirty}
|
||||
type="submit"
|
||||
>
|
||||
{formik.isSubmitting ? "" : configSaved ? <Check/> : "Save"}
|
||||
|
||||
@@ -7,7 +7,7 @@ const CheckboxInput = ({label, ...props}) => {
|
||||
const [field] = useField(props);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-grow items-center gap-2 my-2">
|
||||
<div className="flex flex-row flex-1 items-center gap-2 my-2">
|
||||
<Checkbox
|
||||
{...field}
|
||||
{...props}
|
||||
|
||||
@@ -9,7 +9,7 @@ const Input = ({label, ...props}) => {
|
||||
const [field, meta] = useField(props);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full items-start gap-2 my-2">
|
||||
<div className="flex flex-col flex-1 items-start gap-2 my-2">
|
||||
<NextUiInput
|
||||
{...props}
|
||||
{...field}
|
||||
|
||||
@@ -7,17 +7,18 @@ const SelectInput = ({label, values, ...props}) => {
|
||||
const [field] = useField(props);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-1 items-center gap-2 my-2">
|
||||
<div className="flex flex-row flex-1 justify-center gap-2 my-2">
|
||||
<Select
|
||||
{...field}
|
||||
{...props}
|
||||
id={field.name}
|
||||
label={label}
|
||||
defaultSelectedKeys={[field.value]}
|
||||
disallowEmptySelection
|
||||
>
|
||||
{values.map((value: string) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{value.toLowerCase()}
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function withSideMenu(menuItems: MenuItem[]) {
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Outlet/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user