first commit
This commit is contained in:
167
src/component/Admin/Node/EditNode/BasicInfoSection.tsx
Executable file
167
src/component/Admin/Node/EditNode/BasicInfoSection.tsx
Executable file
@@ -0,0 +1,167 @@
|
||||
import { Alert, FormControl, FormControlLabel, Switch, Typography } from "@mui/material";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useCallback, useContext, useMemo, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { testNode } from "../../../../api/api";
|
||||
import { Node, NodeStatus, NodeType } from "../../../../api/dashboard";
|
||||
import { useAppDispatch } from "../../../../redux/hooks";
|
||||
import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar";
|
||||
import { DenseFilledTextField, SecondaryButton } from "../../../Common/StyledComponents";
|
||||
import SettingForm from "../../../Pages/Setting/SettingForm";
|
||||
import { Code } from "../../../Common/Code.tsx";
|
||||
import { EndpointInput } from "../../Common/EndpointInput";
|
||||
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings";
|
||||
import { NodeSettingContext } from "./NodeSettingWrapper";
|
||||
const BasicInfoSection = () => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const dispatch = useAppDispatch();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { values, setNode } = useContext(NodeSettingContext);
|
||||
const [testNodeLoading, setTestNodeLoading] = useState(false);
|
||||
|
||||
const onNameChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({ ...p, name: e.target.value }));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onServerChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({ ...p, server: e.target.value }));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onSlaveKeyChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({ ...p, slave_key: e.target.value }));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onWeightChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const weight = parseInt(e.target.value);
|
||||
setNode((p: Node) => ({ ...p, weight: isNaN(weight) ? 1 : weight }));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onStatusChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
status: e.target.checked ? NodeStatus.active : NodeStatus.suspended,
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const isActive = useMemo(() => {
|
||||
return values.status === NodeStatus.active;
|
||||
}, [values.status]);
|
||||
|
||||
const nodeTypeText = useMemo(() => {
|
||||
return values.type === NodeType.master ? t("node.master") : t("node.slave");
|
||||
}, [values.type, t]);
|
||||
|
||||
const onTestNode = useCallback(() => {
|
||||
setTestNodeLoading(true);
|
||||
dispatch(testNode({ node: values }))
|
||||
.then((res) => {
|
||||
enqueueSnackbar(t("node.testNodeSuccess"), { variant: "success", action: DefaultCloseAction });
|
||||
})
|
||||
.finally(() => {
|
||||
setTestNodeLoading(false);
|
||||
});
|
||||
}, [dispatch, values]);
|
||||
|
||||
return (
|
||||
<SettingSection>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("policy.basicInfo")}
|
||||
</Typography>
|
||||
<SettingSectionContent>
|
||||
{values.type === NodeType.master && (
|
||||
<SettingForm lgWidth={5}>
|
||||
<Alert severity="info">{t("node.thisIsMasterNodes")}</Alert>
|
||||
</SettingForm>
|
||||
)}
|
||||
<SettingForm lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
disabled={values.type === NodeType.master}
|
||||
control={<Switch checked={isActive} onChange={onStatusChange} />}
|
||||
label={t("node.enableNode")}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.enableNodeDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("node.name")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField required value={values.name} onChange={onNameChange} />
|
||||
<NoMarginHelperText>{t("node.nameNode")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("node.type")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField disabled value={nodeTypeText} />
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
{values.type === NodeType.slave && (
|
||||
<>
|
||||
<SettingForm title={t("node.server")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<EndpointInput
|
||||
fullWidth
|
||||
enforceProtocol
|
||||
required
|
||||
value={values.server}
|
||||
onChange={onServerChange}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.serverDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("node.slaveSecret")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField required value={values.slave_key} onChange={onSlaveKeyChange} />
|
||||
<NoMarginHelperText>
|
||||
<Trans i18nKey="node.slaveSecretDes" ns="dashboard" components={[<Code />, <Code />]} />
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</>
|
||||
)}
|
||||
<SettingForm title={t("node.loadBalancerRank")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
type="number"
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
inputProps: {
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
required
|
||||
value={values.weight}
|
||||
onChange={onWeightChange}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.loadBalancerRankDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
{values.type === NodeType.slave && (
|
||||
<SettingForm lgWidth={5}>
|
||||
<SecondaryButton loading={testNodeLoading} variant="contained" onClick={onTestNode}>
|
||||
{t("node.testNode")}
|
||||
</SecondaryButton>
|
||||
</SettingForm>
|
||||
)}
|
||||
</SettingSectionContent>
|
||||
</SettingSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicInfoSection;
|
||||
568
src/component/Admin/Node/EditNode/CapabilitiesSection.tsx
Executable file
568
src/component/Admin/Node/EditNode/CapabilitiesSection.tsx
Executable file
@@ -0,0 +1,568 @@
|
||||
import {
|
||||
CircularProgress,
|
||||
Collapse,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Link,
|
||||
ListItemText,
|
||||
SelectChangeEvent,
|
||||
Switch,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { testNodeDownloader } from "../../../../api/api";
|
||||
import { DownloaderProvider, Node, NodeType } from "../../../../api/dashboard";
|
||||
import { NodeCapability } from "../../../../api/workflow";
|
||||
import { useAppDispatch } from "../../../../redux/hooks";
|
||||
import Boolset from "../../../../util/boolset";
|
||||
import { DefaultCloseAction } from "../../../Common/Snackbar/snackbar";
|
||||
import { DenseFilledTextField, DenseSelect, SecondaryButton } from "../../../Common/StyledComponents";
|
||||
import { SquareMenuItem } from "../../../FileManager/ContextMenu/ContextMenu";
|
||||
import QuestionCircle from "../../../Icons/QuestionCircle";
|
||||
import SettingForm from "../../../Pages/Setting/SettingForm";
|
||||
import { Code } from "../../../Common/Code.tsx";
|
||||
import { EndpointInput } from "../../Common/EndpointInput";
|
||||
import { NoMarginHelperText, SettingSection, SettingSectionContent } from "../../Settings/Settings";
|
||||
import { NodeSettingContext } from "./NodeSettingWrapper";
|
||||
import StoreFilesHintDialog from "./StoreFilesHintDialog";
|
||||
const MonacoEditor = lazy(() => import("../../../Viewers/CodeViewer/MonacoEditor"));
|
||||
|
||||
const CapabilitiesSection = () => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { values, setNode } = useContext(NodeSettingContext);
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [editedConfigAria2, setEditedConfigAria2] = useState("");
|
||||
const [editedConfigQbittorrent, setEditedConfigQbittorrent] = useState("");
|
||||
const [testDownloaderLoading, setTestDownloaderLoading] = useState(false);
|
||||
const [storeFilesHintDialogOpen, setStoreFilesHintDialogOpen] = useState(false);
|
||||
|
||||
const capabilities = useMemo(() => {
|
||||
return new Boolset(values.capabilities ?? "");
|
||||
}, [values.capabilities]);
|
||||
|
||||
const hasRemoteDownload = useMemo(() => {
|
||||
return capabilities.enabled(NodeCapability.remote_download);
|
||||
}, [capabilities]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditedConfigAria2(
|
||||
values.settings?.aria2?.options ? JSON.stringify(values.settings?.aria2?.options, null, 2) : "",
|
||||
);
|
||||
}, [values.settings?.aria2?.options]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditedConfigQbittorrent(
|
||||
values.settings?.qbittorrent?.options ? JSON.stringify(values.settings?.qbittorrent?.options, null, 2) : "",
|
||||
);
|
||||
}, [values.settings?.qbittorrent?.options]);
|
||||
|
||||
const onCapabilityChange = useCallback(
|
||||
(capability: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
capabilities: new Boolset(p.capabilities).set(capability, e.target.checked).toString(),
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onProviderChange = useCallback(
|
||||
(e: SelectChangeEvent<unknown>) => {
|
||||
const provider = e.target.value as DownloaderProvider;
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
provider,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onAria2ServerChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
aria2: {
|
||||
...p.settings?.aria2,
|
||||
server: e.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onAria2TokenChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
aria2: {
|
||||
...p.settings?.aria2,
|
||||
token: e.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onAria2TempPathChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
aria2: {
|
||||
...p.settings?.aria2,
|
||||
temp_path: e.target.value ? e.target.value : undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onQBittorrentServerChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
qbittorrent: {
|
||||
...p.settings?.qbittorrent,
|
||||
server: e.target.value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onQBittorrentUserChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
qbittorrent: {
|
||||
...p.settings?.qbittorrent,
|
||||
user: e.target.value ? e.target.value : undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onQBittorrentPasswordChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
qbittorrent: {
|
||||
...p.settings?.qbittorrent,
|
||||
password: e.target.value ? e.target.value : undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onQBittorrentTempPathChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
qbittorrent: {
|
||||
...p.settings?.qbittorrent,
|
||||
temp_path: e.target.value ? e.target.value : undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onIntervalChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const interval = parseInt(e.target.value);
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
interval: isNaN(interval) ? undefined : interval,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onWaitForSeedingChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: {
|
||||
...p.settings,
|
||||
wait_for_seeding: e.target.checked ? true : undefined,
|
||||
},
|
||||
}));
|
||||
},
|
||||
[setNode],
|
||||
);
|
||||
|
||||
const onEditedConfigAria2Blur = useCallback(
|
||||
(value: string) => {
|
||||
var res: Record<string, any> | undefined = undefined;
|
||||
if (value) {
|
||||
try {
|
||||
res = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
setNode((p: Node) => ({ ...p, settings: { ...p.settings, aria2: { ...p.settings?.aria2, options: res } } }));
|
||||
},
|
||||
[editedConfigAria2, setNode],
|
||||
);
|
||||
|
||||
const onEditedConfigQbittorrentBlur = useCallback(
|
||||
(value: string) => {
|
||||
var res: Record<string, any> | undefined = undefined;
|
||||
if (value) {
|
||||
try {
|
||||
res = JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
setNode((p: Node) => ({
|
||||
...p,
|
||||
settings: { ...p.settings, qbittorrent: { ...p.settings?.qbittorrent, options: res } },
|
||||
}));
|
||||
},
|
||||
[editedConfigQbittorrent, setNode],
|
||||
);
|
||||
|
||||
const onTestDownloaderClick = useCallback(() => {
|
||||
setTestDownloaderLoading(true);
|
||||
dispatch(testNodeDownloader({ node: values }))
|
||||
.then((res) => {
|
||||
enqueueSnackbar({
|
||||
variant: "success",
|
||||
message: t("node.downloaderTestPass", { version: res }),
|
||||
action: DefaultCloseAction,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setTestDownloaderLoading(false);
|
||||
});
|
||||
}, [values]);
|
||||
|
||||
const onStoreFilesClick = useCallback(() => {
|
||||
setStoreFilesHintDialogOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StoreFilesHintDialog open={storeFilesHintDialogOpen} onClose={() => setStoreFilesHintDialogOpen(false)} />
|
||||
<SettingSection>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("node.features")}
|
||||
</Typography>
|
||||
<SettingSectionContent>
|
||||
<SettingForm lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={capabilities.enabled(NodeCapability.create_archive)}
|
||||
onChange={onCapabilityChange(NodeCapability.create_archive)}
|
||||
/>
|
||||
}
|
||||
label={t("application:fileManager.createArchive")}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.createArchiveDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={capabilities.enabled(NodeCapability.extract_archive)}
|
||||
onChange={onCapabilityChange(NodeCapability.extract_archive)}
|
||||
/>
|
||||
}
|
||||
label={t("application:fileManager.extractArchive")}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.extractArchiveDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={capabilities.enabled(NodeCapability.remote_download)}
|
||||
onChange={onCapabilityChange(NodeCapability.remote_download)}
|
||||
/>
|
||||
}
|
||||
label={t("application:navbar.remoteDownload")}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.remoteDownloadDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
{values.type === NodeType.slave && (
|
||||
<SettingForm lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={onStoreFilesClick}
|
||||
disabled={(values.edges?.storage_policy?.length ?? 0) > 0}
|
||||
checked={(values.edges?.storage_policy?.length ?? 0) > 0}
|
||||
/>
|
||||
}
|
||||
label={t("node.storeFiles")}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.storeFilesDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
)}
|
||||
</SettingSectionContent>
|
||||
</SettingSection>
|
||||
|
||||
<Collapse in={hasRemoteDownload} unmountOnExit>
|
||||
<SettingSection>
|
||||
<Typography
|
||||
variant="h6"
|
||||
gutterBottom
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
{t("node.remoteDownload")}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
window.open("https://docs.cloudreve.org/usage/remote-download", "_blank");
|
||||
}}
|
||||
>
|
||||
<QuestionCircle />
|
||||
</IconButton>
|
||||
</Typography>
|
||||
<SettingSectionContent>
|
||||
<SettingForm title={t("node.downloader")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseSelect value={values.settings?.provider || DownloaderProvider.aria2} onChange={onProviderChange}>
|
||||
<SquareMenuItem value={DownloaderProvider.aria2}>
|
||||
<ListItemText primary="Aria2" slotProps={{ primary: { variant: "body2" } }} />
|
||||
</SquareMenuItem>
|
||||
<SquareMenuItem value={DownloaderProvider.qbittorrent}>
|
||||
<ListItemText primary="qBittorrent" slotProps={{ primary: { variant: "body2" } }} />
|
||||
</SquareMenuItem>
|
||||
</DenseSelect>
|
||||
<NoMarginHelperText>
|
||||
{values.settings?.provider === DownloaderProvider.qbittorrent
|
||||
? t("node.qbittorrentDes")
|
||||
: t("node.aria2Des")}
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
|
||||
{values.settings?.provider === DownloaderProvider.aria2 && (
|
||||
<>
|
||||
<SettingForm title={t("node.rpcServer")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<EndpointInput
|
||||
fullWidth
|
||||
required
|
||||
value={values.settings?.aria2?.server || ""}
|
||||
onChange={onAria2ServerChange}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
<NoMarginHelperText>
|
||||
<Trans i18nKey="node.rpcServerHelpDes" ns="dashboard" components={[<Code />]} />
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("node.rpcToken")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField value={values.settings?.aria2?.token || ""} onChange={onAria2TokenChange} />
|
||||
<NoMarginHelperText>
|
||||
<Trans i18nKey="node.rpcTokenDes" ns="dashboard" components={[<Code />]} />
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("group.aria2Options")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<MonacoEditor
|
||||
theme={theme.palette.mode === "dark" ? "vs-dark" : "vs"}
|
||||
language="json"
|
||||
value={editedConfigAria2}
|
||||
onChange={(value) => setEditedConfigAria2(value || "")}
|
||||
onBlur={onEditedConfigAria2Blur}
|
||||
height="200px"
|
||||
minHeight="200px"
|
||||
options={{
|
||||
wordWrap: "on",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
<NoMarginHelperText>
|
||||
<Trans
|
||||
i18nKey="node.downloaderOptionDes"
|
||||
ns="dashboard"
|
||||
components={[
|
||||
<Link href="https://aria2.github.io/manual/en/html/aria2c.html#id2" target="_blank" />,
|
||||
]}
|
||||
/>
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("node.tempPath")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
value={values.settings?.aria2?.temp_path || ""}
|
||||
onChange={onAria2TempPathChange}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.tempPathDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</>
|
||||
)}
|
||||
|
||||
{values.settings?.provider === DownloaderProvider.qbittorrent && (
|
||||
<>
|
||||
<SettingForm title={t("node.webUIEndpoint")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<EndpointInput
|
||||
fullWidth
|
||||
required
|
||||
value={values.settings?.qbittorrent?.server || ""}
|
||||
onChange={onQBittorrentServerChange}
|
||||
variant={"outlined"}
|
||||
/>
|
||||
<NoMarginHelperText>
|
||||
<Trans i18nKey="node.webUIEndpointDes" ns="dashboard" components={[<Code />]} />
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("policy.accessCredential")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
placeholder={t("node.webUIUsername")}
|
||||
value={values.settings?.qbittorrent?.user || ""}
|
||||
onChange={onQBittorrentUserChange}
|
||||
/>
|
||||
<DenseFilledTextField
|
||||
placeholder={t("node.webUIPassword")}
|
||||
type="password"
|
||||
sx={{ mt: 1 }}
|
||||
value={values.settings?.qbittorrent?.password || ""}
|
||||
onChange={onQBittorrentPasswordChange}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.webUICredDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("group.aria2Options")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<Suspense fallback={<CircularProgress />}>
|
||||
<MonacoEditor
|
||||
theme={theme.palette.mode === "dark" ? "vs-dark" : "vs"}
|
||||
language="json"
|
||||
value={editedConfigQbittorrent}
|
||||
onChange={(value) => setEditedConfigQbittorrent(value || "")}
|
||||
onBlur={onEditedConfigQbittorrentBlur}
|
||||
height="200px"
|
||||
minHeight="200px"
|
||||
options={{
|
||||
wordWrap: "on",
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
<NoMarginHelperText>
|
||||
<Trans
|
||||
i18nKey="node.downloaderOptionDes"
|
||||
ns="dashboard"
|
||||
components={[
|
||||
<Link
|
||||
href="https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-new-torrent"
|
||||
target="_blank"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm title={t("node.tempPath")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
value={values.settings?.qbittorrent?.temp_path || ""}
|
||||
onChange={onQBittorrentTempPathChange}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.tempPathDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingForm title={t("node.refreshInterval")} lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<DenseFilledTextField
|
||||
type="number"
|
||||
slotProps={{ htmlInput: { min: 1 } }}
|
||||
required
|
||||
value={values.settings?.interval || ""}
|
||||
onChange={onIntervalChange}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.refreshIntervalDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
|
||||
<SettingForm lgWidth={5}>
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch checked={values.settings?.wait_for_seeding || false} onChange={onWaitForSeedingChange} />
|
||||
}
|
||||
label={t("node.waitForSeeding")}
|
||||
/>
|
||||
<NoMarginHelperText>{t("node.waitForSeedingDes")}</NoMarginHelperText>
|
||||
</FormControl>
|
||||
</SettingForm>
|
||||
<SettingForm lgWidth={5}>
|
||||
<SecondaryButton onClick={onTestDownloaderClick} variant="contained" loading={testDownloaderLoading}>
|
||||
{t("node.testDownloader")}
|
||||
</SecondaryButton>
|
||||
</SettingForm>
|
||||
</SettingSectionContent>
|
||||
</SettingSection>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CapabilitiesSection;
|
||||
34
src/component/Admin/Node/EditNode/EditNode.tsx
Executable file
34
src/component/Admin/Node/EditNode/EditNode.tsx
Executable file
@@ -0,0 +1,34 @@
|
||||
import { Container } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Node } from "../../../../api/dashboard";
|
||||
import PageContainer from "../../../Pages/PageContainer";
|
||||
import PageHeader from "../../../Pages/PageHeader";
|
||||
import BasicInfoSection from "./BasicInfoSection";
|
||||
import CapabilitiesSection from "./CapabilitiesSection";
|
||||
import NodeForm from "./NodeForm";
|
||||
import NodeSettingWrapper from "./NodeSettingWrapper";
|
||||
|
||||
const EditNode = () => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [node, setNode] = useState<Node | null>(null);
|
||||
const nodeID = parseInt(id ?? "0");
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Container maxWidth="xl">
|
||||
<PageHeader title={t("node.editNode", { node: node?.name })} />
|
||||
<NodeSettingWrapper nodeID={nodeID} onNodeChange={setNode}>
|
||||
<NodeForm>
|
||||
<BasicInfoSection />
|
||||
<CapabilitiesSection />
|
||||
</NodeForm>
|
||||
</NodeSettingWrapper>
|
||||
</Container>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditNode;
|
||||
19
src/component/Admin/Node/EditNode/NodeForm.tsx
Executable file
19
src/component/Admin/Node/EditNode/NodeForm.tsx
Executable file
@@ -0,0 +1,19 @@
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import { useContext } from "react";
|
||||
import { NodeSettingContext } from "./NodeSettingWrapper";
|
||||
|
||||
export interface NodeFormProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const NodeForm = ({ children }: NodeFormProps) => {
|
||||
const { formRef } = useContext(NodeSettingContext);
|
||||
|
||||
return (
|
||||
<Box component="form" ref={formRef} noValidate>
|
||||
<Stack spacing={5}>{children}</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeForm;
|
||||
157
src/component/Admin/Node/EditNode/NodeSettingWrapper.tsx
Executable file
157
src/component/Admin/Node/EditNode/NodeSettingWrapper.tsx
Executable file
@@ -0,0 +1,157 @@
|
||||
import { Box } from "@mui/material";
|
||||
import * as React from "react";
|
||||
import { createContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CSSTransition, SwitchTransition } from "react-transition-group";
|
||||
import { getNodeDetail, upsertNode } from "../../../../api/api.ts";
|
||||
import { Node, StoragePolicy } from "../../../../api/dashboard.ts";
|
||||
import { useAppDispatch } from "../../../../redux/hooks.ts";
|
||||
import FacebookCircularProgress from "../../../Common/CircularProgress.tsx";
|
||||
import { SavingFloat } from "../../Settings/SettingWrapper.tsx";
|
||||
|
||||
export interface NodeSettingWrapperProps {
|
||||
nodeID: number;
|
||||
children: React.ReactNode;
|
||||
onNodeChange: (node: Node) => void;
|
||||
}
|
||||
|
||||
export interface NodeSettingContextProps {
|
||||
values: Node;
|
||||
setNode: (f: (p: Node) => Node) => void;
|
||||
formRef?: React.RefObject<HTMLFormElement>;
|
||||
}
|
||||
|
||||
const defaultNode: Node = {
|
||||
id: 0,
|
||||
name: "",
|
||||
status: undefined,
|
||||
type: undefined,
|
||||
server: "",
|
||||
slave_key: "",
|
||||
capabilities: "",
|
||||
weight: 1,
|
||||
settings: {},
|
||||
edges: {
|
||||
storage_policy: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const NodeSettingContext = createContext<NodeSettingContextProps>({
|
||||
values: { ...defaultNode },
|
||||
setNode: () => {},
|
||||
});
|
||||
|
||||
const nodeValueFilter = (node: Node): Node => {
|
||||
return {
|
||||
...node,
|
||||
edges: {
|
||||
storage_policy: node.edges.storage_policy?.map(
|
||||
(p): StoragePolicy =>
|
||||
({
|
||||
id: p.id,
|
||||
}) as StoragePolicy,
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const NodeSettingWrapper = ({ nodeID, children, onNodeChange }: NodeSettingWrapperProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation("dashboard");
|
||||
const [values, setValues] = useState<Node>({
|
||||
...defaultNode,
|
||||
});
|
||||
const [modifiedValues, setModifiedValues] = useState<Node>({
|
||||
...defaultNode,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const showSaveButton = useMemo(() => {
|
||||
return JSON.stringify(modifiedValues) !== JSON.stringify(values);
|
||||
}, [modifiedValues, values]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
dispatch(getNodeDetail(nodeID))
|
||||
.then((res) => {
|
||||
setValues(nodeValueFilter(res));
|
||||
setModifiedValues(nodeValueFilter(res));
|
||||
onNodeChange(nodeValueFilter(res));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [nodeID]);
|
||||
|
||||
const revert = () => {
|
||||
setModifiedValues(values);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
if (formRef.current) {
|
||||
if (!formRef.current.checkValidity()) {
|
||||
formRef.current.reportValidity();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
dispatch(
|
||||
upsertNode({
|
||||
node: { ...modifiedValues },
|
||||
}),
|
||||
)
|
||||
.then((res) => {
|
||||
setValues(nodeValueFilter(res));
|
||||
setModifiedValues(nodeValueFilter(res));
|
||||
onNodeChange(nodeValueFilter(res));
|
||||
})
|
||||
.finally(() => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeSettingContext.Provider
|
||||
value={{
|
||||
values: modifiedValues,
|
||||
setNode: setModifiedValues,
|
||||
formRef,
|
||||
}}
|
||||
>
|
||||
<SwitchTransition>
|
||||
<CSSTransition
|
||||
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
|
||||
classNames="fade"
|
||||
key={`${loading}`}
|
||||
>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
{loading && (
|
||||
<Box
|
||||
sx={{
|
||||
pt: 20,
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<FacebookCircularProgress />
|
||||
</Box>
|
||||
)}
|
||||
{!loading && (
|
||||
<Box>
|
||||
{children}
|
||||
<SavingFloat in={showSaveButton} submitting={submitting} revert={revert} submit={submit} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</CSSTransition>
|
||||
</SwitchTransition>
|
||||
</NodeSettingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeSettingWrapper;
|
||||
35
src/component/Admin/Node/EditNode/StoreFilesHintDialog.tsx
Executable file
35
src/component/Admin/Node/EditNode/StoreFilesHintDialog.tsx
Executable file
@@ -0,0 +1,35 @@
|
||||
import { DialogContent, Link, Typography } from "@mui/material";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import DraggableDialog from "../../../Dialogs/DraggableDialog";
|
||||
export interface StoreFilesHintDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const StoreFilesHintDialog = ({ open, onClose }: StoreFilesHintDialogProps) => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
return (
|
||||
<DraggableDialog
|
||||
dialogProps={{
|
||||
open,
|
||||
onClose,
|
||||
fullWidth: true,
|
||||
maxWidth: "sm",
|
||||
}}
|
||||
title={t("node.storeFiles")}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography variant="body2">
|
||||
<Trans
|
||||
i18nKey="node.storeFilesHint"
|
||||
ns="dashboard"
|
||||
components={[<Link component={RouterLink} to="/admin/policy" />]}
|
||||
/>
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default StoreFilesHintDialog;
|
||||
3
src/component/Admin/Node/EditNode/index.tsx
Executable file
3
src/component/Admin/Node/EditNode/index.tsx
Executable file
@@ -0,0 +1,3 @@
|
||||
import EditNode from "./EditNode";
|
||||
|
||||
export default EditNode;
|
||||
Reference in New Issue
Block a user