aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/src/pages/System
diff options
context:
space:
mode:
authorLiang Yi <[email protected]>2022-01-22 21:35:11 +0800
committerGitHub <[email protected]>2022-01-22 21:35:11 +0800
commitd8d2300980ca69a4ae6511cb49a6dc548c0da793 (patch)
tree23f2f136c495b4064f43a0c4148391c46b9fa997 /frontend/src/pages/System
parent6b82a734e2bc597b219472774c0ec58038630c65 (diff)
downloadbazarr-d8d2300980ca69a4ae6511cb49a6dc548c0da793.tar.gz
bazarr-d8d2300980ca69a4ae6511cb49a6dc548c0da793.zip
Add React-Query to improve network and cache performancev1.0.3-beta.15
Diffstat (limited to 'frontend/src/pages/System')
-rw-r--r--frontend/src/pages/System/Logs/index.tsx55
-rw-r--r--frontend/src/pages/System/Logs/modal.tsx26
-rw-r--r--frontend/src/pages/System/Logs/table.tsx87
-rw-r--r--frontend/src/pages/System/Providers/index.tsx48
-rw-r--r--frontend/src/pages/System/Providers/table.tsx31
-rw-r--r--frontend/src/pages/System/Releases/index.tsx78
-rw-r--r--frontend/src/pages/System/Status/index.tsx164
-rw-r--r--frontend/src/pages/System/Status/style.scss3
-rw-r--r--frontend/src/pages/System/Status/table.tsx35
-rw-r--r--frontend/src/pages/System/Tasks/index.tsx39
-rw-r--r--frontend/src/pages/System/Tasks/table.tsx62
11 files changed, 628 insertions, 0 deletions
diff --git a/frontend/src/pages/System/Logs/index.tsx b/frontend/src/pages/System/Logs/index.tsx
new file mode 100644
index 000000000..2f835b44b
--- /dev/null
+++ b/frontend/src/pages/System/Logs/index.tsx
@@ -0,0 +1,55 @@
+import { faDownload, faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
+import { useDeleteLogs, useSystemLogs } from "apis/hooks";
+import { ContentHeader, QueryOverlay } from "components";
+import React, { FunctionComponent, useCallback } from "react";
+import { Container, Row } from "react-bootstrap";
+import { Helmet } from "react-helmet";
+import { Environment } from "utilities";
+import Table from "./table";
+
+interface Props {}
+
+const SystemLogsView: FunctionComponent<Props> = () => {
+ const logs = useSystemLogs();
+ const { isFetching, data, refetch } = logs;
+
+ const { mutate, isLoading } = useDeleteLogs();
+
+ const download = useCallback(() => {
+ window.open(`${Environment.baseUrl}/bazarr.log`);
+ }, []);
+
+ return (
+ <QueryOverlay result={logs}>
+ <Container fluid>
+ <Helmet>
+ <title>Logs - Bazarr (System)</title>
+ </Helmet>
+ <ContentHeader>
+ <ContentHeader.Button
+ updating={isFetching}
+ icon={faSync}
+ onClick={() => refetch()}
+ >
+ Refresh
+ </ContentHeader.Button>
+ <ContentHeader.Button icon={faDownload} onClick={download}>
+ Download
+ </ContentHeader.Button>
+ <ContentHeader.Button
+ updating={isLoading}
+ icon={faTrash}
+ onClick={() => mutate()}
+ >
+ Empty
+ </ContentHeader.Button>
+ </ContentHeader>
+ <Row>
+ <Table logs={data ?? []}></Table>
+ </Row>
+ </Container>
+ </QueryOverlay>
+ );
+};
+
+export default SystemLogsView;
diff --git a/frontend/src/pages/System/Logs/modal.tsx b/frontend/src/pages/System/Logs/modal.tsx
new file mode 100644
index 000000000..c06241cad
--- /dev/null
+++ b/frontend/src/pages/System/Logs/modal.tsx
@@ -0,0 +1,26 @@
+import { BaseModal, BaseModalProps, useModalPayload } from "components";
+import React, { FunctionComponent, useMemo } from "react";
+
+interface Props extends BaseModalProps {}
+
+const SystemLogModal: FunctionComponent<Props> = ({ ...modal }) => {
+ const stack = useModalPayload<string>(modal.modalKey);
+ const result = useMemo(
+ () =>
+ stack?.split("\\n").map((v, idx) => (
+ <p key={idx} className="text-nowrap my-1">
+ {v}
+ </p>
+ )),
+ [stack]
+ );
+ return (
+ <BaseModal title="Stack traceback" {...modal}>
+ <pre>
+ <code className="zmdi-language-python-alt">{result}</code>
+ </pre>
+ </BaseModal>
+ );
+};
+
+export default SystemLogModal;
diff --git a/frontend/src/pages/System/Logs/table.tsx b/frontend/src/pages/System/Logs/table.tsx
new file mode 100644
index 000000000..b8eace161
--- /dev/null
+++ b/frontend/src/pages/System/Logs/table.tsx
@@ -0,0 +1,87 @@
+import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import {
+ faBug,
+ faCode,
+ faExclamationCircle,
+ faInfoCircle,
+ faLayerGroup,
+ faQuestion,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { ActionButton, PageTable, useShowModal } from "components";
+import { isUndefined } from "lodash";
+import React, { FunctionComponent, useCallback, useMemo } from "react";
+import { Column, Row } from "react-table";
+import SystemLogModal from "./modal";
+
+interface Props {
+ logs: readonly System.Log[];
+}
+
+function mapTypeToIcon(type: System.LogType): IconDefinition {
+ switch (type) {
+ case "DEBUG":
+ return faCode;
+ case "ERROR":
+ return faBug;
+ case "INFO":
+ return faInfoCircle;
+ case "WARNING":
+ return faExclamationCircle;
+ default:
+ return faQuestion;
+ }
+}
+
+const Table: FunctionComponent<Props> = ({ logs }) => {
+ const showModal = useShowModal();
+ const show = useCallback(
+ (row: Row<System.Log>, text: string) =>
+ showModal<string>("system-log", text),
+ [showModal]
+ );
+ const columns: Column<System.Log>[] = useMemo<Column<System.Log>[]>(
+ () => [
+ {
+ accessor: "type",
+ Cell: (row) => (
+ <FontAwesomeIcon icon={mapTypeToIcon(row.value)}></FontAwesomeIcon>
+ ),
+ },
+ {
+ Header: "Message",
+ accessor: "message",
+ },
+ {
+ Header: "Date",
+ accessor: "timestamp",
+ className: "text-nowrap",
+ },
+ {
+ accessor: "exception",
+ Cell: ({ row, value, update }) => {
+ if (!isUndefined(value)) {
+ return (
+ <ActionButton
+ icon={faLayerGroup}
+ onClick={() => update && update(row, value)}
+ ></ActionButton>
+ );
+ } else {
+ return null;
+ }
+ },
+ },
+ ],
+ []
+ );
+
+ return (
+ <React.Fragment>
+ <PageTable columns={columns} data={logs} update={show}></PageTable>
+ <SystemLogModal size="xl" modalKey="system-log"></SystemLogModal>
+ </React.Fragment>
+ );
+};
+
+export default Table;
diff --git a/frontend/src/pages/System/Providers/index.tsx b/frontend/src/pages/System/Providers/index.tsx
new file mode 100644
index 000000000..f6b3b14fc
--- /dev/null
+++ b/frontend/src/pages/System/Providers/index.tsx
@@ -0,0 +1,48 @@
+import { faSync, faTrash } from "@fortawesome/free-solid-svg-icons";
+import { useResetProvider, useSystemProviders } from "apis/hooks";
+import { ContentHeader, QueryOverlay } from "components";
+import React, { FunctionComponent } from "react";
+import { Container, Row } from "react-bootstrap";
+import { Helmet } from "react-helmet";
+import Table from "./table";
+
+interface Props {}
+
+const SystemProvidersView: FunctionComponent<Props> = () => {
+ const providers = useSystemProviders();
+
+ const { isFetching, data, refetch } = providers;
+
+ const { mutate: reset, isLoading: isResetting } = useResetProvider();
+
+ return (
+ <QueryOverlay result={providers}>
+ <Container fluid>
+ <Helmet>
+ <title>Providers - Bazarr (System)</title>
+ </Helmet>
+ <ContentHeader>
+ <ContentHeader.Button
+ updating={isFetching}
+ icon={faSync}
+ onClick={() => refetch()}
+ >
+ Refresh
+ </ContentHeader.Button>
+ <ContentHeader.Button
+ icon={faTrash}
+ updating={isResetting}
+ onClick={() => reset()}
+ >
+ Reset
+ </ContentHeader.Button>
+ </ContentHeader>
+ <Row>
+ <Table providers={data ?? []}></Table>
+ </Row>
+ </Container>
+ </QueryOverlay>
+ );
+};
+
+export default SystemProvidersView;
diff --git a/frontend/src/pages/System/Providers/table.tsx b/frontend/src/pages/System/Providers/table.tsx
new file mode 100644
index 000000000..bf2168a5b
--- /dev/null
+++ b/frontend/src/pages/System/Providers/table.tsx
@@ -0,0 +1,31 @@
+import { SimpleTable } from "components";
+import React, { FunctionComponent, useMemo } from "react";
+import { Column } from "react-table";
+
+interface Props {
+ providers: readonly System.Provider[];
+}
+
+const Table: FunctionComponent<Props> = (props) => {
+ const columns: Column<System.Provider>[] = useMemo<Column<System.Provider>[]>(
+ () => [
+ {
+ Header: "Name",
+ accessor: "name",
+ },
+ {
+ Header: "Status",
+ accessor: "status",
+ },
+ {
+ Header: "Next Retry",
+ accessor: "retry",
+ },
+ ],
+ []
+ );
+
+ return <SimpleTable columns={columns} data={props.providers}></SimpleTable>;
+};
+
+export default Table;
diff --git a/frontend/src/pages/System/Releases/index.tsx b/frontend/src/pages/System/Releases/index.tsx
new file mode 100644
index 000000000..8863aed6a
--- /dev/null
+++ b/frontend/src/pages/System/Releases/index.tsx
@@ -0,0 +1,78 @@
+import { useSystemReleases } from "apis/hooks";
+import { QueryOverlay } from "components";
+import React, { FunctionComponent, useMemo } from "react";
+import { Badge, Card, Col, Container, Row } from "react-bootstrap";
+import { Helmet } from "react-helmet";
+import { BuildKey } from "utilities";
+
+interface Props {}
+
+const SystemReleasesView: FunctionComponent<Props> = () => {
+ const releases = useSystemReleases();
+ const { data } = releases;
+
+ return (
+ <Container fluid className="px-3 py-4 bg-light">
+ <Helmet>
+ <title>Releases - Bazarr (System)</title>
+ </Helmet>
+ <Row>
+ <QueryOverlay result={releases}>
+ <React.Fragment>
+ {data?.map((v, idx) => (
+ <Col xs={12} key={BuildKey(idx, v.date)}>
+ <InfoElement {...v}></InfoElement>
+ </Col>
+ ))}
+ </React.Fragment>
+ </QueryOverlay>
+ </Row>
+ </Container>
+ );
+};
+
+const headerBadgeCls = "mr-2";
+
+const InfoElement: FunctionComponent<ReleaseInfo> = ({
+ name,
+ body,
+ date,
+ prerelease,
+ current,
+}) => {
+ const infos = useMemo(
+ () => body.map((v) => v.replace(/(\s\[.*?\])\(.*?\)/, "")),
+ [body]
+ );
+ return (
+ <Card className="mb-4 mx-3 d-flex flex-grow-1">
+ <Card.Header>
+ <span className={headerBadgeCls}>{name}</span>
+ <Badge className={headerBadgeCls} variant="info">
+ {date}
+ </Badge>
+ <Badge
+ className={headerBadgeCls}
+ variant={prerelease ? "danger" : "success"}
+ >
+ {prerelease ? "Development" : "Master"}
+ </Badge>
+ <Badge className={headerBadgeCls} hidden={!current} variant="primary">
+ Installed
+ </Badge>
+ </Card.Header>
+ <Card.Body>
+ <Card.Text>
+ From newest to oldest:
+ <div className="mx-4">
+ {infos.map((v, idx) => (
+ <li key={idx}>{v}</li>
+ ))}
+ </div>
+ </Card.Text>
+ </Card.Body>
+ </Card>
+ );
+};
+
+export default SystemReleasesView;
diff --git a/frontend/src/pages/System/Status/index.tsx b/frontend/src/pages/System/Status/index.tsx
new file mode 100644
index 000000000..c5c05b304
--- /dev/null
+++ b/frontend/src/pages/System/Status/index.tsx
@@ -0,0 +1,164 @@
+import { IconDefinition } from "@fortawesome/fontawesome-common-types";
+import {
+ faDiscord,
+ faGithub,
+ faWikipediaW,
+} from "@fortawesome/free-brands-svg-icons";
+import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useSystemHealth, useSystemStatus } from "apis/hooks";
+import { QueryOverlay } from "components";
+import moment from "moment";
+import React, { FunctionComponent, useState } from "react";
+import { Col, Container, Row } from "react-bootstrap";
+import { Helmet } from "react-helmet";
+import { useIntervalWhen } from "rooks";
+import { GithubRepoRoot } from "utilities/constants";
+import "./style.scss";
+import Table from "./table";
+
+interface InfoProps {
+ title: string;
+ children: React.ReactNode;
+}
+
+function CRow(props: InfoProps): JSX.Element {
+ const { title, children } = props;
+ return (
+ <Row>
+ <Col sm={4}>
+ <b>{title}</b>
+ </Col>
+ <Col>{children}</Col>
+ </Row>
+ );
+}
+
+interface IconProps {
+ icon: IconDefinition;
+ link: string;
+ children: string;
+}
+
+function Label(props: IconProps): JSX.Element {
+ const { icon, link, children } = props;
+ return (
+ <React.Fragment>
+ <FontAwesomeIcon icon={icon} style={{ width: "2rem" }}></FontAwesomeIcon>
+ <a href={link} target="_blank" rel="noopener noreferrer">
+ {children}
+ </a>
+ </React.Fragment>
+ );
+}
+
+const InfoContainer: FunctionComponent<{ title: string }> = ({
+ title,
+ children,
+}) => {
+ return (
+ <Container className="py-3">
+ <h4>{title}</h4>
+ <hr></hr>
+ {children}
+ </Container>
+ );
+};
+
+interface Props {}
+
+const SystemStatusView: FunctionComponent<Props> = () => {
+ const health = useSystemHealth();
+ const { data: status } = useSystemStatus();
+
+ const [uptime, setState] = useState<string>();
+ const [intervalWhenState] = useState(true);
+
+ useIntervalWhen(
+ () => {
+ if (status) {
+ let duration = moment.duration(
+ moment().utc().unix() - status.start_time,
+ "seconds"
+ ),
+ days = duration.days(),
+ hours = duration.hours().toString().padStart(2, "0"),
+ minutes = duration.minutes().toString().padStart(2, "0"),
+ seconds = duration.seconds().toString().padStart(2, "0");
+ setState(days + "d " + hours + ":" + minutes + ":" + seconds);
+ }
+ },
+ 1000,
+ intervalWhenState,
+ true
+ );
+
+ return (
+ <Container className="p-5">
+ <Helmet>
+ <title>Status - Bazarr (System)</title>
+ </Helmet>
+ <Row>
+ <InfoContainer title="Health">
+ <QueryOverlay result={health}>
+ <Table health={health.data ?? []}></Table>
+ </QueryOverlay>
+ </InfoContainer>
+ </Row>
+ <Row>
+ <InfoContainer title="About">
+ <CRow title="Bazarr Version">
+ <span>{status?.bazarr_version}</span>
+ </CRow>
+ <CRow title="Sonarr Version">
+ <span>{status?.sonarr_version}</span>
+ </CRow>
+ <CRow title="Radarr Version">
+ <span>{status?.radarr_version}</span>
+ </CRow>
+ <CRow title="Operating System">
+ <span>{status?.operating_system}</span>
+ </CRow>
+ <CRow title="Python Version">
+ <span>{status?.python_version}</span>
+ </CRow>
+ <CRow title="Bazarr Directory">
+ <span>{status?.bazarr_directory}</span>
+ </CRow>
+ <CRow title="Bazarr Config Directory">
+ <span>{status?.bazarr_config_directory}</span>
+ </CRow>
+ <CRow title="Uptime">
+ <span>{uptime}</span>
+ </CRow>
+ </InfoContainer>
+ </Row>
+ <Row>
+ <InfoContainer title="More Info">
+ <CRow title="Home Page">
+ <Label icon={faPaperPlane} link="https://www.bazarr.media/">
+ Bazarr Website
+ </Label>
+ </CRow>
+ <CRow title="Source">
+ <Label icon={faGithub} link={GithubRepoRoot}>
+ Bazarr on Github
+ </Label>
+ </CRow>
+ <CRow title="Wiki">
+ <Label icon={faWikipediaW} link="https://wiki.bazarr.media">
+ Bazarr Wiki
+ </Label>
+ </CRow>
+ <CRow title="Discord">
+ <Label icon={faDiscord} link="https://discord.gg/MH2e2eb">
+ Bazarr on Discord
+ </Label>
+ </CRow>
+ </InfoContainer>
+ </Row>
+ </Container>
+ );
+};
+
+export default SystemStatusView;
diff --git a/frontend/src/pages/System/Status/style.scss b/frontend/src/pages/System/Status/style.scss
new file mode 100644
index 000000000..b3f9bbe50
--- /dev/null
+++ b/frontend/src/pages/System/Status/style.scss
@@ -0,0 +1,3 @@
+.status-issue {
+ min-width: 16rem;
+}
diff --git a/frontend/src/pages/System/Status/table.tsx b/frontend/src/pages/System/Status/table.tsx
new file mode 100644
index 000000000..3e499f5f4
--- /dev/null
+++ b/frontend/src/pages/System/Status/table.tsx
@@ -0,0 +1,35 @@
+import { SimpleTable } from "components";
+import React, { FunctionComponent, useMemo } from "react";
+import { Column } from "react-table";
+
+interface Props {
+ health: readonly System.Health[];
+}
+
+const Table: FunctionComponent<Props> = ({ health }) => {
+ const columns: Column<System.Health>[] = useMemo<Column<System.Health>[]>(
+ () => [
+ {
+ Header: "Object",
+ accessor: "object",
+ },
+ {
+ Header: "Issue",
+ accessor: "issue",
+ className: "status-issue",
+ },
+ ],
+ []
+ );
+
+ return (
+ <SimpleTable
+ responsive
+ columns={columns}
+ data={health}
+ emptyText="No issues with your configuration"
+ ></SimpleTable>
+ );
+};
+
+export default Table;
diff --git a/frontend/src/pages/System/Tasks/index.tsx b/frontend/src/pages/System/Tasks/index.tsx
new file mode 100644
index 000000000..1bb3430f4
--- /dev/null
+++ b/frontend/src/pages/System/Tasks/index.tsx
@@ -0,0 +1,39 @@
+import { faSync } from "@fortawesome/free-solid-svg-icons";
+import { useSystemTasks } from "apis/hooks";
+import { ContentHeader, QueryOverlay } from "components";
+import React, { FunctionComponent } from "react";
+import { Container, Row } from "react-bootstrap";
+import { Helmet } from "react-helmet";
+import Table from "./table";
+
+interface Props {}
+
+const SystemTasksView: FunctionComponent<Props> = () => {
+ const tasks = useSystemTasks();
+
+ const { isFetching, data, refetch } = tasks;
+
+ return (
+ <QueryOverlay result={tasks}>
+ <Container fluid>
+ <Helmet>
+ <title>Tasks - Bazarr (System)</title>
+ </Helmet>
+ <ContentHeader>
+ <ContentHeader.Button
+ updating={isFetching}
+ icon={faSync}
+ onClick={() => refetch()}
+ >
+ Refresh
+ </ContentHeader.Button>
+ </ContentHeader>
+ <Row>
+ <Table tasks={data ?? []}></Table>
+ </Row>
+ </Container>
+ </QueryOverlay>
+ );
+};
+
+export default SystemTasksView;
diff --git a/frontend/src/pages/System/Tasks/table.tsx b/frontend/src/pages/System/Tasks/table.tsx
new file mode 100644
index 000000000..e7a9bc42e
--- /dev/null
+++ b/frontend/src/pages/System/Tasks/table.tsx
@@ -0,0 +1,62 @@
+import { faSync } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useRunTask } from "apis/hooks";
+import { AsyncButton, SimpleTable } from "components";
+import React, { FunctionComponent, useMemo } from "react";
+import { Column, useSortBy } from "react-table";
+
+interface Props {
+ tasks: readonly System.Task[];
+}
+
+const Table: FunctionComponent<Props> = ({ tasks }) => {
+ const columns: Column<System.Task>[] = useMemo<Column<System.Task>[]>(
+ () => [
+ {
+ Header: "Name",
+ accessor: "name",
+ className: "text-nowrap",
+ },
+ {
+ Header: "Interval",
+ accessor: "interval",
+ className: "text-nowrap",
+ },
+ {
+ Header: "Next Execution",
+ accessor: "next_run_in",
+ className: "text-nowrap",
+ },
+ {
+ accessor: "job_running",
+ Cell: (row) => {
+ const { job_id } = row.row.original;
+ const { mutateAsync } = useRunTask();
+ return (
+ <AsyncButton
+ promise={() => mutateAsync(job_id)}
+ variant="light"
+ size="sm"
+ disabled={row.value}
+ animation={false}
+ >
+ <FontAwesomeIcon icon={faSync} spin={row.value}></FontAwesomeIcon>
+ </AsyncButton>
+ );
+ },
+ },
+ ],
+ []
+ );
+
+ return (
+ <SimpleTable
+ initialState={{ sortBy: [{ id: "name", desc: false }] }}
+ columns={columns}
+ data={tasks}
+ plugins={[useSortBy]}
+ ></SimpleTable>
+ );
+};
+
+export default Table;