diff options
author | Liang Yi <[email protected]> | 2022-01-22 21:35:11 +0800 |
---|---|---|
committer | GitHub <[email protected]> | 2022-01-22 21:35:11 +0800 |
commit | d8d2300980ca69a4ae6511cb49a6dc548c0da793 (patch) | |
tree | 23f2f136c495b4064f43a0c4148391c46b9fa997 /frontend/src/pages/System | |
parent | 6b82a734e2bc597b219472774c0ec58038630c65 (diff) | |
download | bazarr-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.tsx | 55 | ||||
-rw-r--r-- | frontend/src/pages/System/Logs/modal.tsx | 26 | ||||
-rw-r--r-- | frontend/src/pages/System/Logs/table.tsx | 87 | ||||
-rw-r--r-- | frontend/src/pages/System/Providers/index.tsx | 48 | ||||
-rw-r--r-- | frontend/src/pages/System/Providers/table.tsx | 31 | ||||
-rw-r--r-- | frontend/src/pages/System/Releases/index.tsx | 78 | ||||
-rw-r--r-- | frontend/src/pages/System/Status/index.tsx | 164 | ||||
-rw-r--r-- | frontend/src/pages/System/Status/style.scss | 3 | ||||
-rw-r--r-- | frontend/src/pages/System/Status/table.tsx | 35 | ||||
-rw-r--r-- | frontend/src/pages/System/Tasks/index.tsx | 39 | ||||
-rw-r--r-- | frontend/src/pages/System/Tasks/table.tsx | 62 |
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; |