admin page + beginning of settings page

Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
mrjvs 2023-10-25 18:05:40 +02:00
parent e267482d33
commit de68438793
8 changed files with 257 additions and 7 deletions

View File

@ -0,0 +1,12 @@
import classNames from "classnames";
export function Divider(props: { marginClass?: string }) {
return (
<hr
className={classNames(
"w-full h-px border-0 bg-utils-divider bg-opacity-50",
props.marginClass ?? "my-8"
)}
/>
);
}

79
src/pages/Settings.tsx Normal file
View File

@ -0,0 +1,79 @@
import { Icon, Icons } from "@/components/Icon";
import { WideContainer } from "@/components/layout/WideContainer";
import { Divider } from "@/components/utils/Divider";
import { Heading1 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
import { SubPageLayout } from "./layouts/SubPageLayout";
// TODO Put all of this not here (when I'm done writing them)
function SidebarSection(props: { title: string; children: React.ReactNode }) {
return (
<section>
<p className="text-sm font-bold uppercase text-settings-sidebar-type-secondary mb-2">
{props.title}
</p>
{props.children}
</section>
);
}
function SidebarLink(props: { children: React.ReactNode; icon: Icons }) {
return (
<div className="w-full px-2 py-1 flex items-center space-x-3">
<Icon
className="text-2xl text-settings-sidebar-type-icon"
icon={props.icon}
/>
<span>{props.children}</span>
</div>
);
}
function SettingsSidebar() {
// eslint-disable-next-line no-restricted-globals
const hostname = location.hostname;
return (
<div>
<div className="sticky top-24 text-settings-sidebar-type-inactive">
<SidebarSection title="Settings">
<SidebarLink icon={Icons.WAND}>Account</SidebarLink>
</SidebarSection>
<Divider />
<SidebarSection title="App information">
<div className="flex justify-between items-center space-x-3">
<span>Version</span>
<span>{conf().APP_VERSION}</span>
</div>
<div className="flex justify-between items-center space-x-3">
<span>Domain</span>
<span className="text-right">{hostname}</span>
</div>
</SidebarSection>
</div>
</div>
);
}
function SettingsLayout(props: { children: React.ReactNode }) {
return (
<WideContainer ultraWide>
<div className="grid grid-cols-[260px,1fr] gap-12">
<SettingsSidebar />
{props.children}
</div>
</WideContainer>
);
}
export function SettingsPage() {
return (
<SubPageLayout>
<SettingsLayout>
<Heading1>Setting</Heading1>
</SettingsLayout>
</SubPageLayout>
);
}

View File

@ -1,6 +1,8 @@
import { ThinContainer } from "@/components/layout/ThinContainer";
import { Heading1, Paragraph } from "@/components/utils/Text";
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
import { ConfigValuesPart } from "@/pages/parts/admin/ConfigValuesPart";
import { TMDBTestPart } from "@/pages/parts/admin/TMDBTestPart";
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
export function AdminPage() {
@ -10,7 +12,9 @@ export function AdminPage() {
<Heading1>Admin tools</Heading1>
<Paragraph>Useful tools to test out your current deployment</Paragraph>
<ConfigValuesPart />
<WorkerTestPart />
<TMDBTestPart />
</ThinContainer>
</SubPageLayout>
);

View File

@ -0,0 +1,32 @@
import { ReactNode } from "react";
import { Divider } from "@/components/utils/Divider";
import { Heading2 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
function ConfigValue(props: { name: string; children?: ReactNode }) {
return (
<>
<div className="flex">
<p className="flex-1 font-bold text-white">{props.name}</p>
<p>{props.children}</p>
</div>
<Divider marginClass="my-3" />
</>
);
}
export function ConfigValuesPart() {
const normalRouter = conf().NORMAL_ROUTER;
const appVersion = conf().APP_VERSION;
return (
<>
<Heading2 className="mb-8 mt-12">Configured values</Heading2>
<ConfigValue name="Routing mode">
{normalRouter ? "Normal routing" : "Hash based routing"}
</ConfigValue>
<ConfigValue name="Application version">v{appVersion}</ConfigValue>
</>
);
}

View File

@ -0,0 +1,93 @@
import { useState } from "react";
import { useAsyncFn } from "react-use";
import { getMediaDetails } from "@/backend/metadata/tmdb";
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon";
import { Box } from "@/components/layout/Box";
import { Spinner } from "@/components/layout/Spinner";
import { Heading2 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
export function TMDBTestPart() {
const tmdbApiKey = conf().TMDB_READ_API_KEY;
const [status, setStatus] = useState({
hasTested: false,
success: false,
errorText: "",
});
const [testState, runTests] = useAsyncFn(async () => {
setStatus({
hasTested: false,
success: false,
errorText: "",
});
if (tmdbApiKey.length === 0) {
return setStatus({
hasTested: true,
success: false,
errorText: "TMDB api key is not set",
});
}
const isJWT = tmdbApiKey.split(".").length > 2;
if (!isJWT) {
return setStatus({
hasTested: true,
success: false,
errorText: "TMDB api key is not a read only key",
});
}
try {
await getMediaDetails("556574", TMDBContentTypes.MOVIE);
} catch (err) {
return setStatus({
hasTested: true,
success: false,
errorText:
"Failed to call tmdb, double check api key and your internet connection",
});
}
return setStatus({
hasTested: true,
success: true,
errorText: "",
});
}, [tmdbApiKey, setStatus]);
return (
<>
<Heading2 className="mb-8 mt-12">TMDB tests</Heading2>
<Box>
<div className="flex items-center">
<div className="flex-1">
{!status.hasTested ? (
<p>Run the test to validate TMDB</p>
) : status.success ? (
<p className="flex items-center">
<Icon
icon={Icons.CIRCLE_CHECK}
className="text-video-scraping-success mr-2"
/>
TMDB is working as expected
</p>
) : (
<>
<p className="text-white font-bold">TMDB is not working</p>
<p>{status.errorText}</p>
</>
)}
</div>
<Button theme="purple" onClick={runTests}>
{testState.loading ? <Spinner className="text-base mr-2" /> : null}
Test TMDB
</Button>
</div>
</Box>
</>
);
}

View File

@ -1,13 +1,13 @@
import classNames from "classnames";
import { f } from "ofetch/dist/shared/ofetch.441891d5";
import { useCallback, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { useAsyncFn } from "react-use";
import { mwFetch } from "@/backend/helpers/fetch";
import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon";
import { Box } from "@/components/layout/Box";
import { Divider } from "@/components/player/internals/ContextMenu/Misc";
import { Spinner } from "@/components/layout/Spinner";
import { Divider } from "@/components/utils/Divider";
import { Heading2 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
@ -53,7 +53,7 @@ export function WorkerTestPart() {
{ id: string; status: "error" | "success"; error?: Error }[]
>([]);
const runTests = useAsyncFn(async () => {
const [testState, runTests] = useAsyncFn(async () => {
function updateWorker(id: string, data: (typeof workerState)[number]) {
setWorkerState((s) => {
return [...s.filter((v) => v.id !== id), data];
@ -62,6 +62,14 @@ export function WorkerTestPart() {
setWorkerState([]);
for (const worker of workerList) {
try {
if (worker.url.endsWith("/")) {
updateWorker(worker.id, {
id: worker.id,
status: "error",
error: new Error("URL ends with slash"),
});
continue;
}
await mwFetch(worker.url, {
query: {
destination: "https://postman-echo.com/get",
@ -83,8 +91,8 @@ export function WorkerTestPart() {
return (
<>
<Heading2 className="mb-0 mt-12">Worker tests</Heading2>
<p className="mb-8 mt-2">15 workers registered</p>
<Heading2 className="!mb-0 mt-12">Worker tests</Heading2>
<p className="mb-8 mt-2">{workerList.length} worker(s) registered</p>
<Box>
{workerList.map((v, i) => {
const s = workerState.find((segment) => segment.id);
@ -105,7 +113,10 @@ export function WorkerTestPart() {
})}
<Divider />
<div className="flex justify-end">
<Button theme="purple">Run tests</Button>
<Button theme="purple" onClick={runTests}>
{testState.loading ? <Spinner className="text-base mr-2" /> : null}
Test workers
</Button>
</div>
</Box>
</>

View File

@ -17,6 +17,7 @@ import { DmcaPage } from "@/pages/Dmca";
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
import { HomePage } from "@/pages/HomePage";
import { PlayerView } from "@/pages/PlayerView";
import { SettingsPage } from "@/pages/Settings";
import { Layout } from "@/setup/Layout";
import { BookmarkContextProvider } from "@/state/bookmark";
import { SettingsProvider } from "@/state/settings";
@ -103,6 +104,9 @@ function App() {
<Route exact path="/faq" component={AboutPage} />
<Route exact path="/dmca" component={DmcaPage} />
{/* Settings page */}
<Route exact path="/settings" component={SettingsPage} />
{/* admin routes */}
<Route exact path="/admin" component={AdminPage} />

View File

@ -109,6 +109,21 @@ module.exports = {
badgeText: "#5F5F7A"
},
settings: {
sidebar: {
type: {
secondary: "#4B395F",
inactive: "#8D68A9",
icon: "#926CAD",
activated: "#CBA1E8"
}
}
},
utils: {
divider: "#353549"
},
// Error page
errors: {
card: "#12121B",