This commit is contained in:
2024-02-25 08:27:01 +08:00
commit 20c1fa08dc
279 changed files with 78489 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import React, { useEffect, useState } from "react";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import InputAdornment from "@material-ui/core/InputAdornment";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
export default function DomainInput({ onChange, value, required, label }) {
const [domain, setDomain] = useState("");
const [protocol, setProtocol] = useState("https://");
const [error, setError] = useState();
useState(() => {
value = value ? value : "";
if (value.startsWith("https://")) {
setDomain(value.replace("https://", ""));
setProtocol("https://");
} else {
if (value !== "") {
setDomain(value.replace("http://", ""));
setProtocol("http://");
}
}
}, [value]);
useEffect(() => {
if (protocol === "http://" && window.location.protocol === "https:") {
setError(
"您当前站点启用了 HTTPS ,此处选择 HTTP 可能会导致无法连接。"
);
} else {
setError("");
}
}, [protocol]);
return (
<FormControl>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Input
error={error !== ""}
value={domain}
onChange={(e) => {
setDomain(e.target.value);
onChange({
target: {
value: protocol + e.target.value,
},
});
}}
required={required}
startAdornment={
<InputAdornment position="start">
<Select
value={protocol}
onChange={(e) => {
setProtocol(e.target.value);
onChange({
target: {
value: e.target.value + domain,
},
});
}}
>
<MenuItem value={"http://"}>http://</MenuItem>
<MenuItem value={"https://"}>https://</MenuItem>
</Select>
</InputAdornment>
}
/>
{error !== "" && (
<FormHelperText error={error !== ""}>{error}</FormHelperText>
)}
</FormControl>
);
}

View File

@@ -0,0 +1,94 @@
import React, { useCallback } from "react";
import Autocomplete from "@material-ui/lab/Autocomplete";
import TextField from "@material-ui/core/TextField";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Typography from "@material-ui/core/Typography";
import { toggleSnackbar } from "../../../redux/explorer";
export default function FileSelector({ onChange, value, label }) {
const [selectValue, setSelectValue] = React.useState(
value.map((v) => {
return {
ID: v,
Name: "文件ID " + v,
};
})
);
const [loading, setLoading] = React.useState(false);
const [inputValue, setInputValue] = React.useState("");
const [options, setOptions] = React.useState([]);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
React.useEffect(() => {
let active = true;
if (
inputValue === "" ||
selectValue.findIndex((v) => v.ID.toString() === inputValue) >= 0
) {
setOptions([]);
return;
}
setLoading(true);
API.post("/admin/file/list", {
page: 1,
page_size: 10,
order_by: "id desc",
conditions: {
id: inputValue,
},
searches: {},
})
.then((response) => {
if (active) {
let newOptions = [];
newOptions = [...newOptions, ...response.data.items];
setOptions(newOptions);
}
setLoading(false);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
setLoading(false);
});
return () => {
active = false;
};
}, [selectValue, inputValue]);
return (
<Autocomplete
multiple
style={{ width: 300 }}
options={options}
getOptionLabel={(option) =>
typeof option === "string" ? option : option.Name
}
filterOptions={(x) => x}
loading={loading}
autoComplete
includeInputInList
filterSelectedOptions
value={selectValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
onChange={(event, newValue) => {
setSelectValue(newValue);
onChange(JSON.stringify(newValue.map((v) => v.ID)));
}}
renderInput={(params) => (
<TextField {...params} label={label} type={"number"} />
)}
renderOption={(option) => (
<Typography noWrap>{option.Name}</Typography>
)}
/>
);
}

View File

@@ -0,0 +1,95 @@
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import Input from "@material-ui/core/Input";
import Chip from "@material-ui/core/Chip";
import MenuItem from "@material-ui/core/MenuItem";
import { getSelectItemStyles } from "../../../utils";
import FormHelperText from "@material-ui/core/FormHelperText";
import { FormControl } from "@material-ui/core";
import API from "../../../middleware/Api";
import { useTheme } from "@material-ui/core/styles";
export default function PolicySelector({
onChange,
value,
label,
helperText,
filter,
}) {
const [policies, setPolicies] = useState({});
const theme = useTheme();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
let data = response.data.items;
if (filter) {
data = data.filter(filter);
}
data.forEach((v) => {
res[v.ID] = v.Name;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
return (
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
multiple
value={value}
onChange={onChange}
input={<Input id="select-multiple-chip" />}
renderValue={(selected) => (
<div>
{selected.map((value) => (
<Chip
style={{
margin: 2,
}}
key={value}
size={"small"}
label={policies[value]}
/>
))}
</div>
)}
>
{Object.keys(policies).map((pid) => (
<MenuItem
key={pid}
value={pid}
style={getSelectItemStyles(pid, value, theme)}
>
{policies[pid]}
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{helperText}
</FormHelperText>
</FormControl>
);
}

View File

@@ -0,0 +1,101 @@
import React, { useCallback, useEffect, useState } from "react";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import InputAdornment from "@material-ui/core/InputAdornment";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import FormHelperText from "@material-ui/core/FormHelperText";
const unitTransform = (v) => {
if (!v || v.toString() === "0") {
return [0, 1024 * 1024];
}
for (let i = 4; i >= 0; i--) {
const base = Math.pow(1024, i);
if (v % base === 0) {
return [v / base, base];
}
}
};
export default function SizeInput({
onChange,
min,
value,
required,
label,
max,
suffix,
}) {
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const [unit, setUnit] = useState(1);
const [val, setVal] = useState(value);
const [err, setError] = useState("");
useEffect(() => {
onChange({
target: {
value: (val * unit).toString(),
},
});
if (val * unit > max || val * unit < min) {
setError("不符合尺寸限制");
} else {
setError("");
}
}, [val, unit, max, min]);
useEffect(() => {
const res = unitTransform(value);
setUnit(res[1]);
setVal(res[0]);
}, []);
return (
<FormControl error={err !== ""}>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Input
style={{ width: 200 }}
value={val}
type={"number"}
inputProps={{ step: 1 }}
onChange={(e) => setVal(e.target.value)}
required={required}
endAdornment={
<InputAdornment position="end">
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={unit}
onChange={(e) => setUnit(e.target.value)}
>
<MenuItem value={1}>B{suffix && suffix}</MenuItem>
<MenuItem value={1024}>
KB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024}>
MB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024 * 1024}>
GB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024 * 1024 * 1024}>
TB{suffix && suffix}
</MenuItem>
</Select>
</InputAdornment>
}
/>
{err !== "" && <FormHelperText>{err}</FormHelperText>}
</FormControl>
);
}

View File

@@ -0,0 +1,477 @@
import React, { useCallback, useEffect, useState } from "react";
import clsx from "clsx";
import { lighten, makeStyles, useTheme } from "@material-ui/core/styles";
import Drawer from "@material-ui/core/Drawer";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import List from "@material-ui/core/List";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import UserAvatar from "../Navbar/UserAvatar";
import {
Assignment,
AttachMoney,
Category,
CloudDownload,
Contactless,
Contacts,
Group,
Home,
Image,
InsertDriveFile,
Language,
ListAlt,
Mail,
Palette,
Person,
Report,
Settings,
SettingsEthernet,
Share,
ShoppingCart,
Storage,
} from "@material-ui/icons";
import { withStyles } from "@material-ui/core";
import MuiExpansionPanel from "@material-ui/core/ExpansionPanel";
import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import { useHistory, useLocation } from "react-router";
import { useRouteMatch } from "react-router-dom";
import { useDispatch } from "react-redux";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import pathHelper from "../../utils/page";
import { useTranslation } from "react-i18next";
const ExpansionPanel = withStyles({
root: {
maxWidth: "100%",
boxShadow: "none",
"&:not(:last-child)": {
borderBottom: 0,
},
"&:before": {
display: "none",
},
"&$expanded": { margin: 0 },
},
expanded: {},
})(MuiExpansionPanel);
const ExpansionPanelSummary = withStyles({
root: {
minHeight: 0,
padding: 0,
"&$expanded": {
minHeight: 0,
},
},
content: {
maxWidth: "100%",
margin: 0,
display: "block",
"&$expanded": {
margin: "0",
},
},
expanded: {},
})(MuiExpansionPanelSummary);
const ExpansionPanelDetails = withStyles((theme) => ({
root: {
display: "block",
padding: theme.spacing(0),
},
}))(MuiExpansionPanelDetails);
const drawerWidth = 240;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
width: "100%",
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: "none",
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: "nowrap",
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
width: theme.spacing(7) + 1,
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9) + 1,
},
},
title: {
flexGrow: 1,
},
toolbar: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
sub: {
paddingLeft: 36,
color: theme.palette.text.secondary,
},
subMenu: {
backgroundColor: theme.palette.background.default,
paddingTop: 0,
paddingBottom: 0,
},
active: {
backgroundColor: lighten(theme.palette.primary.main, 0.8),
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: lighten(theme.palette.primary.main, 0.7),
},
},
activeText: {
fontWeight: 500,
},
activeIcon: {
color: theme.palette.primary.main,
},
}));
const items = [
{
title: "nav.summary",
icon: <Home />,
path: "home",
},
{
title: "nav.settings",
icon: <Settings />,
sub: [
{
title: "nav.basicSetting",
path: "basic",
icon: <Language />,
},
{
title: "nav.publicAccess",
path: "access",
icon: <Contacts />,
},
{
title: "nav.email",
path: "mail",
icon: <Mail />,
},
{
title: "nav.transportation",
path: "upload",
icon: <SettingsEthernet />,
},
{
title: "vas.vas",
path: "vas",
icon: <AttachMoney />,
},
{
title: "nav.appearance",
path: "theme",
icon: <Palette />,
},
{
title: "nav.image",
path: "image",
icon: <Image />,
},
{
title: "nav.captcha",
path: "captcha",
icon: <Category />,
},
],
},
{
title: "nav.storagePolicy",
icon: <Storage />,
path: "policy",
},
{
title: "nav.nodes",
icon: <Contactless />,
path: "node",
},
{
title: "nav.groups",
icon: <Group />,
path: "group",
},
{
title: "nav.users",
icon: <Person />,
path: "user",
},
{
title: "nav.files",
icon: <InsertDriveFile />,
path: "file",
},
{
title: "nav.shares",
icon: <Share />,
path: "share",
},
{
title: "vas.reports",
icon: <Report />,
path: "report",
},
{
title: "vas.orders",
icon: <ShoppingCart />,
path: "order",
},
{
title: "nav.tasks",
icon: <Assignment />,
sub: [
{
title: "nav.remoteDownload",
path: "download",
icon: <CloudDownload />,
},
{
title: "nav.generalTasks",
path: "task",
icon: <ListAlt />,
},
],
},
];
export default function Dashboard({ content }) {
const { t } = useTranslation("dashboard");
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = useState(!pathHelper.isMobile());
const [menuOpen, setMenuOpen] = useState(null);
const history = useHistory();
const location = useLocation();
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
useEffect(() => {
SetSubTitle(t("nav.title"));
}, []);
useEffect(() => {
return () => {
SetSubTitle();
};
}, []);
const { path } = useRouteMatch();
return (
<div className={classes.root}>
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title} noWrap>
{t("nav.dashboard")}
</Typography>
<UserAvatar />
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? (
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton>
</div>
<Divider />
<List className={classes.noPadding}>
{items.map((item) => {
if (item.path !== undefined) {
return (
<ListItem
onClick={() =>
history.push("/admin/" + item.path)
}
button
className={clsx({
[classes.active]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
key={item.title}
>
<ListItemIcon
className={clsx({
[classes.activeIcon]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
>
{item.icon}
</ListItemIcon>
<ListItemText
className={clsx({
[classes.activeText]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
primary={t(item.title)}
/>
</ListItem>
);
}
return (
// eslint-disable-next-line react/jsx-key
<ExpansionPanel
key={item.title}
square
expanded={menuOpen === item.title}
onChange={(event, isExpanded) => {
setMenuOpen(isExpanded ? item.title : null);
}}
>
<ExpansionPanelSummary
aria-controls="panel1d-content"
id="panel1d-header"
>
<ListItem button key={item.title}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={t(item.title)} />
</ListItem>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<List className={classes.subMenu}>
{item.sub.map((sub) => (
<ListItem
onClick={() =>
history.push(
"/admin/" + sub.path
)
}
className={clsx({
[classes.sub]: open,
[classes.active]: location.pathname.startsWith(
"/admin/" + sub.path
),
})}
button
key={sub.title}
>
<ListItemIcon
className={clsx({
[classes.activeIcon]: location.pathname.startsWith(
"/admin/" + sub.path
),
})}
>
{sub.icon}
</ListItemIcon>
<ListItemText
primary={t(sub.title)}
/>
</ListItem>
))}
</List>
</ExpansionPanelDetails>
</ExpansionPanel>
);
})}
</List>
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
{content(path)}
</main>
</div>
);
}

View File

@@ -0,0 +1,268 @@
import React, { useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Switch from "@material-ui/core/Switch";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
const defaultGroup = {
name: "",
group_id: 2,
time: "",
price: "",
score: "",
des: "",
highlight: false,
};
const groupEditToForm = (target) => {
return {
...target,
time: (target.time / 86400).toString(),
price: (target.price / 100).toString(),
score: target.score.toString(),
des: target.des.join("\n"),
};
};
export default function AddGroup({ open, onClose, onSubmit, groupEdit }) {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const [groups, setGroups] = useState([]);
const [group, setGroup] = useState(defaultGroup);
useEffect(() => {
if (groupEdit) {
setGroup(groupEditToForm(groupEdit));
} else {
setGroup(defaultGroup);
}
}, [groupEdit]);
useEffect(() => {
if (open && groups.length === 0) {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
}
// eslint-disable-next-line
}, [open]);
const handleChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.checked,
});
};
const submit = (e) => {
e.preventDefault();
const groupCopy = { ...group };
groupCopy.time = parseInt(groupCopy.time) * 86400;
groupCopy.price = parseInt(groupCopy.price) * 100;
groupCopy.score = parseInt(groupCopy.score);
groupCopy.id = groupEdit ? groupEdit.id : new Date().valueOf();
groupCopy.des = groupCopy.des.split("\n");
onSubmit(groupCopy, groupEdit !== null);
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
scroll={"paper"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">
{groupEdit ? t("editMembership") : t("addMembership")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("name")}
</InputLabel>
<Input
value={group.name}
onChange={handleChange("name")}
required
/>
<FormHelperText id="component-helper-text">
{t("productNameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("group")}
</InputLabel>
<Select
value={group.group_id}
onChange={handleChange("group_id")}
required
>
{groups.map((v) => {
if (v.ID !== 3) {
return (
<MenuItem value={v.ID}>
{v.Name}
</MenuItem>
);
}
return null;
})}
</Select>
<FormHelperText id="component-helper-text">
{t("groupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("durationDay")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={group.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
{t("durationGroupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceYuan")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0.01,
step: 0.01,
}}
value={group.price}
onChange={handleChange("price")}
required
/>
<FormHelperText id="component-helper-text">
{t("groupPriceDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceCredits")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={group.score}
onChange={handleChange("score")}
required
/>
<FormHelperText id="component-helper-text">
{t("priceCreditsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("productDescription")}
</InputLabel>
<Input
value={group.des}
onChange={handleChange("des")}
multiline
rowsMax={10}
required
/>
<FormHelperText id="component-helper-text">
{t("productDescriptionDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={group.highlight}
onChange={handleCheckChange(
"highlight"
)}
/>
}
label={t("highlight")}
/>
<FormHelperText id="component-helper-text">
{t("highlightDes")}
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button type={"submit"} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,193 @@
import React, { useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import SizeInput from "../Common/SizeInput";
import { makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
const packEditToForm = (target) => {
return {
...target,
size: target.size.toString(),
time: (target.time / 86400).toString(),
price: (target.price / 100).toString(),
score: target.score.toString(),
};
};
const defaultPack = {
name: "",
size: "1073741824",
time: "",
price: "",
score: "",
};
export default function AddPack({ open, onClose, onSubmit, packEdit }) {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const [pack, setPack] = useState(defaultPack);
useEffect(() => {
if (packEdit) {
setPack(packEditToForm(packEdit));
} else {
setPack(defaultPack);
}
}, [packEdit]);
const handleChange = (name) => (event) => {
setPack({
...pack,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
const packCopy = { ...pack };
packCopy.size = parseInt(packCopy.size);
packCopy.time = parseInt(packCopy.time) * 86400;
packCopy.price = parseInt(packCopy.price) * 100;
packCopy.score = parseInt(packCopy.score);
packCopy.id = packEdit ? packEdit.id : new Date().valueOf();
onSubmit(packCopy, packEdit !== null);
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">
{packEdit ? t("editStoragePack") : t("addStoragePack")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("name")}
</InputLabel>
<Input
value={pack.name}
onChange={handleChange("name")}
required
/>
<FormHelperText id="component-helper-text">
{t("productNameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<SizeInput
value={pack.size}
onChange={handleChange("size")}
min={1}
label={t("size")}
max={9223372036854775807}
required
/>
<FormHelperText id="component-helper-text">
{t("packSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("durationDay")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={pack.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
{t("durationDayDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceYuan")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0.01,
step: 0.01,
}}
value={pack.price}
onChange={handleChange("price")}
required
/>
<FormHelperText id="component-helper-text">
{t("packPriceDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("priceCredits")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={pack.score}
onChange={handleChange("score")}
required
/>
<FormHelperText id="component-helper-text">
{t("priceCreditsDes")}
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button type={"submit"} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,145 @@
import React from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import Typography from "@material-ui/core/Typography";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import Grid from "@material-ui/core/Grid";
import Card from "@material-ui/core/Card";
import CardActionArea from "@material-ui/core/CardActionArea";
import { makeStyles } from "@material-ui/core/styles";
import CardMedia from "@material-ui/core/CardMedia";
import CardContent from "@material-ui/core/CardContent";
import { useHistory } from "react-router";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
cardContainer: {
display: "flex",
},
cover: {
width: 100,
height: 60,
},
card: {},
content: {
flex: "1 0 auto",
},
bg: {
backgroundColor: theme.palette.background.default,
padding: "24px 24px",
},
dialogFooter: {
justifyContent: "space-between",
},
}));
const policies = [
{
name: "local",
img: "local.png",
path: "/admin/policy/add/local",
},
{
name: "remote",
img: "remote.png",
path: "/admin/policy/add/remote",
},
{
name: "qiniu",
img: "qiniu.png",
path: "/admin/policy/add/qiniu",
},
{
name: "oss",
img: "oss.png",
path: "/admin/policy/add/oss",
},
{
name: "upyun",
img: "upyun.png",
path: "/admin/policy/add/upyun",
},
{
name: "cos",
img: "cos.png",
path: "/admin/policy/add/cos",
},
{
name: "onedrive",
img: "onedrive.png",
path: "/admin/policy/add/onedrive",
},
{
name: "s3",
img: "s3.png",
path: "/admin/policy/add/s3",
},
];
export default function AddPolicy({ open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const location = useHistory();
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"sm"}
fullWidth
>
<DialogTitle id="alert-dialog-title">
{t("selectAStorageProvider")}
</DialogTitle>
<DialogContent dividers className={classes.bg}>
<Grid container spacing={2}>
{policies.map((v, index) => (
<Grid key={index} item sm={12} md={6}>
<Card className={classes.card}>
<CardActionArea
onClick={() => {
location.push(v.path);
onClose();
}}
className={classes.cardContainer}
>
<CardMedia
className={classes.cover}
image={"/static/img/" + v.img}
/>
<CardContent className={classes.content}>
<Typography
variant="subtitle1"
color="textSecondary"
>
{t(v.name)}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</DialogContent>
<DialogActions className={classes.dialogFooter}>
<Button
onClick={() =>
window.open(t("comparesStoragePoliciesLink"))
}
color="primary"
>
{t("comparesStoragePolicies")}
</Button>
<Button onClick={onClose} color="primary">
{tCommon("cancel")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,183 @@
import React, { useCallback, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import { makeStyles } from "@material-ui/core/styles";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
export default function AddRedeem({ open, onClose, products, onSuccess }) {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tCommon } = useTranslation("common");
const { t: tApp } = useTranslation();
const classes = useStyles();
const [input, setInput] = useState({
num: 1,
id: 0,
time: 1,
});
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({
...input,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
input.num = parseInt(input.num);
input.id = parseInt(input.id);
input.time = parseInt(input.time);
input.type = 2;
for (let i = 0; i < products.length; i++) {
if (products[i].id === input.id) {
if (products[i].group_id !== undefined) {
input.type = 1;
} else {
input.type = 0;
}
break;
}
}
API.post("/admin/redeem", input)
.then((response) => {
onSuccess(response.data);
onClose();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">
{t("generateGiftCode")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("numberOfCodes")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
max: 100,
}}
value={input.num}
onChange={handleChange("num")}
required
/>
<FormHelperText id="component-helper-text">
{t("numberOfCodesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("linkedProduct")}
</InputLabel>
<Select
value={input.id}
onChange={(e) => {
handleChange("id")(e);
}}
>
{products.map((v) => (
<MenuItem
key={v.id}
value={v.id}
data-type={"1"}
>
{v.name}
</MenuItem>
))}
<MenuItem value={0}>
{tApp("vas.credits")}
</MenuItem>
</Select>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("productQyt")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
value={input.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
{t("productQytDes")}
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
disabled={loading}
onClick={onClose}
color="default"
>
{tCommon("cancel")}
</Button>
<Button disabled={loading} type={"submit"} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,33 @@
import React from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import Typography from "@material-ui/core/Typography";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import { useTranslation } from "react-i18next";
export default function AlertDialog({ title, msg, open, onClose }) {
const { t } = useTranslation("common");
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography>{msg}</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{t("ok")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,355 @@
import React, { useCallback, useState } from "react";
import DialogContent from "@material-ui/core/DialogContent";
import { CompactPicker } from "react-color";
import Typography from "@material-ui/core/Typography";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import { createMuiTheme, makeStyles } from "@material-ui/core/styles";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import IconButton from "@material-ui/core/IconButton";
import { Add, Menu } from "@material-ui/icons";
import { ThemeProvider } from "@material-ui/styles";
import Fab from "@material-ui/core/Fab";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
picker: {
"& div": {
boxShadow: "none !important",
},
marginTop: theme.spacing(1),
},
"@global": {
".compact-picker:parent ": {
boxShadow: "none !important",
},
},
statusBar: {
height: 24,
width: "100%",
},
fab: {
textAlign: "right",
},
}));
export default function CreateTheme({ open, onClose, onSubmit }) {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tGlobal } = useTranslation("common");
const classes = useStyles();
const [theme, setTheme] = useState({
palette: {
primary: {
main: "#3f51b5",
contrastText: "#fff",
},
secondary: {
main: "#d81b60",
contrastText: "#fff",
},
},
});
const subTheme = useCallback(() => {
try {
return createMuiTheme(theme);
} catch (e) {
return createMuiTheme({});
}
}, [theme]);
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth={"md"}>
<DialogContent>
<Grid container>
<Grid spacing={2} md={8} xs={12} container>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("primaryColor")}
</Typography>
<TextField
value={theme.palette.primary.main}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
main: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
colors={[
"#4D4D4D",
"#999999",
"#FFFFFF",
"#f44336",
"#ff9800",
"#ffeb3b",
"#cddc39",
"#A4DD00",
"#00bcd4",
"#03a9f4",
"#AEA1FF",
"#FDA1FF",
"#333333",
"#808080",
"#cccccc",
"#ff5722",
"#ffc107",
"#FCC400",
"#8bc34a",
"#4caf50",
"#009688",
"#2196f3",
"#3f51b5",
"#e91e63",
"#000000",
"#666666",
"#B3B3B3",
"#9F0500",
"#C45100",
"#FB9E00",
"#808900",
"#194D33",
"#0C797D",
"#0062B1",
"#673ab7",
"#9c27b0",
]}
color={theme.palette.primary.main}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
main: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("secondaryColor")}
</Typography>
<TextField
value={theme.palette.secondary.main}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
main: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
colors={[
"#4D4D4D",
"#999999",
"#FFFFFF",
"#ff1744",
"#ff3d00",
"#ffeb3b",
"#cddc39",
"#A4DD00",
"#00bcd4",
"#00e5ff",
"#AEA1FF",
"#FDA1FF",
"#333333",
"#808080",
"#cccccc",
"#ff5722",
"#ffea00",
"#ffc400",
"#c6ff00",
"#00e676",
"#76ff03",
"#00b0ff",
"#2979ff",
"#f50057",
"#000000",
"#666666",
"#B3B3B3",
"#9F0500",
"#C45100",
"#FB9E00",
"#808900",
"#1de9b6",
"#0C797D",
"#3d5afe",
"#651fff",
"#d500f9",
]}
color={theme.palette.secondary.main}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
main: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("primaryColorText")}
</Typography>
<TextField
value={theme.palette.primary.contrastText}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
contrastText: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
color={theme.palette.primary.contrastText}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
contrastText: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
{t("secondaryColorText")}
</Typography>
<TextField
value={theme.palette.secondary.contrastText}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
contrastText: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
color={theme.palette.secondary.contrastText}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
contrastText: c.hex,
},
},
});
}}
/>
</div>
</Grid>
</Grid>
<Grid spacing={2} md={4} xs={12}>
<ThemeProvider theme={subTheme()}>
<div
className={classes.statusBar}
style={{
backgroundColor: subTheme().palette.primary
.dark,
}}
/>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<Menu />
</IconButton>
<Typography
variant="h6"
className={classes.title}
>
Color
</Typography>
</Toolbar>
</AppBar>
<div style={{ padding: 16 }}>
<TextField
fullWidth
color={"secondary"}
label={"Text input"}
/>
<div
className={classes.fab}
style={{ paddingTop: 64 }}
>
<Fab color="secondary" aria-label="add">
<Add />
</Fab>
</div>
</div>
</ThemeProvider>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tGlobal("cancel")}
</Button>
<Button onClick={() => onSubmit(theme)} color="primary">
{tGlobal("ok")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,134 @@
import React, { useCallback, useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import TextField from "@material-ui/core/TextField";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
export default function FileFilter({ setFilter, setSearch, open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "file" });
const { t: tDashboard } = useTranslation("dashboard");
const { t: tCommon } = useTranslation("common");
const [input, setInput] = useState({
policy_id: "all",
user_id: "",
});
const [policies, setPolicies] = useState([]);
const [keywords, setKeywords] = useState("");
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
setPolicies(response.data.items);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all" && input[v] !== "") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
name: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">
{tDashboard("user.filterCondition")}
</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
{t("storagePolicy")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.policy_id}
onChange={handleChange("policy_id")}
>
<MenuItem value={"all"}>
{tDashboard("user.all")}
</MenuItem>
{policies.map((v) => {
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={input.user_id}
onChange={handleChange("user_id")}
id="standard-basic"
label={t("uploaderID")}
/>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label={t("searchFileName")}
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button onClick={submit} color="primary">
{tDashboard("user.apply")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,180 @@
import React from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import TableBody from "@material-ui/core/TableBody";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { useTranslation } from "react-i18next";
const magicVars = [
{
value: "{randomkey16}",
des: "16digitsRandomString",
example: "N6IimT5XZP324ACK",
fileOnly: false,
},
{
value: "{randomkey8}",
des: "8digitsRandomString",
example: "gWz78q30",
fileOnly: false,
},
{
value: "{timestamp}",
des: "secondTimestamp",
example: "1582692933",
fileOnly: false,
},
{
value: "{timestamp_nano}",
des: "nanoTimestamp",
example: "1582692933231834600",
fileOnly: false,
},
{
value: "{uid}",
des: "uid",
example: "1",
fileOnly: false,
},
{
value: "{originname}",
des: "originalFileName",
example: "MyPico.mp4",
fileOnly: true,
},
{
value: "{originname_without_ext}",
des: "originFileNameNoext",
example: "MyPico",
fileOnly: true,
},
{
value: "{ext}",
des: "extension",
example: ".jpg",
fileOnly: true,
},
{
value: "{uuid}",
des: "uuidV4",
example: "31f0a770-659d-45bf-a5a9-166c06f33281",
fileOnly: true,
},
{
value: "{date}",
des: "date",
example: "20060102",
fileOnly: false,
},
{
value: "{datetime}",
des: "dateAndTime",
example: "20060102150405",
fileOnly: false,
},
{
value: "{year}",
des: "year",
example: "2006",
fileOnly: false,
},
{
value: "{month}",
des: "month",
example: "01",
fileOnly: false,
},
{
value: "{day}",
des: "day",
example: "02",
fileOnly: false,
},
{
value: "{hour}",
des: "hour",
example: "15",
fileOnly: false,
},
{
value: "{minute}",
des: "minute",
example: "04",
fileOnly: false,
},
{
value: "{second}",
des: "second",
example: "05",
fileOnly: false,
},
];
export default function MagicVar({ isFile, open, onClose, isSlave }) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy.magicVar" });
const { t: tCommon } = useTranslation("common");
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{isFile ? t("fileNameMagicVar") : t("pathMagicVar")}
</DialogTitle>
<DialogContent>
<TableContainer>
<Table size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<TableCell>{t("variable")}</TableCell>
<TableCell>{t("description")}</TableCell>
<TableCell>{t("example")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{magicVars.map((m) => {
if (!m.fileOnly || isFile) {
return (
<TableRow>
<TableCell
component="th"
scope="row"
>
{m.value}
</TableCell>
<TableCell>{t(m.des)}</TableCell>
<TableCell>{m.example}</TableCell>
</TableRow>
);
}
})}
{!isFile && (
<TableRow>
<TableCell component="th" scope="row">
{"{path}"}
</TableCell>
<TableCell>{t("userUploadPath")}</TableCell>
<TableCell>/MyFile/Documents/</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{tCommon("close")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,103 @@
import React, { useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import TextField from "@material-ui/core/TextField";
import { useTranslation } from "react-i18next";
export default function ShareFilter({ setFilter, setSearch, open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "share" });
const { t: tDashboard } = useTranslation("dashboard");
const { t: tCommon } = useTranslation("common");
const [input, setInput] = useState({
is_dir: "all",
user_id: "",
});
const [keywords, setKeywords] = useState("");
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all" && input[v] !== "") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
source_name: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">
{tDashboard("user.filterCondition")}
</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
{t("srcType")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.is_dir}
onChange={handleChange("is_dir")}
>
<MenuItem value={"all"}>
{tDashboard("user.all")}
</MenuItem>
<MenuItem value={"1"}>{t("folder")}</MenuItem>
<MenuItem value={"0"}>{t("file")}</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={input.user_id}
onChange={handleChange("user_id")}
id="standard-basic"
label={tDashboard("file.uploaderID")}
/>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label={tDashboard("file.searchFileName")}
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button onClick={submit} color="primary">
{tCommon("ok")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,139 @@
import React, { useCallback, useEffect, useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import TextField from "@material-ui/core/TextField";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
export default function UserFilter({ setFilter, setSearch, open, onClose }) {
const { t } = useTranslation("dashboard", { keyPrefix: "user" });
const { t: tCommon } = useTranslation("common");
const [input, setInput] = useState({
group_id: "all",
status: "all",
});
const [groups, setGroups] = useState([]);
const [keywords, setKeywords] = useState("");
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
useEffect(() => {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
nick: keywords,
email: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">
{t("filterCondition")}
</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
{t("group")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.group_id}
onChange={handleChange("group_id")}
>
<MenuItem value={"all"}>{t("all")}</MenuItem>
{groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<InputLabel id="demo-simple-select-label">
{t("userStatus")}
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.status}
onChange={handleChange("status")}
>
<MenuItem value={"all"}>{t("all")}</MenuItem>
<MenuItem value={"0"}>{t("active")}</MenuItem>
<MenuItem value={"1"}>{t("notActivated")}</MenuItem>
<MenuItem value={"2"}>{t("banned")}</MenuItem>
<MenuItem value={"3"}>{t("bannedBySys")}</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label={t("searchNickUserName")}
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
{tCommon("cancel")}
</Button>
<Button onClick={submit} color="primary">
{t("apply")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,502 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Delete, DeleteForever, FilterList, LinkOff } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Badge from "@material-ui/core/Badge";
import FileFilter from "../Dialogs/FileFilter";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import Chip from "@material-ui/core/Chip";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
disabledBadge: {
marginLeft: theme.spacing(1),
height: 18,
},
}));
export default function File() {
const { t } = useTranslation("dashboard", { keyPrefix: "file" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [files, setFiles] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/file/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setFiles(response.data.items);
setTotal(response.data.total);
setSelected([]);
setUsers(response.data.users);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deleteFile = (id, unlink = false) => {
setLoading(true);
API.post("/admin/file/delete", { id: [id], unlink })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleteAsync"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch =
(force, unlink = false) =>
() => {
setLoading(true);
API.post("/admin/file/delete", { id: selected, force, unlink })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleteAsync"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = files.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<FileFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/file/import")}
variant={"contained"}
style={{
alignSelf: "center",
}}
>
{t("import")}
</Button>
<div className={classes.headerRight}>
<Tooltip title={tDashboard("user.filter")}>
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch(false)}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip title={t("forceDelete")}>
<IconButton
onClick={deleteBatch(true)}
disabled={loading}
aria-label="delete"
>
<DeleteForever />
</IconButton>
</Tooltip>
<Tooltip title={tDashboard("file.unlink")}>
<IconButton
disabled={loading}
onClick={deleteBatch(true, true)}
size={"small"}
>
<LinkOff />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < files.length
}
checked={
files.length > 0 &&
selected.length === files.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 59 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 250 }}>
<TableSortLabel
active={orderBy[0] === "name"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"name",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("name")}
{orderBy[0] === "name" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
align={"right"}
style={{ minWidth: 70 }}
>
<TableSortLabel
active={orderBy[0] === "size"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"size",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("size")}
{orderBy[0] === "size" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
{t("uploader")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("createdAt")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{files.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>
<Link
target={"_blank"}
color="inherit"
href={
"/api/v3/admin/file/preview/" +
row.ID
}
>
{row.Name}
{row.UploadSessionID && (
<Chip
className={
classes.disabledBadge
}
size="small"
label={t("uploading")}
/>
)}
</Link>
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.Size)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: t("unknownUploader")}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(
row.CreatedAt,
"YYYY-MM-DD H:mm:ss"
)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deleteFile(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip
title={tDashboard("file.unlink")}
>
<IconButton
disabled={loading}
onClick={() =>
deleteFile(row.ID, true)
}
size={"small"}
>
<LinkOff />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,481 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Alert from "@material-ui/lab/Alert";
import Fade from "@material-ui/core/Fade";
import Paper from "@material-ui/core/Paper";
import Popper from "@material-ui/core/Popper";
import InputAdornment from "@material-ui/core/InputAdornment";
import Chip from "@material-ui/core/Chip";
import { Dialog } from "@material-ui/core";
import DialogTitle from "@material-ui/core/DialogTitle";
import PathSelector from "../../FileManager/PathSelector";
import DialogActions from "@material-ui/core/DialogActions";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
userSelect: {
width: 400,
borderRadius: 0,
},
}));
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value]);
return debouncedValue;
}
export default function Import() {
const { t } = useTranslation("dashboard", { keyPrefix: "file" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
policy: 1,
userInput: "",
src: "",
dst: "",
recursive: true,
});
const [anchorEl, setAnchorEl] = useState(null);
const [policies, setPolicies] = useState({});
const [users, setUsers] = useState([]);
const [user, setUser] = useState(null);
const [selectRemote, setSelectRemote] = useState(false);
const [selectLocal, setSelectLocal] = useState(false);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.checked,
});
};
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submit = (e) => {
e.preventDefault();
if (user === null) {
ToggleSnackbar("top", "right", t("selectTargetUser"), "warning");
return;
}
setLoading(true);
API.post("/admin/task/import", {
uid: user.ID,
policy_id: parseInt(options.policy),
src: options.src,
dst: options.dst,
recursive: options.recursive,
})
.then(() => {
setLoading(false);
history.push("/admin/file");
ToggleSnackbar(
"top",
"right",
t("importTaskCreated"),
"success"
);
})
.catch((error) => {
setLoading(false);
ToggleSnackbar("top", "right", error.message, "error");
});
};
const debouncedSearchTerm = useDebounce(options.userInput, 500);
useEffect(() => {
if (debouncedSearchTerm !== "") {
API.post("/admin/user/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
searches: {
nick: debouncedSearchTerm,
email: debouncedSearchTerm,
},
})
.then((response) => {
setUsers(response.data.items);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}
}, [debouncedSearchTerm]);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const selectUser = (u) => {
setOptions({
...options,
userInput: "",
});
setUser(u);
};
const setMoveTarget = (setter) => (folder) => {
const path =
folder.path === "/"
? folder.path + folder.name
: folder.path + "/" + folder.name;
setter(path === "//" ? "/" : path);
};
const openPathSelector = (isSrcSelect) => {
if (isSrcSelect) {
if (
!policies[options.policy] ||
policies[options.policy].Type === "local" ||
policies[options.policy].Type === "remote"
) {
ToggleSnackbar(
"top",
"right",
t("manuallyPathOnly"),
"warning"
);
return;
}
setSelectRemote(true);
} else {
if (user === null) {
ToggleSnackbar(
"top",
"right",
t("selectTargetUser"),
"warning"
);
return;
}
setSelectLocal(true);
}
};
return (
<div>
<Dialog
open={selectRemote}
onClose={() => setSelectRemote(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("selectFolder")}
</DialogTitle>
<PathSelector
presentPath="/"
api={"/admin/file/folders/policy/" + options.policy}
selected={[]}
onSelect={setMoveTarget((p) =>
setOptions({
...options,
src: p,
})
)}
/>
<DialogActions>
<Button
onClick={() => setSelectRemote(false)}
color="primary"
>
{tCommon("ok")}
</Button>
</DialogActions>
</Dialog>
<Dialog
open={selectLocal}
onClose={() => setSelectLocal(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("selectFolder")}
</DialogTitle>
<PathSelector
presentPath="/"
api={
"/admin/file/folders/user/" +
(user === null ? 0 : user.ID)
}
selected={[]}
onSelect={setMoveTarget((p) =>
setOptions({
...options,
dst: p,
})
)}
/>
<DialogActions>
<Button
onClick={() => setSelectLocal(false)}
color="primary"
>
{tCommon("ok")}
</Button>
</DialogActions>
</Dialog>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("importExternalFolder")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
{t("importExternalFolderDes")}
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("storagePolicy")}
</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
value={options.policy}
onChange={handleChange("policy")}
input={<Input id="select-multiple-chip" />}
>
{Object.keys(policies).map((pid) => (
<MenuItem key={pid} value={pid}>
{policies[pid].Name}
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{t("storagePolicyDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("targetUser")}
</InputLabel>
<Input
value={options.userInput}
onChange={(e) => {
handleChange("userInput")(e);
setAnchorEl(e.currentTarget);
}}
startAdornment={
user !== null && (
<InputAdornment position="start">
<Chip
size="small"
onDelete={() => {
setUser(null);
}}
label={user.Nick}
/>
</InputAdornment>
)
}
disabled={user !== null}
/>
<Popper
open={
options.userInput !== "" &&
users.length > 0
}
anchorEl={anchorEl}
placement={"bottom"}
transition
>
{({ TransitionProps }) => (
<Fade
{...TransitionProps}
timeout={350}
>
<Paper
className={classes.userSelect}
>
{users.map((u) => (
<MenuItem
key={u.ID}
onClick={() =>
selectUser(u)
}
>
{u.Nick}{" "}
{"<" + u.Email + ">"}
</MenuItem>
))}
</Paper>
</Fade>
)}
</Popper>
<FormHelperText id="component-helper-text">
{t("targetUserDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("srcFolderPath")}
</InputLabel>
<Input
value={options.src}
onChange={(e) => {
handleChange("src")(e);
setAnchorEl(e.currentTarget);
}}
required
endAdornment={
<Button
onClick={() =>
openPathSelector(true)
}
>
{t("select")}
</Button>
}
/>
<FormHelperText id="component-helper-text">
{t("selectSrcDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("dstFolderPath")}
</InputLabel>
<Input
value={options.dst}
onChange={(e) => {
handleChange("dst")(e);
setAnchorEl(e.currentTarget);
}}
required
endAdornment={
<Button
onClick={() =>
openPathSelector(false)
}
>
{t("select")}
</Button>
}
/>
<FormHelperText id="component-helper-text">
{t("dstFolderPathDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={options.recursive}
onChange={handleCheckChange(
"recursive"
)}
/>
}
label={t("recursivelyImport")}
/>
<FormHelperText id="component-helper-text">
{t("recursivelyImportDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("createImportTask")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import GroupForm from "./GroupForm";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
export default function EditGroupPreload() {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const [group, setGroup] = useState({});
const { id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setGroup({});
API.get("/admin/group/" + id)
.then((response) => {
// 布尔值转换
["ShareEnabled", "WebDAVEnabled"].forEach((v) => {
response.data[v] = response.data[v] ? "true" : "false";
});
[
"archive_download",
"archive_task",
"relocate",
"one_time_download",
"share_download",
"webdav_proxy",
"share_free",
"aria2",
"redirected_source",
"advance_delete",
"select_node",
].forEach((v) => {
if (response.data.OptionsSerialized[v] !== undefined) {
response.data.OptionsSerialized[v] = response.data
.OptionsSerialized[v]
? "true"
: "false";
}
});
// 整型转换
["MaxStorage", "SpeedLimit"].forEach((v) => {
response.data[v] = response.data[v].toString();
});
[
"compress_size",
"decompress_size",
"source_batch",
"aria2_batch",
].forEach((v) => {
if (response.data.OptionsSerialized[v] !== undefined) {
response.data.OptionsSerialized[v] =
response.data.OptionsSerialized[v].toString();
}
});
response.data.PolicyList = response.data.PolicyList.map((v) => {
return v.toString();
});
response.data.OptionsSerialized.available_nodes = response.data
.OptionsSerialized.available_nodes
? response.data.OptionsSerialized.available_nodes.map(
(v) => {
return v.toString();
}
)
: [];
// JSON转换
if (
response.data.OptionsSerialized.aria2_options === undefined
) {
response.data.OptionsSerialized.aria2_options = "{}";
} else {
try {
response.data.OptionsSerialized.aria2_options =
JSON.stringify(
response.data.OptionsSerialized.aria2_options
);
} catch (e) {
ToggleSnackbar(
"top",
"right",
t("aria2FormatError"),
"warning"
);
return;
}
}
setGroup(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return <div>{group.ID !== undefined && <GroupForm group={group} />}</div>;
}

View File

@@ -0,0 +1,252 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory, useLocation } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Delete, Edit } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
}));
const columns = [
{ id: "#", minWidth: 50 },
{ id: "name", minWidth: 170 },
{ id: "type", label: "存储策略", minWidth: 170 },
{
id: "count",
minWidth: 50,
align: "right",
},
{
id: "size",
minWidth: 100,
align: "right",
},
{
id: "action",
minWidth: 170,
align: "right",
},
];
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export default function Group() {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [groups, setGroups] = useState([]);
const [statics, setStatics] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [policies, setPolicies] = React.useState({});
const location = useLocation();
const history = useHistory();
const query = useQuery();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/group/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
})
.then((response) => {
setGroups(response.data.items);
setStatics(response.data.statics);
setTotal(response.data.total);
setPolicies(response.data.policies);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
if (query.get("code") === "0") {
ToggleSnackbar("top", "right", "授权成功", "success");
} else if (query.get("msg") && query.get("msg") !== "") {
ToggleSnackbar(
"top",
"right",
query.get("msg") + ", " + query.get("err"),
"warning"
);
}
}, [location]);
useEffect(() => {
loadList();
}, [page, pageSize]);
const deletePolicy = (id) => {
API.delete("/admin/group/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
return (
<div>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/group/add")}
variant={"contained"}
>
{t("new")}
</Button>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{t(column.id)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{groups.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>
{row.PolicyList !== null &&
row.PolicyList.map((pid, key) => {
let res = "";
if (policies[pid]) {
res += policies[pid].Name;
}
if (
key !==
row.PolicyList.length - 1
) {
res += " / ";
}
return res;
})}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
statics[row.ID].toLocaleString()}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
sizeToString(row.MaxStorage)}
</TableCell>
<TableCell align={"right"}>
<Tooltip
title={tDashboard("policy.edit")}
>
<IconButton
onClick={() =>
history.push(
"/admin/group/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,825 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Chip from "@material-ui/core/Chip";
import SizeInput from "../Common/SizeInput";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import Collapse from "@material-ui/core/Collapse";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "@material-ui/core";
import { getSelectItemStyles } from "../../../utils";
import NodeSelector from "./NodeSelector";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function GroupForm(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [group, setGroup] = useState(
props.group
? props.group
: {
ID: 0,
Name: "",
MaxStorage: "1073741824", // 转换类型
ShareEnabled: "true", // 转换类型
WebDAVEnabled: "true", // 转换类型
SpeedLimit: "0", // 转换类型
PolicyList: ["1"], // 转换类型,至少选择一个
OptionsSerialized: {
// 批量转换类型
share_download: "true",
aria2_options: "{}", // json decode
compress_size: "0",
decompress_size: "0",
source_batch: "0",
aria2_batch: "1",
available_nodes: [],
},
}
);
const [policies, setPolicies] = useState({});
const theme = useTheme();
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v.Name;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const handleChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "true" : "false";
setGroup({
...group,
[name]: value,
});
};
const handleOptionCheckChange = (name) => (event) => {
const value = event.target.checked ? "true" : "false";
setGroup({
...group,
OptionsSerialized: {
...group.OptionsSerialized,
[name]: value,
},
});
};
const handleOptionChange = (name) => (event) => {
setGroup({
...group,
OptionsSerialized: {
...group.OptionsSerialized,
[name]: event.target.value,
},
});
};
const submit = (e) => {
e.preventDefault();
const groupCopy = {
...group,
OptionsSerialized: { ...group.OptionsSerialized },
};
// 布尔值转换
["ShareEnabled", "WebDAVEnabled"].forEach((v) => {
groupCopy[v] = groupCopy[v] === "true";
});
[
"archive_download",
"archive_task",
"relocate",
"one_time_download",
"share_download",
"webdav_proxy",
"share_free",
"aria2",
"redirected_source",
"advance_delete",
"select_node",
].forEach((v) => {
if (groupCopy.OptionsSerialized[v] !== undefined) {
groupCopy.OptionsSerialized[v] =
groupCopy.OptionsSerialized[v] === "true";
}
});
// 整型转换
["MaxStorage", "SpeedLimit"].forEach((v) => {
groupCopy[v] = parseInt(groupCopy[v]);
});
[
"compress_size",
"decompress_size",
"source_batch",
"aria2_batch",
].forEach((v) => {
if (groupCopy.OptionsSerialized[v] !== undefined) {
groupCopy.OptionsSerialized[v] = parseInt(
groupCopy.OptionsSerialized[v]
);
}
});
groupCopy.PolicyList = groupCopy.PolicyList.map((v) => {
return parseInt(v);
});
groupCopy.OptionsSerialized.available_nodes =
groupCopy.OptionsSerialized.available_nodes.map((v) => {
return parseInt(v);
});
if (groupCopy.PolicyList.length < 1 && groupCopy.ID !== 3) {
ToggleSnackbar("top", "right", t("atLeastOnePolicy"), "warning");
return;
}
// JSON转换
try {
groupCopy.OptionsSerialized.aria2_options = JSON.parse(
groupCopy.OptionsSerialized.aria2_options
);
} catch (e) {
ToggleSnackbar("top", "right", t("aria2FormatError"), "warning");
return;
}
setLoading(true);
API.post("/admin/group", {
group: groupCopy,
})
.then(() => {
history.push("/admin/group");
ToggleSnackbar(
"top",
"right",
props.group ? t("saved") : t("added"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{group.ID === 0 && t("new")}
{group.ID !== 0 &&
t("editGroup", { group: group.Name })}
</Typography>
<div className={classes.formContainer}>
{group.ID !== 3 && (
<>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("nameOfGroup")}
</InputLabel>
<Input
value={group.Name}
onChange={handleChange("Name")}
required
/>
<FormHelperText id="component-helper-text">
{t("nameOfGroupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("availablePolicies")}
</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
multiple
value={group.PolicyList}
onChange={handleChange(
"PolicyList"
)}
input={
<Input id="select-multiple-chip" />
}
renderValue={(selected) => (
<div>
{selected.map((value) => (
<Chip
style={{
margin: 2,
}}
key={value}
size={"small"}
label={
policies[value]
}
className={
classes.chip
}
/>
))}
</div>
)}
>
{Object.keys(policies).map(
(pid) => (
<MenuItem
key={pid}
value={pid}
style={getSelectItemStyles(
pid,
group.PolicyList,
theme
)}
>
{policies[pid]}
</MenuItem>
)
)}
</Select>
<FormHelperText id="component-helper-text">
{t("availablePoliciesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={group.MaxStorage}
onChange={handleChange(
"MaxStorage"
)}
min={0}
max={9223372036854775807}
label={t("initialStorageQuota")}
required
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("initialStorageQuotaDes")}
</FormHelperText>
</div>
</>
)}
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={group.SpeedLimit}
onChange={handleChange("SpeedLimit")}
min={0}
max={9223372036854775807}
label={t("downloadSpeedLimit")}
suffix={"/s"}
required
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("downloadSpeedLimitDes")}
</FormHelperText>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("bathSourceLinkLimit")}
</InputLabel>
<Input
multiline
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={
group.OptionsSerialized.source_batch
}
onChange={handleOptionChange(
"source_batch"
)}
/>
<FormHelperText id="component-helper-text">
{t("bathSourceLinkLimitDes")}
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.ShareEnabled ===
"true"
}
onChange={handleCheckChange(
"ShareEnabled"
)}
/>
}
label={t("allowCreateShareLink")}
/>
<FormHelperText id="component-helper-text">
{t("allowCreateShareLinkDes")}
</FormHelperText>
</FormControl>
</div>
)}
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.share_download === "true"
}
onChange={handleOptionCheckChange(
"share_download"
)}
/>
}
label={t("allowDownloadShare")}
/>
<FormHelperText id="component-helper-text">
{t("allowDownloadShareDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.share_free === "true"
}
onChange={handleOptionCheckChange(
"share_free"
)}
/>
}
label={tVas("freeDownload")}
/>
<FormHelperText id="component-helper-text">
{tVas("freeDownloadDes")}
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.WebDAVEnabled ===
"true"
}
onChange={handleCheckChange(
"WebDAVEnabled"
)}
/>
}
label={t("allowWabDAV")}
/>
<FormHelperText id="component-helper-text">
{t("allowWabDAVDes")}
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && group.WebDAVEnabled === "true" && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized.webdav_proxy ===
"true"
}
onChange={handleOptionCheckChange(
"webdav_proxy"
)}
/>
}
label={t("allowWabDAVProxy")}
/>
<FormHelperText id="component-helper-text">
{t("allowWabDAVProxyDes")}
</FormHelperText>
</FormControl>
</div>
)}
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.one_time_download ===
"true"
}
onChange={handleOptionCheckChange(
"one_time_download"
)}
/>
}
label={t("disableMultipleDownload")}
/>
<FormHelperText id="component-helper-text">
{t("disableMultipleDownloadDes")}
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.aria2 === "true"
}
onChange={handleOptionCheckChange(
"aria2"
)}
/>
}
label={t("allowRemoteDownload")}
/>
<FormHelperText id="component-helper-text">
{t("allowRemoteDownloadDes")}
</FormHelperText>
</FormControl>
</div>
)}
<Collapse in={group.OptionsSerialized.aria2 === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("aria2Options")}
</InputLabel>
<Input
multiline
value={
group.OptionsSerialized
.aria2_options
}
onChange={handleOptionChange(
"aria2_options"
)}
/>
<FormHelperText id="component-helper-text">
{t("aria2OptionsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("aria2BatchSize")}
</InputLabel>
<Input
multiline
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={
group.OptionsSerialized.aria2_batch
}
onChange={handleOptionChange(
"aria2_batch"
)}
/>
<FormHelperText id="component-helper-text">
{t("aria2BatchSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("availableNodes")}
</InputLabel>
<NodeSelector
selected={
group.OptionsSerialized
.available_nodes
}
handleChange={handleOptionChange(
"available_nodes"
)}
/>
<FormHelperText id="component-helper-text">
{t("availableNodesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.select_node === "true"
}
onChange={handleOptionCheckChange(
"select_node"
)}
/>
}
label={t("allowSelectNode")}
/>
<FormHelperText id="component-helper-text">
{t("allowSelectNodeDes")}
</FormHelperText>
</FormControl>
</div>
</Collapse>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.archive_download === "true"
}
onChange={handleOptionCheckChange(
"archive_download"
)}
/>
}
label={t("serverSideBatchDownload")}
/>
<FormHelperText id="component-helper-text">
{t("serverSideBatchDownloadDes")}
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.archive_task === "true"
}
onChange={handleOptionCheckChange(
"archive_task"
)}
/>
}
label={t("compressTask")}
/>
<FormHelperText id="component-helper-text">
{t("compressTaskDes")}
</FormHelperText>
</FormControl>
</div>
)}
<Collapse
in={group.OptionsSerialized.archive_task === "true"}
>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={
group.OptionsSerialized
.compress_size
}
onChange={handleOptionChange(
"compress_size"
)}
min={0}
max={9223372036854775807}
label={t("compressSize")}
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("compressSizeDes")}
</FormHelperText>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={
group.OptionsSerialized
.decompress_size
}
onChange={handleOptionChange(
"decompress_size"
)}
min={0}
max={9223372036854775807}
label={t("decompressSize")}
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("decompressSizeDes")}
</FormHelperText>
</div>
</Collapse>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.relocate === "true"
}
onChange={handleOptionCheckChange(
"relocate"
)}
/>
}
label={t("migratePolicy")}
/>
<FormHelperText id="component-helper-text">
{t("migratePolicyDes")}
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.redirected_source ===
"true"
}
onChange={handleOptionCheckChange(
"redirected_source"
)}
/>
}
label={t("redirectedSource")}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"group.redirectedSourceDes"
}
components={[
<Link
href={tDashboard(
"policy.comparesStoragePoliciesLink"
)}
key={0}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
)}
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.advance_delete ===
"true"
}
onChange={handleOptionCheckChange(
"advance_delete"
)}
/>
}
label={t("advanceDelete")}
/>
<FormHelperText id="component-helper-text">
{t("advanceDeleteDes")}
</FormHelperText>
</FormControl>
</div>
)}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("settings.save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,78 @@
import React, { useCallback, useEffect, useState } from "react";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
import Input from "@material-ui/core/Input";
import Chip from "@material-ui/core/Chip";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { getSelectItemStyles } from "../../../utils";
import { useTheme } from "@material-ui/core/styles";
export default function NodeSelector({ selected, handleChange }) {
const { t } = useTranslation("dashboard", { keyPrefix: "group" });
const [nodes, setNodes] = useState({});
const theme = useTheme();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/node/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v.Name;
});
setNodes(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
return (
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
multiple
value={selected}
onChange={handleChange}
input={<Input id="select-multiple-chip" />}
renderValue={(selected) => (
<div>
{selected.map((value) => (
<Chip
style={{
margin: 2,
}}
key={value}
size={"small"}
label={nodes[value]}
/>
))}
</div>
)}
>
{Object.keys(nodes).map((pid) => (
<MenuItem
key={pid}
value={pid}
style={getSelectItemStyles(pid, selected, theme)}
>
{nodes[pid]}
</MenuItem>
))}
</Select>
);
}

View File

@@ -0,0 +1,527 @@
import React, { useCallback, useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper";
import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { ResponsiveContainer } from "recharts/lib/component/ResponsiveContainer";
import { makeStyles } from "@material-ui/core/styles";
import pathHelper from "../../utils/page";
import API from "../../middleware/Api";
import { useDispatch } from "react-redux";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemAvatar from "@material-ui/core/ListItemAvatar";
import Avatar from "@material-ui/core/Avatar";
import {
Description,
Favorite,
FileCopy,
Forum,
GitHub,
Home,
Launch,
Lock,
People,
Public,
Telegram,
} from "@material-ui/icons";
import ListItemText from "@material-ui/core/ListItemText";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import { blue, green, red, yellow } from "@material-ui/core/colors";
import axios from "axios";
import TimeAgo from "timeago-react";
import Chip from "@material-ui/core/Chip";
import DialogTitle from "@material-ui/core/DialogTitle";
import Dialog from "@material-ui/core/Dialog";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import { toggleSnackbar } from "../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
paper: {
padding: theme.spacing(3),
height: "100%",
},
logo: {
width: 70,
},
logoContainer: {
padding: theme.spacing(3),
display: "flex",
},
title: {
marginLeft: 16,
},
cloudreve: {
fontSize: 25,
color: theme.palette.text.secondary,
},
version: {
color: theme.palette.text.hint,
},
links: {
padding: theme.spacing(3),
},
iconRight: {
minWidth: 0,
},
userIcon: {
backgroundColor: blue[100],
color: blue[600],
},
fileIcon: {
backgroundColor: yellow[100],
color: yellow[800],
},
publicIcon: {
backgroundColor: green[100],
color: green[800],
},
secretIcon: {
backgroundColor: red[100],
color: red[800],
},
}));
export default function Index() {
const { t } = useTranslation("dashboard");
const classes = useStyles();
const [lineData, setLineData] = useState([]);
// const [news, setNews] = useState([]);
// const [newsUsers, setNewsUsers] = useState({});
const [open, setOpen] = React.useState(false);
const [siteURL, setSiteURL] = React.useState("");
const [statistics, setStatistics] = useState({
fileTotal: 0,
userTotal: 0,
publicShareTotal: 0,
secretShareTotal: 0,
});
const [version, setVersion] = useState({
backend: "-",
});
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const ResetSiteURL = () => {
setOpen(false);
API.patch("/admin/setting", {
options: [
{
key: "siteURL",
value: window.location.origin,
},
],
})
.then(() => {
setSiteURL(window.location.origin);
ToggleSnackbar("top", "right", t("settings.saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
API.get("/admin/summary")
.then((response) => {
const data = [];
response.data.date.forEach((v, k) => {
data.push({
name: v,
file: response.data.files[k],
user: response.data.users[k],
share: response.data.shares[k],
});
});
setLineData(data);
setStatistics({
fileTotal: response.data.fileTotal,
userTotal: response.data.userTotal,
publicShareTotal: response.data.publicShareTotal,
secretShareTotal: response.data.secretShareTotal,
});
setVersion(response.data.version);
setSiteURL(response.data.siteURL);
if (
response.data.siteURL === "" ||
response.data.siteURL !== window.location.origin
) {
setOpen(true);
}
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// axios
// .get("/api/v3/admin/news?tag=" + t("summary.newsTag"))
// .then((response) => {
// setNews(response.data.data);
// const res = {};
// response.data.included.forEach((v) => {
// if (v.type === "users") {
// res[v.id] = v.attributes;
// }
// });
// setNewsUsers(res);
// })
// .catch((error) => {
// ToggleSnackbar(
// "top",
// "right",
// t("summary.newsletterError"),
// "warning"
// );
// });
}, []);
return (
<Grid container spacing={3}>
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{t("summary.confirmSiteURLTitle")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography>
{siteURL === "" &&
t("summary.siteURLNotSet", {
current: window.location.origin,
})}
{siteURL !== "" &&
t("summary.siteURLNotMatch", {
current: window.location.origin,
})}
</Typography>
<Typography>
{t("summary.siteURLDescription")}
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color="default">
{t("summary.ignore")}
</Button>
<Button onClick={() => ResetSiteURL()} color="primary">
{t("summary.changeIt")}
</Button>
</DialogActions>
</Dialog>
<Grid alignContent={"stretch"} item xs={12} md={8} lg={9}>
<Paper className={classes.paper}>
<Typography variant="button" display="block" gutterBottom>
{t("summary.trend")}
</Typography>
<ResponsiveContainer
width="100%"
aspect={pathHelper.isMobile() ? 4.0 / 3.0 : 3.0 / 1.0}
>
<LineChart width={1200} height={300} data={lineData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Line
name={t("nav.files")}
type="monotone"
dataKey="file"
stroke="#3f51b5"
/>
<Line
name={t("nav.users")}
type="monotone"
dataKey="user"
stroke="#82ca9d"
/>
<Line
name={t("nav.shares")}
type="monotone"
dataKey="share"
stroke="#e91e63"
/>
</LineChart>
</ResponsiveContainer>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={3}>
<Paper className={classes.paper}>
<Typography variant="button" display="block" gutterBottom>
{t("summary.summary")}
</Typography>
<Divider />
<List className={classes.root}>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.userIcon}>
<People />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.userTotal}
secondary={t("summary.totalUsers")}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.fileIcon}>
<FileCopy />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.fileTotal}
secondary={t("summary.totalFiles")}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.publicIcon}>
<Public />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.publicShareTotal}
secondary={t("summary.publicShares")}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.secretIcon}>
<Lock />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.secretShareTotal}
secondary={t("summary.privateShares")}
/>
</ListItem>
</List>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={3}>
<Paper>
<div className={classes.logoContainer}>
<img
alt="cloudreve"
className={classes.logo}
src={"/static/img/cloudreve.svg"}
/>
<div className={classes.title}>
<Typography className={classes.cloudreve}>
Cloudreve
</Typography>
<Typography className={classes.version}>
{version.backend}{" "}
{version.is_plus === "true" && (
<Chip size="small" label="Plus" />
)}
</Typography>
</div>
</div>
<Divider />
<div>
<List component="nav" aria-label="main mailbox folders">
<ListItem
button
onClick={() =>
window.open("https://cloudreve.org")
}
>
<ListItemIcon>
<Home />
</ListItemIcon>
<ListItemText primary={t("summary.homepage")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(
"https://github.com/cloudreve/cloudreve"
)
}
>
<ListItemIcon>
<GitHub />
</ListItemIcon>
<ListItemText primary={t("summary.github")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open("https://docs.cloudreve.org/")
}
>
<ListItemIcon>
<Description />
</ListItemIcon>
<ListItemText
primary={t("summary.documents")}
/>
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(t("summary.forumLink"))
}
>
<ListItemIcon>
<Forum />
</ListItemIcon>
<ListItemText primary={t("summary.forum")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(t("summary.telegramGroupLink"))
}
>
<ListItemIcon>
<Telegram />
</ListItemIcon>
<ListItemText
primary={t("summary.telegramGroup")}
/>
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open("https://docs.cloudreve.org/use/pro/jie-shao")
}
>
<ListItemIcon style={{ color: "#ff789d" }}>
<Favorite />
</ListItemIcon>
<ListItemText primary={t("summary.buyPro")} />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
</List>
</div>
</Paper>
</Grid>
{/* <Grid item xs={12} md={8} lg={9}>
<Paper className={classes.paper}>
<List>
{news &&
news.map((v) => (
<>
<ListItem
button
alignItems="flex-start"
onClick={() =>
window.open(
"https://forum.cloudreve.org/d/" +
v.id
)
}
>
<ListItemAvatar>
<Avatar
alt="Travis Howard"
src={
newsUsers[
v.relationships
.startUser.data.id
] &&
newsUsers[
v.relationships
.startUser.data.id
].avatarUrl
}
/>
</ListItemAvatar>
<ListItemText
primary={v.attributes.title}
secondary={
<React.Fragment>
<Typography
component="span"
variant="body2"
className={
classes.inline
}
color="textPrimary"
>
{newsUsers[
v.relationships
.startUser.data
.id
] &&
newsUsers[
v.relationships
.startUser
.data.id
].username}{" "}
</Typography>
<Trans
ns={"dashboard"}
i18nKey="summary.publishedAt"
components={[
<TimeAgo
key={0}
datetime={
v.attributes
.startTime
}
locale={t(
"timeAgoLocaleCode",
{
ns:
"common",
}
)}
/>,
]}
/>
</React.Fragment>
}
/>
</ListItem>
<Divider />
</>
))}
</List>
</Paper>
</Grid> */}
</Grid>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import NodeGuide from "./Guide/NodeGuide";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function AddNode() {
const classes = useStyles();
return (
<div>
<Paper square className={classes.content}>
<NodeGuide />
</Paper>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import NodeGuide from "./Guide/NodeGuide";
import { useParams } from "react-router";
import { useDispatch } from "react-redux";
import API from "../../../middleware/Api";
import { toggleSnackbar } from "../../../redux/explorer";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function EditNode() {
const classes = useStyles();
const { id } = useParams();
const [node, setNode] = useState(null);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.get("/admin/node/" + id)
.then((response) => {
response.data.Rank = response.data.Rank.toString();
response.data.Aria2OptionsSerialized.interval = response.data.Aria2OptionsSerialized.interval.toString();
response.data.Aria2OptionsSerialized.timeout = response.data.Aria2OptionsSerialized.timeout.toString();
response.data.Aria2Enabled = response.data.Aria2Enabled
? "true"
: "false";
setNode(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return (
<div>
<Paper square className={classes.content}>
{node && <NodeGuide node={node} />}
</Paper>
</div>
);
}

View File

@@ -0,0 +1,454 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Collapse from "@material-ui/core/Collapse";
import Button from "@material-ui/core/Button";
import Alert from "@material-ui/lab/Alert";
import Box from "@material-ui/core/Box";
import FormHelperText from "@material-ui/core/FormHelperText";
import API from "../../../../middleware/Api";
import { toggleSnackbar } from "../../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Aria2RPC(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const testAria2 = () => {
setLoading(true);
API.post("/admin/node/aria2/test", {
type: props.node.Type,
server: props.node.Server,
secret: props.node.SlaveKey,
rpc: props.node.Aria2OptionsSerialized.server,
token: props.node.Aria2OptionsSerialized.token,
})
.then((response) => {
ToggleSnackbar(
"top",
"right",
t("ariaSuccess", { version: response.data }),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const mode = props.node.Type === 0 ? t("slave") : t("master");
return (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
props.onSubmit(e);
}}
>
<Alert severity="info" style={{ marginBottom: 10 }}>
<Typography variant="body2">
<Trans
ns={"dashboard"}
i18nKey={"node.aria2Des"}
values={{
mode: mode,
}}
components={[
<Link
href={"https://aria2.github.io/"}
target={"_blank"}
key={0}
/>,
<Box
component="span"
fontWeight="fontWeightBold"
key={1}
/>,
<Link
href={t("aria2DocURL")}
target={"_blank"}
key={2}
/>,
]}
/>
</Typography>
</Alert>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{props.node.Type === 0
? t("slaveTakeOverRemoteDownload")
: t("masterTakeOverRemoteDownload")}
<br />
{props.node.Type === 0
? t("routeTaskSlave")
: t("routeTaskMaster")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={props.node.Aria2Enabled}
onChange={props.handleTextChange(
"Aria2Enabled"
)}
row
>
<FormControlLabel
value={"true"}
control={<Radio color={"primary"} />}
label={t("enable")}
/>
<FormControlLabel
value={"false"}
control={<Radio color={"primary"} />}
label={t("disable")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={props.node.Aria2Enabled === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("aria2ConfigDes", {
target:
props.node.Type === 0
? t("slaveNodeTarget")
: t("masterNodeTarget"),
})}
</Typography>
<pre>
# {t("enableRPCComment")}
<br />
enable-rpc=true
<br /># {t("rpcPortComment")}
<br />
rpc-listen-port=6800
<br /># {t("rpcSecretComment")}
<br />
rpc-secret=
{props.node.Aria2OptionsSerialized.token}
<br />
</pre>
<Alert severity="info" style={{ marginBottom: 10 }}>
<Typography variant="body2">
{t("rpcConfigDes")}
</Typography>
</Alert>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"node.rpcServerDes"}
values={{
mode: mode,
}}
components={[
<code key={0} />,
<code key={1} />,
<code key={2} />,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("rpcServer")}
</InputLabel>
<Input
required
type={"url"}
value={
props.node.Aria2OptionsSerialized.server
}
onChange={props.handleOptionChange(
"server"
)}
/>
<FormHelperText id="component-helper-text">
{t("rpcServerHelpDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"node.rpcTokenDes"}
components={[<code key={0} />]}
/>
</Typography>
<div className={classes.form}>
<Input
value={props.node.Aria2OptionsSerialized.token}
onChange={props.handleOptionChange("token")}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"node.aria2PathDes"}
components={[<strong key={0} />]}
/>
</Typography>
<div className={classes.form}>
<Input
value={
props.node.Aria2OptionsSerialized.temp_path
}
onChange={props.handleOptionChange("temp_path")}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("aria2SettingDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("refreshInterval")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
required
value={
props.node.Aria2OptionsSerialized
.interval
}
onChange={props.handleOptionChange(
"interval"
)}
/>
<FormHelperText id="component-helper-text">
{t("refreshIntervalDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("rpcTimeout")}
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
required
value={
props.node.Aria2OptionsSerialized
.timeout
}
onChange={props.handleOptionChange(
"timeout"
)}
/>
<FormHelperText id="component-helper-text">
{t("rpcTimeoutDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("globalOptions")}
</InputLabel>
<Input
multiline
required
value={
props.node.Aria2OptionsSerialized
.options
}
onChange={props.handleOptionChange(
"options"
)}
/>
<FormHelperText id="component-helper-text">
{t("globalOptionsDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>6</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("testAria2Des", { mode })}
{props.node.Type === 0 &&
t("testAria2DesSlaveAddition")}
</Typography>
<div className={classes.form}>
<Button
disabled={loading}
variant={"outlined"}
onClick={() => testAria2()}
color={"primary"}
>
{t("testAria2")}
</Button>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
{props.activeStep !== 0 && (
<Button
color={"default"}
className={classes.button}
onClick={props.onBack}
>
{tDashboard("policy.back")}
</Button>
)}
<Button
disabled={loading || props.loading}
type={"submit"}
variant={"contained"}
color={"primary"}
onClick={props.onSubmit}
>
{tDashboard("policy.next")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,305 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import Alert from "@material-ui/lab/Alert";
import Box from "@material-ui/core/Box";
import { toggleSnackbar } from "../../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Communication(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const testSlave = () => {
setLoading(true);
// 测试路径是否可用
API.post("/admin/policy/test/slave", {
server: props.node.Server,
secret: props.node.SlaveKey,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
tDashboard("policy.communicationOK"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
props.onSubmit(e);
}}
>
<Alert severity="info" style={{ marginBottom: 10 }}>
<Trans
ns={"dashboard"}
i18nKey={"node.slaveNodeDes"}
components={[<Box key={0} fontWeight="fontWeightBold" />]}
/>
</Alert>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.remoteCopyBinaryDescription")}
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.remoteSecretDescription")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tDashboard("policy.remoteSecret")}
</InputLabel>
<Input
required
inputProps={{
minlength: 64,
}}
value={props.node.SlaveKey}
onChange={props.handleTextChange("SlaveKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.modifyRemoteConfig")}
<br />
<Trans
ns={"dashboard"}
i18nKey={"policy.addRemoteConfigDes"}
components={[<code key={0} />]}
/>
</Typography>
<pre>
[System]
<br />
Mode = slave
<br />
Listen = :5212
<br />
<br />
[Slave]
<br />
Secret = {props.node.SlaveKey}
<br />
<br />
<Trans
ns={"dashboard"}
i18nKey={"node.overwriteDes"}
components={[<br key={0} />, <br key={1} />]}
/>
<br />
[OptionOverwrite]
<br />; {t("workerNumDes")}
<br />
max_worker_num = 50
<br />; {t("parallelTransferDes")}
<br />
max_parallel_transfer = 10
<br />; {t("chunkRetriesDes")}
<br />
chunk_retries = 10
</pre>
<Typography variant={"body2"}>
{tDashboard("policy.remoteConfigDifference")}
<ul>
<li>
<Trans
ns={"dashboard"}
i18nKey={"policy.remoteConfigDifference1"}
components={[
<code key={0} />,
<code key={1} />,
<code key={2} />,
]}
/>
</li>
<li>
<Trans
ns={"dashboard"}
i18nKey={"policy.remoteConfigDifference2"}
components={[
<code key={0} />,
<code key={1} />,
]}
/>
</li>
</ul>
{t("multipleMasterDes")}
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.inputRemoteAddress")}
<br />
{tDashboard("policy.inputRemoteAddressDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tDashboard("policy.remoteAddress")}
</InputLabel>
<Input
fullWidth
required
type={"url"}
value={props.node.Server}
onChange={props.handleTextChange("Server")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{tDashboard("policy.testCommunicationDes")}
</Typography>
<div className={classes.form}>
<Button
disabled={loading}
variant={"outlined"}
onClick={() => testSlave()}
color={"primary"}
>
{tDashboard("policy.testCommunication")}
</Button>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading || props.loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("policy.next")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,98 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React from "react";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import { useHistory } from "react-router";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Completed(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const classes = useStyles();
const history = useHistory();
return (
<form className={classes.stepContent}>
<Typography>{t("nodeSaved")}</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("nodeSavedFutureAction")}
</Typography>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/node")}
>
{t("backToNodeList")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,157 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import Button from "@material-ui/core/Button";
import { toggleSnackbar } from "../../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
viewButtonLabel: { textTransform: "none" },
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
},
},
}));
export default function Metainfo(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
return (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
props.onSubmit(e);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>{t("nameNode")}</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<Input
required
value={props.node.Name}
onChange={props.handleTextChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("loadBalancerRankDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("loadBalancerRank")}
</InputLabel>
<Input
type={"number"}
required
inputProps={{
step: 1,
min: 0,
}}
value={props.node.Rank}
onChange={props.handleTextChange("Rank")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading || props.loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("policy.next")}
</Button>
</div>
</form>
);
}

View File

@@ -0,0 +1,191 @@
import React, { useCallback, useMemo, useState } from "react";
import Stepper from "@material-ui/core/Stepper";
import StepLabel from "@material-ui/core/StepLabel";
import Step from "@material-ui/core/Step";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import { randomStr } from "../../../../utils";
import Communication from "./Communication";
import Aria2RPC from "./Aria2RPC";
import API from "../../../../middleware/Api";
import Metainfo from "./Metainfo";
import Completed from "./Completed";
import { toggleSnackbar } from "../../../../redux/explorer";
import { useTranslation } from "react-i18next";
const steps = [
{
slaveOnly: true,
title: "communication",
optional: false,
component: function show(p) {
return <Communication {...p} />;
},
},
{
slaveOnly: false,
title: "remoteDownload",
optional: false,
component: function show(p) {
return <Aria2RPC {...p} />;
},
},
{
slaveOnly: false,
title: "otherSettings",
optional: false,
component: function show(p) {
return <Metainfo {...p} />;
},
},
{
slaveOnly: false,
title: "finish",
optional: false,
component: function show(p) {
return <Completed {...p} />;
},
},
];
export default function NodeGuide(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const { t: tDashboard } = useTranslation("dashboard");
const [activeStep, setActiveStep] = useState(0);
const [skipped, setSkipped] = React.useState(new Set());
const [loading, setLoading] = useState(false);
const [node, setNode] = useState(
props.node
? props.node
: {
Status: 1,
Type: 0,
Aria2Enabled: "false",
Server: "https://example.com:5212",
SlaveKey: randomStr(64),
MasterKey: randomStr(64),
Rank: "0",
Aria2OptionsSerialized: {
token: randomStr(32),
options: "{}",
interval: "10",
timeout: "10",
},
}
);
const usedSteps = useMemo(() => {
return steps.filter((step) => !(step.slaveOnly && node.Type === 1));
}, [node.Type]);
const isStepSkipped = (step) => {
return skipped.has(step);
};
const handleTextChange = (name) => (event) => {
setNode({
...node,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setNode({
...node,
Aria2OptionsSerialized: {
...node.Aria2OptionsSerialized,
[name]: event.target.value,
},
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const nextStep = () => {
if (props.node || activeStep + 1 === steps.length - 1) {
setLoading(true);
const nodeCopy = { ...node };
nodeCopy.Aria2OptionsSerialized = {
...node.Aria2OptionsSerialized,
};
nodeCopy.Rank = parseInt(nodeCopy.Rank);
nodeCopy.Aria2OptionsSerialized.interval = parseInt(
nodeCopy.Aria2OptionsSerialized.interval
);
nodeCopy.Aria2OptionsSerialized.timeout = parseInt(
nodeCopy.Aria2OptionsSerialized.timeout
);
nodeCopy.Aria2Enabled = nodeCopy.Aria2Enabled === "true";
API.post("/admin/node", {
node: nodeCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.node ? t("nodeSavedNow") : t("nodeAdded"),
"success"
);
setActiveStep(activeStep + 1);
setLoading(false);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
} else {
setActiveStep(activeStep + 1);
}
};
return (
<div>
<Typography variant={"h6"}>
{props.node ? t("editNode") : t("addNode")}
</Typography>
<Stepper activeStep={activeStep}>
{usedSteps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">
{tDashboard("policy.optional")}
</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
if (!(label.slaveOnly && node.Type === 1)) {
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>
{t(label.title)}
</StepLabel>
</Step>
);
}
})}
</Stepper>
{usedSteps[activeStep].component({
onSubmit: (e) => nextStep(),
node: node,
loading: loading,
onBack: (e) => setActiveStep(activeStep - 1),
handleTextChange: handleTextChange,
activeStep: activeStep,
handleOptionChange: handleOptionChange,
})}
</div>
);
}

View File

@@ -0,0 +1,333 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import {
Cancel,
CheckCircle,
Delete,
Edit,
Pause,
PlayArrow,
} from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Chip from "@material-ui/core/Chip";
import classNames from "classnames";
import Box from "@material-ui/core/Box";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
disabledBadge: {
marginLeft: theme.spacing(1),
height: 18,
},
disabledCell: {
color: theme.palette.text.disabled,
},
verticalAlign: {
verticalAlign: "middle",
display: "inline-block",
},
}));
const columns = [
{ id: "#", minWidth: 50 },
{ id: "name", minWidth: 170 },
{
id: "status",
minWidth: 50,
},
{
id: "features",
minWidth: 170,
},
{
id: "action",
minWidth: 170,
},
];
const features = [
{
field: "Aria2Enabled",
name: "remoteDownload",
},
];
export default function Node() {
const { t } = useTranslation("dashboard", { keyPrefix: "node" });
const classes = useStyles();
const [nodes, setNodes] = useState([]);
const [isActive, setIsActive] = useState({});
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/node/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
})
.then((response) => {
setNodes(response.data.items);
setTotal(response.data.total);
setIsActive(response.data.active);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
const toggleNode = (id, desired) => {
setLoading(true);
API.patch("/admin/node/enable/" + id + "/" + desired)
.then((response) => {
loadList();
ToggleSnackbar(
"top",
"right",
desired === 1 ? t("nodeDisabled") : t("nodeEnabled"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteNode = (id) => {
setLoading(true);
API.delete("/admin/node/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("nodeDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
useEffect(() => {
loadList();
}, [page, pageSize]);
const getStatusBadge = (status) => {
if (status === 1) {
return (
<Chip
className={classes.disabledBadge}
size="small"
label={t("disabled")}
/>
);
}
};
const getFeatureBadge = (node) =>
features.map((feature) => {
if (node[feature.field]) {
return (
<Chip
className={classes.disabledBadge}
size="small"
color="primary"
label={t(feature.name)}
/>
);
}
});
const getRealStatusBadge = (status) =>
status ? (
<Box color="success.main" fontSize="small">
<CheckCircle
className={classes.verticalAlign}
fontSize="small"
/>{" "}
<span className={classes.verticalAlign}>{t("online")}</span>
</Box>
) : (
<Box color="error.main" fontSize="small">
<Cancel className={classes.verticalAlign} fontSize="small" />{" "}
<span className={classes.verticalAlign}>{t("offline")}</span>
</Box>
);
return (
<div>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/node/add")}
variant={"contained"}
>
{t("addNewNode")}
</Button>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{t("refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{
minWidth: column.minWidthclassNames,
}}
>
{t(column.id)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{nodes.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell
className={classNames({
[classes.disabledCell]:
row.Status === 1,
})}
>
{row.Name}
{getStatusBadge(row.Status)}
</TableCell>
<TableCell>
{getRealStatusBadge(isActive[row.ID])}
</TableCell>
<TableCell>
{getFeatureBadge(row)}
</TableCell>
<TableCell align={"right"}>
<Tooltip
title={
row.Status === 1
? t("enableNode")
: t("disableNode")
}
>
<IconButton
disabled={loading}
onClick={() =>
toggleNode(
row.ID,
1 - row.Status
)
}
size={"small"}
>
{row.Status === 1 && (
<PlayArrow />
)}
{row.Status !== 1 && <Pause />}
</IconButton>
</Tooltip>
<Tooltip title={t("edit")}>
<IconButton
disabled={loading}
onClick={() =>
history.push(
"/admin/node/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title={t("delete")}>
<IconButton
onClick={() =>
deleteNode(row.ID)
}
disabled={loading}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,432 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import ShareFilter from "../Dialogs/ShareFilter";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Order() {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [orders, setOrders] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/order/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setOrders(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/order/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("orderDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/order/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("orderDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = orders.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < orders.length
}
checked={
orders.length > 0 &&
selected.length === orders.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{t("orderName")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("product")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
<TableSortLabel
active={orderBy[0] === "order_no"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"order_no",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("orderNumber")}
{orderBy[0] === "order_no" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
{t("price")}
</TableCell>
<TableCell
style={{ minWidth: 80 }}
align={"right"}
>
{t("qyt")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("status")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{t("paidBy")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("orderOwner")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{tDashboard("file.createdAt")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{orders.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>
{row.Type === 0 && t("storagePack")}
{row.Type === 1 &&
t("purchasableGroups")}
{row.Type === 2 && t("credits")}
</TableCell>
<TableCell>{row.OrderNo}</TableCell>
<TableCell align={"right"}>
{row.Method === "score" && row.Price}
{row.Method !== "score" && (
<>
{(row.Price / 100).toFixed(2)}
</>
)}
</TableCell>
<TableCell align={"right"}>
{row.Num}
</TableCell>
<TableCell>
{row.Status === 0 && t("unpaid")}
{row.Status === 1 && t("paid")}
</TableCell>
<TableCell>
{row.Method === "score" && t("credits")}
{row.Method === "alipay" && t("alipay")}
{row.Method === "payjs" && t("payjs")}
{row.Method === "custom" &&
t("customPayment")}
{row.Method === "weixin" &&
t("wechatPay")}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,45 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import { useParams } from "react-router";
import LocalGuide from "./Guid/LocalGuide";
import RemoteGuide from "./Guid/RemoteGuide";
import QiniuGuide from "./Guid/QiniuGuide";
import OSSGuide from "./Guid/OSSGuide";
import UpyunGuide from "./Guid/UpyunGuide";
import COSGuide from "./Guid/COSGuide";
import OneDriveGuide from "./Guid/OneDriveGuide";
import S3Guide from "./Guid/S3Guide";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function AddPolicyParent() {
const classes = useStyles();
const { type } = useParams();
return (
<div>
<Paper square className={classes.content}>
{type === "local" && <LocalGuide />}
{type === "remote" && <RemoteGuide />}
{type === "qiniu" && <QiniuGuide />}
{type === "oss" && <OSSGuide />}
{type === "upyun" && <UpyunGuide />}
{type === "cos" && <COSGuide />}
{type === "onedrive" && <OneDriveGuide />}
{type === "s3" && <S3Guide />}
</Paper>
</div>
);
}

View File

@@ -0,0 +1,80 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import { useParams } from "react-router";
import LocalGuide from "./Guid/LocalGuide";
import RemoteGuide from "./Guid/RemoteGuide";
import QiniuGuide from "./Guid/QiniuGuide";
import OSSGuide from "./Guid/OSSGuide";
import UpyunGuide from "./Guid/UpyunGuide";
import S3Guide from "./Guid/S3Guide";
import COSGuide from "./Guid/COSGuide";
import OneDriveGuide from "./Guid/OneDriveGuide";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import EditPro from "./Guid/EditPro";
import { toggleSnackbar } from "../../../redux/explorer";
import { transformResponse } from "./utils";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function EditPolicyPreload() {
const classes = useStyles();
const [type, setType] = useState("");
const [policy, setPolicy] = useState({});
const { mode, id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setType("");
API.get("/admin/policy/" + id)
.then((response) => {
response = transformResponse(response);
setPolicy(response.data);
setType(response.data.Type);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return (
<div>
<Paper square className={classes.content}>
{mode === "guide" && (
<>
{type === "local" && <LocalGuide policy={policy} />}
{type === "remote" && <RemoteGuide policy={policy} />}
{type === "qiniu" && <QiniuGuide policy={policy} />}
{type === "oss" && <OSSGuide policy={policy} />}
{type === "upyun" && <UpyunGuide policy={policy} />}
{type === "cos" && <COSGuide policy={policy} />}
{type === "onedrive" && (
<OneDriveGuide policy={policy} />
)}
{type === "s3" && <S3Guide policy={policy} />}
</>
)}
{mode === "pro" && type !== "" && <EditPro policy={policy} />}
</Paper>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,690 @@
import React, { useCallback, useState } from "react";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import { toggleSnackbar } from "../../../../redux/explorer";
import { useTranslation } from "react-i18next";
import { transformPolicyRequest } from "../utils";
export default function EditPro(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const [, setLoading] = useState(false);
const [policy, setPolicy] = useState(props.policy);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
let policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy = transformPolicyRequest(policyCopy);
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.policy ? t("policySaved") : t("policyAdded"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>{t("editPolicy")}</Typography>
<TableContainer>
<form onSubmit={submitPolicy}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>{t("setting")}</TableCell>
<TableCell>{t("value")}</TableCell>
<TableCell>{t("description")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
{t("id")}
</TableCell>
<TableCell>{policy.ID}</TableCell>
<TableCell>{t("policyID")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("type")}
</TableCell>
<TableCell>{policy.Type}</TableCell>
<TableCell>{t("policyType")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("name")}
</TableCell>
<TableCell>
<FormControl>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</TableCell>
<TableCell>{t("policyName")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("server")}
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.Server}
onChange={handleChange("Server")}
/>
</FormControl>
</TableCell>
<TableCell>{t("policyEndpoint")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("bucketName")}
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.BucketName}
onChange={handleChange(
"BucketName"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("bucketID")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("privateBucket")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>{t("privateBucketDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("resourceRootURL")}
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
/>
</FormControl>
</TableCell>
<TableCell>{t("resourceRootURLDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("accessKey")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
rowsMax={10}
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</TableCell>
<TableCell>{t("akDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("secretKey")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
rowsMax={10}
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</TableCell>
<TableCell>{t("secretKey")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("maxSizeBytes")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
/>
</FormControl>
</TableCell>
<TableCell>{t("maxSizeBytesDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("autoRename")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.AutoRename}
onChange={handleChange(
"AutoRename"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>{t("autoRenameDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("storagePath")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={policy.DirNameRule}
onChange={handleChange(
"DirNameRule"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("storagePathDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("fileName")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("fileNameDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("allowGetSourceLink")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={handleChange(
"IsOriginLinkEnable"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
{t("allowGetSourceLinkDes")}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("upyunToken")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized.token
}
onChange={handleOptionChange(
"token"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("upyunOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("allowedFileExtension")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("emptyIsNoLimit")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("allowedMimetype")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.mimetype
}
onChange={handleOptionChange(
"mimetype"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("qiniuOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("odRedirectURL")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_redirect
}
onChange={handleOptionChange(
"od_redirect"
)}
/>
</FormControl>
</TableCell>
<TableCell>
{t("noModificationNeeded")}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("odReverseProxy")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_proxy
}
onChange={handleOptionChange(
"od_proxy"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("odDriverID")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_driver
}
onChange={handleOptionChange(
"od_driver"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odDriverIDDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("s3Region")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized.region
}
onChange={handleOptionChange(
"region"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("s3Only")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("lanEndpoint")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.server_side_endpoint
}
onChange={handleOptionChange(
"server_side_endpoint"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("ossOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("chunkSizeBytes")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={
policy.OptionsSerialized
.chunk_size
}
onChange={handleOptionChange(
"chunk_size"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("chunkSizeBytesDes")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("placeHolderWithSize")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={
policy.OptionsSerialized
.placeholder_with_size
}
onChange={handleOptionChange(
"placeholder_with_size"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
{t("placeHolderWithSizeDes")}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("tps")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
step: 0.1,
}}
value={
policy.OptionsSerialized
.tps_limit
}
onChange={handleOptionChange(
"tps_limit"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("tpsBurst")}
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
step: 1,
}}
value={
policy.OptionsSerialized
.tps_limit_burst
}
onChange={handleOptionChange(
"tps_limit_burst"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("odOnly")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("usePathEndpoint")}
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={
policy.OptionsSerialized
.s3_path_style
}
onChange={handleOptionChange(
"s3_path_style"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("yes")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("no")}
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>{t("s3Only")}</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{t("thumbExt")}
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.thumb_exts
}
onChange={handleOptionChange(
"thumb_exts"
)}
/>
</FormControl>
</TableCell>
<TableCell>{t("thumbExtDes")}</TableCell>
</TableRow>
</TableBody>
</Table>
<Button
type={"submit"}
color={"primary"}
variant={"contained"}
style={{ margin: 8 }}
>
{t("saveChanges")}
</Button>
</form>
</TableContainer>
</div>
);
}

View File

@@ -0,0 +1,797 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Stepper from "@material-ui/core/Stepper";
import StepLabel from "@material-ui/core/StepLabel";
import Step from "@material-ui/core/Step";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Collapse from "@material-ui/core/Collapse";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import MagicVar from "../../Dialogs/MagicVar";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../redux/explorer";
import { getNumber } from "../../../../utils";
import { Trans, useTranslation } from "react-i18next";
import { transformPolicyRequest } from "../utils";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
}));
const steps = [
{
title: "storagePathStep",
optional: false,
},
{
title: "sourceLinkStep",
optional: false,
},
{
title: "uploadSettingStep",
optional: false,
},
{
title: "finishStep",
optional: false,
},
];
export default function LocalGuide(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [useCDN, setUseCDN] = useState("false");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "local",
Name: "",
DirNameRule: "uploads/{uid}/{path}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
BaseURL: "",
IsPrivate: "true",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
chunk_size: 25 << 20,
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const checkPathSetting = (e) => {
e.preventDefault();
setLoading(true);
// 测试路径是否可用
API.post("/admin/policy/test/path", {
path: policy.DirNameRule,
})
.then(() => {
setActiveStep(1);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
let policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 处理存储策略
if (useCDN === "false" || policy.IsOriginLinkEnable === "false") {
policyCopy.BaseURL = "";
}
// 类型转换
policyCopy = transformPolicyRequest(policyCopy);
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.policy ? t("policySaved") : t("policyAdded"),
"success"
);
setActiveStep(4);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy
? t("editLocalStoragePolicy")
: t("addLocalStoragePolicy")}
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">
{t("optional")}
</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={t(label.title)} {...stepProps}>
<StepLabel {...labelProps}>
{t(label.title)}
</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={checkPathSetting}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.pathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("path")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("pathOfFolderToStoreFiles")}
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.filePathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("file")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("autoRenameStoredFile")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("keepOriginalFileName")}
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("renameRule")}
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enableGettingPermanentSourceLink")}
<br />
{t("enableGettingPermanentSourceLinkDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={handleChange(
"IsOriginLinkEnable"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("allowed")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("forbidden")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.IsOriginLinkEnable === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("useCDN")}
<br />
{t("useCDNDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={useCDN}
onChange={(e) => {
if (
e.target.value === "false"
) {
setPolicy({
...policy,
BaseURL: "",
});
}
setUseCDN(e.target.value);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("use")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notUse")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={useCDN === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("cdnDomain")}
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required={
policy.IsOriginLinkEnable ===
"true" && useCDN === "true"
}
label={t("cdnPrefix")}
/>
</div>
</div>
</div>
</Collapse>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileSize")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterSizeLimit")}
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={t("maxSizeOfSingleFile")}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileExt")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterFileExt")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("extList")}
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{getNumber(3, [
policy.MaxSize !== "0",
policy.OptionsSerialized.file_type !== "",
])}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("chunkSizeLabel")}
<br />
{t("chunkSizeDes")}
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.OptionsSerialized.chunk_size}
onChange={handleOptionChange("chunk_size")}
min={0}
max={9223372036854775807}
label={t("chunkSize")}
/>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer} />
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("nameThePolicy")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("policyName")}
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("finish")}
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<>
<form className={classes.stepContent}>
<Typography>
{props.policy ? t("policySaved") : t("policyAdded")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("furtherActions")}
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
{t("backToList")}
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,903 @@
import { lighten, makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import Stepper from "@material-ui/core/Stepper";
import StepLabel from "@material-ui/core/StepLabel";
import Step from "@material-ui/core/Step";
import Typography from "@material-ui/core/Typography";
import { useDispatch } from "react-redux";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Radio from "@material-ui/core/Radio";
import Collapse from "@material-ui/core/Collapse";
import Button from "@material-ui/core/Button";
import API from "../../../../middleware/Api";
import MagicVar from "../../Dialogs/MagicVar";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
import { transformPolicyRequest } from "../utils";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
}));
const steps = [
{
title: "storageBucket",
optional: false,
},
{
title: "storagePathStep",
optional: false,
},
{
title: "sourceLinkStep",
optional: false,
},
{
title: "uploadSettingStep",
optional: false,
},
{
title: "finishStep",
optional: false,
},
];
export default function UpyunGuide(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "upyun",
Name: "",
SecretKey: "",
AccessKey: "",
BaseURL: "",
IsPrivate: "false",
DirNameRule: "uploads/{year}/{month}/{day}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
token: "",
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
let policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy = transformPolicyRequest(policyCopy);
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
props.policy ? t("policySaved") : t("policyAdded"),
"success"
);
setActiveStep(5);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy
? t("editUpyunStoragePolicy")
: t("addUpyunStoragePolicy")}
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">可选</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>
{t(label.title)}
</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(1);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>0</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.wanSiteURLDes"}
components={[<strong key={0} />]}
/>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.createUpyunBucketDes"}
components={[
<Link
key={0}
href={
"https://console.upyun.com/services/create/file/"
}
target={"_blank"}
/>,
]}
/>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("storageServiceNameDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("storageServiceName")}
</InputLabel>
<Input
required
value={policy.BucketName}
onChange={handleChange("BucketName")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("operatorNameDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("operatorName")}
</InputLabel>
<Input
required
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("operatorPassword")}
</InputLabel>
<Input
required
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("upyunCDNDes")}
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required
label={t("bucketCDNDomain")}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("upyunOptionalDes")}
<br />
{t("upyunTokenDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("tokenEnabled")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("tokenDisabled")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.IsPrivate === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>6</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("upyunTokenSecretDes")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("upyunTokenSecret")}
</InputLabel>
<Input
value={
policy.OptionsSerialized.token
}
onChange={handleOptionChange(
"token"
)}
required={
policy.IsPrivate === "true"
}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.pathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("path")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("pathOfFolderToStoreFiles")}
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
<Trans
ns={"dashboard"}
i18nKey={"policy.filePathMagicVarDes"}
components={[
<Link
key={0}
color={"secondary"}
href={"javascript:void()"}
onClick={() => setMagicVar("file")}
/>,
]}
/>
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("autoRenameStoredFile")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("keepOriginalFileName")}
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("renameRule")}
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
{t("back")}
</Button>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enableGettingPermanentSourceLink")}
<br />
{t("enableGettingPermanentSourceLinkDes")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={(e) => {
if (
policy.IsPrivate === "true" &&
e.target.value === "true"
) {
ToggleSnackbar(
"top",
"right",
t(
"cannotEnableForTokenProtectedBucket"
),
"warning"
);
}
handleChange("IsOriginLinkEnable")(
e
);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("allowed")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("forbidden")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(4);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileSize")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterSizeLimit")}
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={t("maxSizeOfSingleFile")}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("limitFileExt")}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label={t("limit")}
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label={t("notLimit")}
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("enterFileExt")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("extList")}
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("next")}
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}></div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
{t("nameThePolicy")}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("policyName")}
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(3)}
>
{t("back")}
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("finish")}
</Button>
</div>
</form>
)}
{activeStep === 5 && (
<>
<form className={classes.stepContent}>
<Typography>
{props.policy ? t("policySaved") : t("policyAdded")}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
{t("furtherActions")}
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
{t("backToList")}
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

View File

@@ -0,0 +1,297 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import AddPolicy from "../Dialogs/AddPolicy";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { useHistory, useLocation } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Delete, Edit } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Menu from "@material-ui/core/Menu";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
}));
const columns = [
{ id: "#", label: "sharp", minWidth: 50 },
{ id: "name", label: "name", minWidth: 170 },
{ id: "type", label: "type", minWidth: 170 },
{
id: "count",
label: "childFiles",
minWidth: 50,
align: "right",
},
{
id: "size",
label: "totalSize",
minWidth: 100,
align: "right",
},
{
id: "action",
label: "actions",
minWidth: 170,
align: "right",
},
];
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export default function Policy() {
const { t } = useTranslation("dashboard", { keyPrefix: "policy" });
const classes = useStyles();
const [policies, setPolicies] = useState([]);
const [statics, setStatics] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [addDialog, setAddDialog] = useState(false);
const [filter, setFilter] = useState("all");
const [anchorEl, setAnchorEl] = React.useState(null);
const [editID, setEditID] = React.useState(0);
const location = useLocation();
const history = useHistory();
const query = useQuery();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
if (query.get("code") === "0") {
ToggleSnackbar("top", "right", t("authSuccess"), "success");
} else if (query.get("msg") && query.get("msg") !== "") {
ToggleSnackbar(
"top",
"right",
query.get("msg") + ", " + query.get("err"),
"warning"
);
}
}, [location]);
const loadList = () => {
API.post("/admin/policy/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
conditions: filter === "all" ? {} : { type: filter },
})
.then((response) => {
setPolicies(response.data.items);
setStatics(response.data.statics);
setTotal(response.data.total);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, filter]);
const deletePolicy = (id) => {
API.delete("/admin/policy/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("policyDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
const open = Boolean(anchorEl);
return (
<div>
<AddPolicy open={addDialog} onClose={() => setAddDialog(false)} />
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => setAddDialog(true)}
variant={"contained"}
>
{t("newStoragePolicy")}
</Button>
<div className={classes.headerRight}>
<Select
style={{
marginRight: 8,
}}
value={filter}
onChange={(e) => setFilter(e.target.value)}
>
{[
"all",
"local",
"remote",
"qiniu",
"upyun",
"oss",
"cos",
"onedrive",
"s3",
].map((v) => (
<MenuItem key={v} value={v}>
{t(v)}
</MenuItem>
))}
</Select>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{t("refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{t(column.label)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{policies.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>{t(row.Type)}</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
statics[row.ID][0].toLocaleString()}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
sizeToString(statics[row.ID][1])}
</TableCell>
<TableCell align={"right"}>
<Tooltip title={t("delete")}>
<IconButton
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip title={t("edit")}>
<IconButton
onClick={(e) => {
setEditID(row.ID);
handleClick(e);
}}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
<Menu
open={open}
anchorEl={anchorEl}
onClose={handleClose}
keepMounted
>
<MenuItem
onClick={(e) => {
handleClose(e);
history.push("/admin/policy/edit/pro/" + editID);
}}
>
{t("editInProMode")}
</MenuItem>
<MenuItem
onClick={(e) => {
handleClose(e);
history.push("/admin/policy/edit/guide/" + editID);
}}
>
{t("editInWizardMode")}
</MenuItem>
</Menu>
</div>
);
}

View File

@@ -0,0 +1,76 @@
const boolFields = ["IsOriginLinkEnable", "AutoRename", "IsPrivate"];
const numberFields = ["MaxSize"];
const boolFieldsInOptions = ["placeholder_with_size", "s3_path_style"];
const numberFieldsInOptions = ["chunk_size", "tps_limit", "tps_limit_burst"];
const listJsonFieldsInOptions = ["file_type", "thumb_exts"];
export const transformResponse = (response) => {
boolFields.forEach(
(field) =>
(response.data[field] = response.data[field] ? "true" : "false")
);
numberFields.forEach(
(field) => (response.data[field] = response.data[field].toString())
);
boolFieldsInOptions.forEach(
(field) =>
(response.data.OptionsSerialized[field] = response.data
.OptionsSerialized[field]
? "true"
: "false")
);
numberFieldsInOptions.forEach(
(field) =>
(response.data.OptionsSerialized[field] = response.data
.OptionsSerialized[field]
? response.data.OptionsSerialized[field].toString()
: 0)
);
listJsonFieldsInOptions.forEach((field) => {
response.data.OptionsSerialized[field] = response.data
.OptionsSerialized[field]
? response.data.OptionsSerialized[field].join(",")
: "";
});
return response;
};
export const transformPolicyRequest = (policyCopy) => {
boolFields.forEach(
(field) => (policyCopy[field] = policyCopy[field] === "true")
);
numberFields.forEach(
(field) => (policyCopy[field] = parseInt(policyCopy[field]))
);
boolFieldsInOptions.forEach(
(field) =>
(policyCopy.OptionsSerialized[field] =
policyCopy.OptionsSerialized[field] === "true")
);
numberFieldsInOptions.forEach(
(field) =>
(policyCopy.OptionsSerialized[field] = parseInt(
policyCopy.OptionsSerialized[field]
))
);
listJsonFieldsInOptions.forEach((field) => {
policyCopy.OptionsSerialized[field] = policyCopy.OptionsSerialized[
field
]
? policyCopy.OptionsSerialized[field].split(",")
: [];
if (
policyCopy.OptionsSerialized[field].length === 1 &&
policyCopy.OptionsSerialized[field][0] === ""
) {
policyCopy.OptionsSerialized[field] = [];
}
});
return policyCopy;
};

View File

@@ -0,0 +1,422 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import { reportReasons } from "../../../config";
import CheckCircleOutlineIcon from "@material-ui/icons/CheckCircleOutline";
import { Delete } from "@material-ui/icons";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function ReportList() {
const { t } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tApp } = useTranslation("application");
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [reports, setReports] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [users, setUsers] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [ids, setIds] = useState({});
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/report/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
})
.then((response) => {
setUsers(response.data.users);
setReports(response.data.items);
setTotal(response.data.total);
setIds(response.data.ids);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy]);
const deleteReport = (id) => {
setLoading(true);
API.post("/admin/report/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("markSuccessful"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteShare = (id) => {
setLoading(true);
API.post("/admin/share/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar(
"top",
"right",
tDashboard("share.deleted"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/report/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("markSuccessful"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = reports.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={t("markAsResolved")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<CheckCircleOutlineIcon />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < reports.length
}
checked={
reports.length > 0 &&
selected.length === reports.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{t("reportedContent")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{tDashboard("policy.type")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("reason")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("description")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("vas.shareLink")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("reportTime")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reports.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{row.Share.ID === 0 && t("invalid")}
{row.Share.ID > 0 && (
<Link
target={"_blank"}
color="inherit"
href={
"/s/" +
ids[row.Share.ID] +
(row.Share.Password === ""
? ""
: "?password=" +
row.Share.Password)
}
>
{row.Share.SourceName}
</Link>
)}
</TableCell>
<TableCell>
{row.Share.ID > 0 &&
(row.Share.IsDir
? tDashboard("share.folder")
: tDashboard("share.file"))}
</TableCell>
<TableCell>
{tApp(reportReasons[row.Reason])}
</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{row.Description}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" +
row.Share.UserID
}
>
{users[row.Share.UserID]
? users[row.Share.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip title={t("markAsResolved")}>
<IconButton
disabled={loading}
onClick={() =>
deleteReport(row.ID)
}
size={"small"}
>
<CheckCircleOutlineIcon />
</IconButton>
</Tooltip>
{row.Share.ID > 0 && (
<Tooltip title={t("deleteShare")}>
<IconButton
disabled={loading}
onClick={() =>
deleteShare(
row.Share.ID
)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,502 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Switch from "@material-ui/core/Switch";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import AlertDialog from "../Dialogs/Alert";
import Alert from "@material-ui/lab/Alert";
import FileSelector from "../Common/FileSelector";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function Access() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [initCompleted, setInitComplete] = useState(false);
const [options, setOptions] = useState({
register_enabled: "1",
default_group: "1",
email_active: "0",
login_captcha: "0",
reg_captcha: "0",
forget_captcha: "0",
qq_login: "0",
qq_direct_login: "0",
qq_login_id: "",
qq_login_key: "",
authn_enabled: "0",
mail_domain_filter: "0",
mail_domain_filter_list: "",
initial_files: "[]",
});
const [siteURL, setSiteURL] = useState("");
const [groups, setGroups] = useState([]);
const [httpAlert, setHttpAlert] = useState(false);
const handleChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const handleInputChange = (name) => (event) => {
const value = event.target.value;
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: [...Object.keys(options), "siteURL"],
})
.then((response) => {
setSiteURL(response.data.siteURL);
delete response.data.siteURL;
setOptions(response.data);
setInitComplete(true);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<AlertDialog
title={t("hint")}
msg={t("webauthnNoHttps")}
onClose={() => setHttpAlert(false)}
open={httpAlert}
/>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("accountManagement")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.register_enabled === "1"
}
onChange={handleChange(
"register_enabled"
)}
/>
}
label={t("allowNewRegistrations")}
/>
<FormHelperText id="component-helper-text">
{t("allowNewRegistrationsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.email_active === "1"
}
onChange={handleChange(
"email_active"
)}
/>
}
label={t("emailActivation")}
/>
<FormHelperText id="component-helper-text">
{t("emailActivationDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.reg_captcha === "1"
}
onChange={handleChange(
"reg_captcha"
)}
/>
}
label={t("captchaForSignup")}
/>
<FormHelperText id="component-helper-text">
{t("captchaForSignupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.login_captcha === "1"
}
onChange={handleChange(
"login_captcha"
)}
/>
}
label={t("captchaForLogin")}
/>
<FormHelperText id="component-helper-text">
{t("captchaForLoginDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.forget_captcha === "1"
}
onChange={handleChange(
"forget_captcha"
)}
/>
}
label={t("captchaForReset")}
/>
<FormHelperText id="component-helper-text">
{t("captchaForResetDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.authn_enabled === "1"
}
onChange={(e) => {
if (
!siteURL.startsWith(
"https://"
)
) {
setHttpAlert(true);
return;
}
handleChange("authn_enabled")(
e
);
}}
/>
}
label={t("webauthn")}
/>
<FormHelperText id="component-helper-text">
{t("webauthnDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("defaultGroup")}
</InputLabel>
<Select
value={options.default_group}
onChange={handleInputChange(
"default_group"
)}
required
>
{groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem
key={v.ID}
value={v.ID.toString()}
>
{v.Name}
</MenuItem>
);
})}
</Select>
<FormHelperText id="component-helper-text">
{t("defaultGroupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
{initCompleted && (
<FileSelector
label={tVas("initialFiles")}
value={JSON.parse(
options.initial_files
)}
onChange={(v) =>
handleInputChange("initial_files")({
target: { value: v },
})
}
/>
)}
<FormHelperText id="component-helper-text">
{tVas("initialFilesDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{tVas("filterEmailProvider")}
</InputLabel>
<Select
value={options.mail_domain_filter}
onChange={handleInputChange(
"mail_domain_filter"
)}
required
>
{[
tVas("filterEmailProviderDisabled"),
tVas("filterEmailProviderWhitelist"),
tVas("filterEmailProviderBlacklist"),
].map((v, i) => (
<MenuItem key={i} value={i.toString()}>
{v}
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{tVas("filterEmailProviderDes")}
</FormHelperText>
</FormControl>
</div>
{options.mail_domain_filter !== "0" && (
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("filterEmailProviderRule")}
</InputLabel>
<Input
value={options.mail_domain_filter_list}
onChange={handleChange(
"mail_domain_filter_list"
)}
multiline
rowsMax="10"
/>
<FormHelperText id="component-helper-text">
{tVas("filterEmailProviderRuleDes")}
</FormHelperText>
</FormControl>
</div>
)}
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{tVas("qqConnect")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
{tVas("qqConnectHint", {
url: siteURL.endsWith("/")
? siteURL + "login/qq"
: siteURL + "/login/qq",
})}
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={options.qq_login === "1"}
onChange={handleChange("qq_login")}
/>
}
label={tVas("enableQQConnect")}
/>
<FormHelperText id="component-helper-text">
{tVas("enableQQConnectDes")}
</FormHelperText>
</FormControl>
</div>
{options.qq_login === "1" && (
<>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.qq_direct_login ===
"1"
}
onChange={handleChange(
"qq_direct_login"
)}
/>
}
label={tVas("loginWithoutBinding")}
/>
<FormHelperText id="component-helper-text">
{tVas("loginWithoutBindingDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appid")}
</InputLabel>
<Input
required
value={options.qq_login_id}
onChange={handleInputChange(
"qq_login_id"
)}
/>
<FormHelperText id="component-helper-text">
{tVas("appidDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appKey")}
</InputLabel>
<Input
required
value={options.qq_login_key}
onChange={handleInputChange(
"qq_login_key"
)}
/>
<FormHelperText id="component-helper-text">
{tVas("appKeyDes")}
</FormHelperText>
</FormControl>
</div>
</>
)}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,537 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Input from "@material-ui/core/Input";
import Link from "@material-ui/core/Link";
import { toggleSnackbar } from "../../../redux/explorer";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function Captcha() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
captcha_type: "normal",
captcha_height: "1",
captcha_width: "1",
captcha_mode: "3",
captcha_CaptchaLen: "6",
captcha_ComplexOfNoiseText: "0",
captcha_ComplexOfNoiseDot: "0",
captcha_IsShowHollowLine: "0",
captcha_IsShowNoiseDot: "0",
captcha_IsShowNoiseText: "0",
captcha_IsShowSlimeLine: "0",
captcha_IsShowSineLine: "0",
captcha_ReCaptchaKey: "",
captcha_ReCaptchaSecret: "",
captcha_TCaptcha_CaptchaAppId: "",
captcha_TCaptcha_AppSecretKey: "",
captcha_TCaptcha_SecretId: "",
captcha_TCaptcha_SecretKey: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "1" : "0";
setOptions({
...options,
[name]: value,
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("captcha")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("captchaType")}
</InputLabel>
<Select
value={options.captcha_type}
onChange={handleChange("captcha_type")}
required
>
<MenuItem value={"normal"}>
{t("plainCaptcha")}
</MenuItem>
<MenuItem value={"recaptcha"}>
{t("reCaptchaV2")}
</MenuItem>
<MenuItem value={"tcaptcha"}>
{t("tencentCloudCaptcha")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("captchaProvider")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
{options.captcha_type === "normal" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("plainCaptchaTitle")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaWidth")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_width}
onChange={handleChange("captcha_width")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaHeight")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_height}
onChange={handleChange(
"captcha_height"
)}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaLength")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_CaptchaLen}
onChange={handleChange(
"captcha_CaptchaLen"
)}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("captchaMode")}
</InputLabel>
<Select
value={options.captcha_mode}
onChange={handleChange("captcha_mode")}
required
>
<MenuItem value={"0"}>
{t("captchaModeNumber")}
</MenuItem>
<MenuItem value={"1"}>
{t("captchaModeLetter")}
</MenuItem>
<MenuItem value={"2"}>
{t("captchaModeMath")}
</MenuItem>
<MenuItem value={"3"}>
{t("captchaModeNumberLetter")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("captchaElement")}
</FormHelperText>
</FormControl>
</div>
{[
{
name: "complexOfNoiseText",
field: "captcha_ComplexOfNoiseText",
},
{
name: "complexOfNoiseDot",
field: "captcha_ComplexOfNoiseDot",
},
{
name: "showHollowLine",
field: "captcha_IsShowHollowLine",
},
{
name: "showNoiseDot",
field: "captcha_IsShowNoiseDot",
},
{
name: "showNoiseText",
field: "captcha_IsShowNoiseText",
},
{
name: "showSlimeLine",
field: "captcha_IsShowSlimeLine",
},
{
name: "showSineLine",
field: "captcha_IsShowSineLine",
},
].map((input) => (
<div key={input.name} className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options[input.field] ===
"1"
}
onChange={handleCheckChange(
input.field
)}
/>
}
label={t(input.name)}
/>
</FormControl>
</div>
))}
</div>
</div>
)}
{options.captcha_type === "recaptcha" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("reCaptchaV2")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteKey")}
</InputLabel>
<Input
required
value={options.captcha_ReCaptchaKey}
onChange={handleChange(
"captcha_ReCaptchaKey"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={"settings.siteKeyDes"}
components={[
<Link
key={0}
href={
"https://www.google.com/recaptcha/admin/create"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteSecret")}
</InputLabel>
<Input
required
value={
options.captcha_ReCaptchaSecret
}
onChange={handleChange(
"captcha_ReCaptchaSecret"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.siteSecretDes"
}
components={[
<Link
key={0}
href={
"https://www.google.com/recaptcha/admin/create"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
</div>
</div>
</div>
)}
{options.captcha_type === "tcaptcha" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("tencentCloudCaptcha")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("secretID")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_SecretId
}
onChange={handleChange(
"captcha_TCaptcha_SecretId"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.siteSecretDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/capi"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("secretKey")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_SecretKey
}
onChange={handleChange(
"captcha_TCaptcha_SecretKey"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.secretKeyDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/capi"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("tCaptchaAppID")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_CaptchaAppId
}
onChange={handleChange(
"captcha_TCaptcha_CaptchaAppId"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.tCaptchaAppIDDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/captcha/graphical"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("tCaptchaSecretKey")}
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_AppSecretKey
}
onChange={handleChange(
"captcha_TCaptcha_AppSecretKey"
)}
/>
<FormHelperText id="component-helper-text">
<Trans
ns={"dashboard"}
i18nKey={
"settings.tCaptchaSecretKeyDes"
}
components={[
<Link
key={0}
href={
"https://console.cloud.tencent.com/captcha/graphical"
}
target={"_blank"}
/>,
]}
/>
</FormHelperText>
</FormControl>
</div>
</div>
</div>
</div>
)}
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,664 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import SizeInput from "../Common/SizeInput";
import { toggleSnackbar } from "../../../redux/explorer";
import Alert from "@material-ui/lab/Alert";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { Trans, useTranslation } from "react-i18next";
import Link from "@material-ui/core/Link";
import ThumbGenerators from "./ThumbGenerators";
import PolicySelector from "../Common/PolicySelector";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function ImageSetting() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
gravatar_server: "",
avatar_path: "",
avatar_size: "",
avatar_size_l: "",
avatar_size_m: "",
avatar_size_s: "",
thumb_width: "",
thumb_height: "",
office_preview_service: "",
thumb_file_suffix: "",
thumb_max_task_count: "",
thumb_encode_method: "",
thumb_gc_after_gen: "0",
thumb_encode_quality: "",
maxEditSize: "",
wopi_enabled: "0",
wopi_endpoint: "",
wopi_session_timeout: "0",
thumb_builtin_enabled: "0",
thumb_vips_enabled: "0",
thumb_vips_exts: "",
thumb_ffmpeg_enabled: "0",
thumb_vips_path: "",
thumb_ffmpeg_path: "",
thumb_ffmpeg_exts: "",
thumb_ffmpeg_seek: "",
thumb_libreoffice_path: "",
thumb_libreoffice_enabled: "0",
thumb_libreoffice_exts: "",
thumb_proxy_enabled: "0",
thumb_proxy_policy: [],
thumb_max_src_size: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
response.data.thumb_proxy_policy = JSON.parse(
response.data.thumb_proxy_policy
).map((v) => {
return v.toString();
});
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const reload = () => {
API.get("/admin/reload/wopi")
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
let value = options[k];
if (k === "thumb_proxy_policy") {
value = JSON.stringify(value.map((v) => parseInt(v)));
}
option.push({
key: k,
value,
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
reload();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "1" : "0";
setOptions({
...options,
[name]: value,
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("avatar")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("gravatarServer")}
</InputLabel>
<Input
type={"url"}
value={options.gravatar_server}
onChange={handleChange("gravatar_server")}
required
/>
<FormHelperText id="component-helper-text">
{t("gravatarServerDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("avatarFilePath")}
</InputLabel>
<Input
value={options.avatar_path}
onChange={handleChange("avatar_path")}
required
/>
<FormHelperText id="component-helper-text">
{t("avatarFilePathDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
{options.avatar_size !== "" && (
<SizeInput
value={options.avatar_size}
onChange={handleChange("avatar_size")}
required
min={0}
max={2147483647}
label={t("avatarSize")}
/>
)}
<FormHelperText id="component-helper-text">
{t("avatarSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("smallAvatarSize")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_s}
onChange={handleChange("avatar_size_s")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("mediumAvatarSize")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_m}
onChange={handleChange("avatar_size_m")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("largeAvatarSize")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_l}
onChange={handleChange("avatar_size_l")}
required
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("filePreview")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("officePreviewService")}
</InputLabel>
<Input
value={options.office_preview_service}
onChange={handleChange(
"office_preview_service"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("officePreviewServiceDes")}
<br />
<code>{"{$src}"}</code> -{" "}
{t("officePreviewServiceSrcDes")}
<br />
<code>{"{$srcB64}"}</code> -{" "}
{t("officePreviewServiceSrcB64Des")}
<br />
<code>{"{$name}"}</code> -{" "}
{t("officePreviewServiceName")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
{options.maxEditSize !== "" && (
<SizeInput
value={options.maxEditSize}
onChange={handleChange("maxEditSize")}
required
min={0}
max={2147483647}
label={t("textEditMaxSize")}
/>
)}
<FormHelperText id="component-helper-text">
{t("textEditMaxSizeDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("wopiClient")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
<Trans
ns={"dashboard"}
i18nKey={"settings.wopiClientDes"}
components={[
<Link
key={0}
target={"_blank"}
href={t("wopiDocLink")}
/>,
]}
/>
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.wopi_enabled === "1"
}
onChange={handleCheckChange(
"wopi_enabled"
)}
/>
}
label={t("enableWopi")}
/>
</FormControl>
</div>
{options.wopi_enabled === "1" && (
<>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("wopiEndpoint")}
</InputLabel>
<Input
value={options.wopi_endpoint}
onChange={handleChange(
"wopi_endpoint"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("wopiEndpointDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("wopiSessionTtl")}
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.wopi_session_timeout}
onChange={handleChange(
"wopi_session_timeout"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("wopiSessionTtlDes")}
</FormHelperText>
</FormControl>
</div>
</>
)}
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("thumbnails")}
</Typography>
<div className={classes.form}>
<Alert severity="info">
<Trans
ns={"dashboard"}
i18nKey={"settings.thumbnailDoc"}
components={[
<Link
key={0}
target={"_blank"}
href={t("thumbnailDocLink")}
/>,
]}
/>
</Alert>
</div>
<Typography variant="subtitle1" gutterBottom>
{t("thumbnailBasic")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbWidth")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.thumb_width}
onChange={handleChange("thumb_width")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbHeight")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.thumb_height}
onChange={handleChange("thumb_height")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbSuffix")}
</InputLabel>
<Input
type={"text"}
value={options.thumb_file_suffix}
onChange={handleChange("thumb_file_suffix")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbConcurrent")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: -1,
step: 1,
}}
value={options.thumb_max_task_count}
onChange={handleChange(
"thumb_max_task_count"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("thumbConcurrentDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbFormat")}
</InputLabel>
<Input
type={"test"}
value={options.thumb_encode_method}
onChange={handleChange(
"thumb_encode_method"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("thumbFormatDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("thumbQuality")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
max: 100,
}}
value={options.thumb_encode_quality}
onChange={handleChange(
"thumb_encode_quality"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("thumbQualityDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
{options.thumb_max_src_size !== "" && (
<SizeInput
value={options.thumb_max_src_size}
onChange={handleChange(
"thumb_max_src_size"
)}
required
min={0}
max={2147483647}
label={t("thumbMaxSize")}
/>
)}
<FormHelperText id="component-helper-text">
{t("thumbMaxSizeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.thumb_gc_after_gen ===
"1"
}
onChange={handleCheckChange(
"thumb_gc_after_gen"
)}
/>
}
label={t("thumbGC")}
/>
</FormControl>
</div>
</div>
<Typography variant="subtitle1" gutterBottom>
{t("generators")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<ThumbGenerators
options={options}
setOptions={setOptions}
/>
</div>
</div>
<Typography variant="subtitle1" gutterBottom>
{t("generatorProxy")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
{t("generatorProxyWarning")}
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.thumb_proxy_enabled ===
"1"
}
onChange={handleCheckChange(
"thumb_proxy_enabled"
)}
/>
}
label={t("enableThumbProxy")}
/>
</FormControl>
</div>
{options.thumb_proxy_enabled === "1" && (
<>
<div className={classes.form}>
<PolicySelector
value={options.thumb_proxy_policy}
onChange={handleChange(
"thumb_proxy_policy"
)}
filter={(t) => t.Type !== "local"}
label={t("proxyPolicyList")}
helperText={t("proxyPolicyListDes")}
/>
</div>
</>
)}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,450 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { useDispatch } from "react-redux";
import Dialog from "@material-ui/core/Dialog";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import TextField from "@material-ui/core/TextField";
import DialogActions from "@material-ui/core/DialogActions";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
buttonMargin: {
marginLeft: 8,
},
}));
export default function Mail() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tGlobal } = useTranslation("common");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [test, setTest] = useState(false);
const [tesInput, setTestInput] = useState("");
const [options, setOptions] = useState({
fromName: "",
fromAdress: "",
smtpHost: "",
smtpPort: "",
replyTo: "",
smtpUser: "",
smtpPass: "",
smtpEncryption: "",
mail_keepalive: "30",
over_used_template: "",
mail_activation_template: "",
mail_reset_pwd_template: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const sendTestMail = () => {
setLoading(true);
API.post("/admin/test/mail", {
to: tesInput,
})
.then(() => {
ToggleSnackbar("top", "right", t("testMailSent"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const reload = () => {
API.get("/admin/reload/email")
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
reload();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<Dialog
open={test}
onClose={() => setTest(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">
{t("testSMTPSettings")}
</DialogTitle>
<DialogContent>
<DialogContentText>
<Typography>{t("testSMTPTooltip")}</Typography>
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label={t("recipient")}
value={tesInput}
onChange={(e) => setTestInput(e.target.value)}
type="email"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setTest(false)} color="default">
{tGlobal("cancel")}
</Button>
<Button
onClick={() => sendTestMail()}
disabled={loading}
color="primary"
>
{t("send")}
</Button>
</DialogActions>
</Dialog>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("smtp")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("senderName")}
</InputLabel>
<Input
value={options.fromName}
onChange={handleChange("fromName")}
required
/>
<FormHelperText id="component-helper-text">
{t("senderNameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("senderAddress")}
</InputLabel>
<Input
type={"email"}
required
value={options.fromAdress}
onChange={handleChange("fromAdress")}
/>
<FormHelperText id="component-helper-text">
{t("senderAddressDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpServer")}
</InputLabel>
<Input
value={options.smtpHost}
onChange={handleChange("smtpHost")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpServerDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpPort")}
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.smtpPort}
onChange={handleChange("smtpPort")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpPortDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpUsername")}
</InputLabel>
<Input
value={options.smtpUser}
onChange={handleChange("smtpUser")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpUsernameDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpPassword")}
</InputLabel>
<Input
type={"password"}
value={options.smtpPass}
onChange={handleChange("smtpPass")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpPasswordDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("replyToAddress")}
</InputLabel>
<Input
value={options.replyTo}
onChange={handleChange("replyTo")}
required
/>
<FormHelperText id="component-helper-text">
{t("replyToAddressDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.smtpEncryption === "1"
}
onChange={handleCheckChange(
"smtpEncryption"
)}
/>
}
label={t("enforceSSL")}
/>
<FormHelperText id="component-helper-text">
{t("enforceSSLDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smtpTTL")}
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.mail_keepalive}
onChange={handleChange("mail_keepalive")}
required
/>
<FormHelperText id="component-helper-text">
{t("smtpTTLDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("emailTemplates")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("activateNewUser")}
</InputLabel>
<Input
value={options.mail_activation_template}
onChange={handleChange(
"mail_activation_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
{t("activateNewUserDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("overuseReminder")}
</InputLabel>
<Input
value={options.over_used_template}
onChange={handleChange(
"over_used_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
{tVas("overuseReminderDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("resetPassword")}
</InputLabel>
<Input
value={options.mail_reset_pwd_template}
onChange={handleChange(
"mail_reset_pwd_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
{t("resetPasswordDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
{" "}
<Button
className={classes.buttonMargin}
variant={"outlined"}
color={"primary"}
onClick={() => setTest(true)}
>
{t("sendTestEmail")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,517 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { toggleSnackbar } from "../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
import InputAdornment from "@material-ui/core/InputAdornment";
import { green } from "@material-ui/core/colors";
import { Cancel, CheckCircle, Sync } from "@material-ui/icons";
import IconButton from "@material-ui/core/IconButton";
import { Tooltip } from "@material-ui/core";
import Alert from "@material-ui/lab/Alert";
import Link from "@material-ui/core/Link";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function SiteInformation() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tVas } = useTranslation("dashboard", { keyPrefix: "vas" });
const { t: tGlobal } = useTranslation("dashboard");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
siteURL: "",
siteName: "",
siteTitle: "",
siteKeywords: "",
siteDes: "",
siteScript: "",
siteNotice: "",
pwa_small_icon: "",
pwa_medium_icon: "",
pwa_large_icon: "",
pwa_display: "",
pwa_theme_color: "",
pwa_background_color: "",
vol_content: "",
show_app_promotion: "0",
app_feedback_link: "",
app_forum_link: "",
});
const vol = useMemo(() => {
if (options.vol_content) {
const volJson = atob(options.vol_content);
return JSON.parse(volJson);
}
}, [options]);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const refresh = () =>
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
useEffect(() => {
refresh();
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const syncVol = () => {
setLoading(true);
API.get("/admin/vol/sync")
.then(() => {
refresh();
ToggleSnackbar("top", "right", tVas("volSynced"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("basicInformation")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("mainTitle")}
</InputLabel>
<Input
value={options.siteName}
onChange={handleChange("siteName")}
required
/>
<FormHelperText id="component-helper-text">
{t("mainTitleDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("subTitle")}
</InputLabel>
<Input
value={options.siteTitle}
onChange={handleChange("siteTitle")}
/>
<FormHelperText id="component-helper-text">
{t("subTitleDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteKeywords")}
</InputLabel>
<Input
value={options.siteKeywords}
onChange={handleChange("siteKeywords")}
/>
<FormHelperText id="component-helper-text">
{t("siteKeywordsDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteDescription")}
</InputLabel>
<Input
value={options.siteDes}
onChange={handleChange("siteDes")}
/>
<FormHelperText id="component-helper-text">
{t("siteDescriptionDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("siteURL")}
</InputLabel>
<Input
type={"url"}
value={options.siteURL}
onChange={handleChange("siteURL")}
required
/>
<FormHelperText id="component-helper-text">
{t("siteURLDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("customFooterHTML")}
</InputLabel>
<Input
multiline
value={options.siteScript}
onChange={handleChange("siteScript")}
/>
<FormHelperText id="component-helper-text">
{t("customFooterHTMLDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("announcement")}
</InputLabel>
<Input
placeholder={t("supportHTML")}
multiline
value={options.siteNotice}
onChange={handleChange("siteNotice")}
/>
<FormHelperText id="component-helper-text">
{t("announcementDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{tVas("mobileApp")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
<Typography variant="body2">
<Trans
ns={"dashboard"}
i18nKey={"vas.volPurchase"}
components={[
<Link
key={0}
href={
"https://cloudreve.org/login"
}
target={"_blank"}
/>,
<Link
key={1}
href={
"https://cloudreve.org/ios"
}
target={"_blank"}
/>,
]}
/>
</Typography>
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("iosVol")}
</InputLabel>
<Input
startAdornment={
<InputAdornment position="start">
{vol ? (
<CheckCircle
style={{
color: green[500],
}}
/>
) : (
<Cancel color={"error"} />
)}
</InputAdornment>
}
endAdornment={
<InputAdornment position="end">
<Tooltip
title={tVas("syncLicense")}
>
<IconButton
disabled={loading}
onClick={() => syncVol()}
aria-label="toggle password visibility"
>
<Sync />
</IconButton>
</Tooltip>
</InputAdornment>
}
readOnly
value={
vol ? vol.domain : tGlobal("share.none")
}
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.show_app_promotion ===
"1"
}
onChange={handleOptionChange(
"show_app_promotion"
)}
/>
}
label={tVas("showAppPromotion")}
/>
<FormHelperText id="component-helper-text">
{tVas("showAppPromotionDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appFeedback")}
</InputLabel>
<Input
value={options.app_feedback_link}
onChange={handleChange("app_feedback_link")}
/>
<FormHelperText id="component-helper-text">
{tVas("appLinkDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tVas("appForum")}
</InputLabel>
<Input
value={options.app_forum_link}
onChange={handleChange("app_forum_link")}
/>
<FormHelperText id="component-helper-text">
{tVas("appLinkDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("pwa")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("smallIcon")}
</InputLabel>
<Input
value={options.pwa_small_icon}
onChange={handleChange("pwa_small_icon")}
/>
<FormHelperText id="component-helper-text">
{t("smallIconDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("mediumIcon")}
</InputLabel>
<Input
value={options.pwa_medium_icon}
onChange={handleChange("pwa_medium_icon")}
/>
<FormHelperText id="component-helper-text">
{t("mediumIconDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("largeIcon")}
</InputLabel>
<Input
value={options.pwa_large_icon}
onChange={handleChange("pwa_large_icon")}
/>
<FormHelperText id="component-helper-text">
{t("largeIconDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("displayMode")}
</InputLabel>
<Select
value={options.pwa_display}
onChange={handleChange("pwa_display")}
>
<MenuItem value={"fullscreen"}>
fullscreen
</MenuItem>
<MenuItem value={"standalone"}>
standalone
</MenuItem>
<MenuItem value={"minimal-ui"}>
minimal-ui
</MenuItem>
<MenuItem value={"browser"}>
browser
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("displayModeDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("themeColor")}
</InputLabel>
<Input
value={options.pwa_theme_color}
onChange={handleChange("pwa_theme_color")}
/>
<FormHelperText id="component-helper-text">
{t("themeColorDes")}
</FormHelperText>
</FormControl>
</div>
</div>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("backgroundColor")}
</InputLabel>
<Input
value={options.pwa_background_color}
onChange={handleChange(
"pwa_background_color"
)}
/>
<FormHelperText id="component-helper-text">
{t("backgroundColorDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,465 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import TableHead from "@material-ui/core/TableHead";
import Table from "@material-ui/core/Table";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import TableBody from "@material-ui/core/TableBody";
import { Delete } from "@material-ui/icons";
import IconButton from "@material-ui/core/IconButton";
import TextField from "@material-ui/core/TextField";
import CreateTheme from "../Dialogs/CreateTheme";
import Alert from "@material-ui/lab/Alert";
import Link from "@material-ui/core/Link";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
import { toggleSnackbar } from "../../../redux/explorer";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 500,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
colorContainer: {
display: "flex",
},
colorDot: {
width: 20,
height: 20,
borderRadius: "50%",
marginLeft: 6,
},
}));
export default function Theme() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const { t: tApp } = useTranslation();
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [theme, setTheme] = useState({});
const [options, setOptions] = useState({
themes: "{}",
defaultTheme: "",
home_view_method: "icon",
share_view_method: "list",
});
const [themeConfig, setThemeConfig] = useState({});
const [themeConfigError, setThemeConfigError] = useState({});
const [create, setCreate] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const deleteTheme = (color) => {
if (color === options.defaultTheme) {
ToggleSnackbar(
"top",
"right",
t("cannotDeleteDefaultTheme"),
"warning"
);
return;
}
if (Object.keys(theme).length <= 1) {
ToggleSnackbar("top", "right", t("keepAtLeastOneTheme"), "warning");
return;
}
const themeCopy = { ...theme };
delete themeCopy[color];
const resStr = JSON.stringify(themeCopy);
setOptions({
...options,
themes: resStr,
});
};
const addTheme = (newTheme) => {
setCreate(false);
if (theme[newTheme.palette.primary.main] !== undefined) {
ToggleSnackbar(
"top",
"right",
t("duplicatedThemePrimaryColor"),
"warning"
);
return;
}
const res = {
...theme,
[newTheme.palette.primary.main]: newTheme,
};
const resStr = JSON.stringify(res);
setOptions({
...options,
themes: resStr,
});
};
useEffect(() => {
const res = JSON.parse(options.themes);
const themeString = {};
Object.keys(res).map((k) => {
themeString[k] = JSON.stringify(res[k]);
});
setTheme(res);
setThemeConfig(themeString);
}, [options.themes]);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("themes")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>{t("colors")}</TableCell>
<TableCell>
{t("themeConfig")}
</TableCell>
<TableCell>{t("actions")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(theme).map((k) => (
<TableRow key={k}>
<TableCell
component="th"
scope="row"
>
<div
className={
classes.colorContainer
}
>
<div
style={{
backgroundColor:
theme[k].palette
.primary
.main,
}}
className={
classes.colorDot
}
/>
<div
style={{
backgroundColor:
theme[k].palette
.secondary
.main,
}}
className={
classes.colorDot
}
/>
</div>
</TableCell>
<TableCell>
<TextField
error={themeConfigError[k]}
helperText={
themeConfigError[k] &&
t("wrongFormat")
}
fullWidth
multiline
onChange={(e) => {
setThemeConfig({
...themeConfig,
[k]: e.target.value,
});
}}
onBlur={(e) => {
try {
const res = JSON.parse(
e.target.value
);
if (
!(
"palette" in
res
) ||
!(
"primary" in
res.palette
) ||
!(
"main" in
res.palette
.primary
) ||
!(
"secondary" in
res.palette
) ||
!(
"main" in
res.palette
.secondary
)
) {
throw "error";
}
setTheme({
...theme,
[k]: res,
});
} catch (e) {
setThemeConfigError(
{
...themeConfigError,
[k]: true,
}
);
return;
}
setThemeConfigError({
...themeConfigError,
[k]: false,
});
}}
value={themeConfig[k]}
/>
</TableCell>
<TableCell>
<IconButton
onClick={() =>
deleteTheme(k)
}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div>
<Button
variant="outlined"
color="primary"
style={{ marginTop: 8 }}
onClick={() => setCreate(true)}
>
{t("createNewTheme")}
</Button>
</div>
<Alert severity="info" style={{ marginTop: 8 }}>
<Typography variant="body2">
<Trans
i18nKey={"settings.themeConfigDes"}
ns={"dashboard"}
components={[
<Link
key={0}
href={t("themeConfigDoc")}
target={"_blank"}
/>,
]}
/>
</Typography>
</Alert>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("defaultTheme")}
</InputLabel>
<Select
value={options.defaultTheme}
onChange={handleChange("defaultTheme")}
>
{Object.keys(theme).map((k) => (
<MenuItem key={k} value={k}>
<div
className={
classes.colorContainer
}
>
<div
style={{
backgroundColor:
theme[k].palette
.primary.main,
}}
className={classes.colorDot}
/>
<div
style={{
backgroundColor:
theme[k].palette
.secondary.main,
}}
className={classes.colorDot}
/>
</div>
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
{t("defaultThemeDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("appearance")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("personalFileListView")}
</InputLabel>
<Select
value={options.home_view_method}
onChange={handleChange("home_view_method")}
required
>
<MenuItem value={"icon"}>
{tApp("fileManager.gridViewLarge")}
</MenuItem>
<MenuItem value={"smallIcon"}>
{tApp("fileManager.gridViewSmall")}
</MenuItem>
<MenuItem value={"list"}>
{tApp("fileManager.listView")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("personalFileListViewDes")}
</FormHelperText>
</FormControl>
</div>
</div>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("sharedFileListView")}
</InputLabel>
<Select
value={options.share_view_method}
onChange={handleChange("share_view_method")}
required
>
<MenuItem value={"icon"}>
{tApp("fileManager.gridViewLarge")}
</MenuItem>
<MenuItem value={"smallIcon"}>
{tApp("fileManager.gridViewSmall")}
</MenuItem>
<MenuItem value={"list"}>
{tApp("fileManager.listView")}
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
{t("sharedFileListViewDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
<CreateTheme
onSubmit={addTheme}
open={create}
onClose={() => setCreate(false)}
/>
</div>
);
}

View File

@@ -0,0 +1,248 @@
import React, { useCallback, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Accordion from "@material-ui/core/Accordion";
import AccordionSummary from "@material-ui/core/AccordionSummary";
import AccordionDetails from "@material-ui/core/AccordionDetails";
import Checkbox from "@material-ui/core/Checkbox";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Typography from "@material-ui/core/Typography";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../redux/explorer";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import { Button, TextField } from "@material-ui/core";
import InputAdornment from "@material-ui/core/InputAdornment";
import API from "../../../middleware/Api";
const useStyles = makeStyles((theme) => ({
root: {
width: "100%",
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(15),
color: theme.palette.text.secondary,
},
column: {
flexBasis: "33.33%",
},
details: {
display: "block",
},
}));
const generators = [
{
name: "policyBuiltin",
des: "policyBuiltinDes",
readOnly: true,
},
{
name: "libreOffice",
des: "libreOfficeDes",
enableFlag: "thumb_libreoffice_enabled",
executableSetting: "thumb_libreoffice_path",
inputs: [
{
name: "thumb_libreoffice_exts",
label: "generatorExts",
des: "generatorExtsDes",
},
],
},
{
name: "vips",
des: "vipsDes",
enableFlag: "thumb_vips_enabled",
executableSetting: "thumb_vips_path",
inputs: [
{
name: "thumb_vips_exts",
label: "generatorExts",
des: "generatorExtsDes",
},
],
},
{
name: "ffmpeg",
des: "ffmpegDes",
enableFlag: "thumb_ffmpeg_enabled",
executableSetting: "thumb_ffmpeg_path",
inputs: [
{
name: "thumb_ffmpeg_exts",
label: "generatorExts",
des: "generatorExtsDes",
},
{
name: "thumb_ffmpeg_seek",
label: "ffmpegSeek",
des: "ffmpegSeekDes",
required: true,
},
],
},
{
name: "cloudreveBuiltin",
des: "cloudreveBuiltinDes",
enableFlag: "thumb_builtin_enabled",
},
];
export default function ThumbGenerators({ options, setOptions }) {
const classes = useStyles();
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const testExecutable = (name, executable) => {
setLoading(true);
API.post("/admin/test/thumb", {
name,
executable,
})
.then((response) => {
ToggleSnackbar(
"top",
"right",
t("executableTestSuccess", { version: response.data }),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleEnableChange = (name) => (event) => {
const newOpts = {
...options,
[name]: event.target.checked ? "1" : "0",
};
setOptions(newOpts);
if (
newOpts["thumb_libreoffice_enabled"] === "1" &&
newOpts["thumb_builtin_enabled"] === "0" &&
newOpts["thumb_vips_enabled"] === "0"
) {
ToggleSnackbar(
"top",
"center",
t("thumbDependencyWarning"),
"warning"
);
}
};
return (
<div className={classes.root}>
{generators.map((generator) => (
<Accordion key={generator.name}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-label="Expand"
aria-controls="additional-actions1-content"
id="additional-actions1-header"
>
<FormControlLabel
aria-label="Acknowledge"
onClick={(event) => event.stopPropagation()}
onFocus={(event) => event.stopPropagation()}
control={
<Checkbox
checked={
generator.readOnly ||
options[generator.enableFlag] === "1"
}
onChange={handleEnableChange(
generator.enableFlag
)}
/>
}
label={t(generator.name)}
disabled={generator.readOnly}
/>
</AccordionSummary>
<AccordionDetails className={classes.details}>
<Typography color="textSecondary">
{t(generator.des)}
</Typography>
{generator.executableSetting && (
<FormControl margin="normal" fullWidth>
<TextField
label={t("executable")}
variant="outlined"
value={options[generator.executableSetting]}
onChange={handleChange(
generator.executableSetting
)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Button
disabled={loading}
onClick={() =>
testExecutable(
generator.name,
options[
generator
.executableSetting
]
)
}
color="primary"
>
{t("executableTest")}
</Button>
</InputAdornment>
),
}}
required
/>
<FormHelperText id="component-helper-text">
{t("executableDes")}
</FormHelperText>
</FormControl>
)}
{generator.inputs &&
generator.inputs.map((input) => (
<FormControl
key={input.name}
margin="normal"
fullWidth
>
<TextField
label={t(input.label)}
variant="outlined"
value={options[input.name]}
onChange={handleChange(input.name)}
required={!!input.required}
/>
<FormHelperText id="component-helper-text">
{t(input.des)}
</FormHelperText>
</FormControl>
))}
</AccordionDetails>
</Accordion>
))}
</div>
);
}

View File

@@ -0,0 +1,407 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import SizeInput from "../Common/SizeInput";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function UploadDownload() {
const { t } = useTranslation("dashboard", { keyPrefix: "settings" });
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
max_worker_num: "1",
max_parallel_transfer: "1",
temp_path: "",
chunk_retries: "0",
archive_timeout: "0",
download_timeout: "0",
preview_timeout: "0",
doc_preview_timeout: "0",
upload_credential_timeout: "0",
upload_session_timeout: "0",
slave_api_timeout: "0",
onedrive_monitor_timeout: "0",
share_download_session_timeout: "0",
onedrive_callback_check: "0",
reset_after_upload_failed: "0",
onedrive_source_timeout: "0",
slave_node_retry: "0",
slave_ping_interval: "0",
slave_recover_interval: "0",
slave_transfer_timeout: "0",
use_temp_chunk_buffer: "1",
public_resource_maxage: "0",
});
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "1" : "0";
setOptions({
...options,
[name]: value,
});
};
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", t("saved"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("transportation")}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("workerNum")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.max_worker_num}
onChange={handleChange("max_worker_num")}
required
/>
<FormHelperText id="component-helper-text">
{t("workerNumDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("transitParallelNum")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.max_parallel_transfer}
onChange={handleChange(
"max_parallel_transfer"
)}
required
/>
<FormHelperText id="component-helper-text">
{t("transitParallelNumDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("tempFolder")}
</InputLabel>
<Input
value={options.temp_path}
onChange={handleChange("temp_path")}
required
/>
<FormHelperText id="component-helper-text">
{t("tempFolderDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t("failedChunkRetry")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={options.chunk_retries}
onChange={handleChange("chunk_retries")}
required
/>
<FormHelperText id="component-helper-text">
{t("failedChunkRetryDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.use_temp_chunk_buffer ===
"1"
}
onChange={handleCheckChange(
"use_temp_chunk_buffer"
)}
/>
}
label={t("cacheChunks")}
/>
<FormHelperText id="component-helper-text">
{t("cacheChunksDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.reset_after_upload_failed ===
"1"
}
onChange={handleCheckChange(
"reset_after_upload_failed"
)}
/>
}
label={t("resetConnection")}
/>
<FormHelperText id="component-helper-text">
{t("resetConnectionDes")}
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("expirationDuration")}
</Typography>
<div className={classes.formContainer}>
{[
{
name: "batchDownload",
field: "archive_timeout",
},
{
name: "downloadSession",
field: "download_timeout",
},
{
name: "previewURL",
field: "preview_timeout",
},
{
name: "docPreviewURL",
field: "doc_preview_timeout",
},
{
name: "staticResourceCache",
field: "public_resource_maxage",
des: "staticResourceCacheDes",
},
{
name: "uploadSession",
field: "upload_session_timeout",
des: "uploadSessionDes",
},
{
name: "downloadSessionForShared",
field: "share_download_session_timeout",
des: "downloadSessionForSharedDes",
},
{
name: "onedriveMonitorInterval",
field: "onedrive_monitor_timeout",
des: "onedriveMonitorIntervalDes",
},
{
name: "onedriveCallbackTolerance",
field: "onedrive_callback_check",
des: "onedriveCallbackToleranceDes",
},
{
name: "onedriveDownloadURLCache",
field: "onedrive_source_timeout",
des: "onedriveDownloadURLCacheDes",
},
].map((input) => (
<div key={input.name} className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t(input.name)}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options[input.field]}
onChange={handleChange(input.field)}
required
/>
{input.des && (
<FormHelperText id="component-helper-text">
{t(input.des)}
</FormHelperText>
)}
</FormControl>
</div>
))}
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{t("nodesCommunication")}
</Typography>
<div className={classes.formContainer}>
{[
{
name: "slaveAPIExpiration",
field: "slave_api_timeout",
des: "slaveAPIExpirationDes",
},
{
name: "heartbeatInterval",
field: "slave_ping_interval",
des: "heartbeatIntervalDes",
},
{
name: "heartbeatFailThreshold",
field: "slave_node_retry",
des: "heartbeatFailThresholdDes",
},
{
name: "heartbeatRecoverModeInterval",
field: "slave_recover_interval",
des: "heartbeatRecoverModeIntervalDes",
},
{
name: "slaveTransitExpiration",
field: "slave_transfer_timeout",
des: "slaveTransitExpirationDes",
},
].map((input) => (
<div key={input.name} className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
{t(input.name)}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options[input.field]}
onChange={handleChange(input.field)}
required
/>
<FormHelperText id="component-helper-text">
{t(input.des)}
</FormHelperText>
</FormControl>
</div>
))}
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{t("save")}
</Button>
</div>
</form>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,532 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete, FilterList } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Badge from "@material-ui/core/Badge";
import ShareFilter from "../Dialogs/ShareFilter";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Share() {
const { t } = useTranslation("dashboard", { keyPrefix: "share" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [shares, setShares] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [ids, setIds] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/share/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setIds(response.data.ids);
setShares(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/share/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/share/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = shares.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Tooltip title={tDashboard("user.filter")}>
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("node.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < shares.length
}
checked={
shares.length > 0 &&
selected.length === shares.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 200 }}>
<TableSortLabel
active={orderBy[0] === "source_name"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"source_name",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("objectName")}
{orderBy[0] === "source_name" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 70 }}>
{tDashboard("policy.type")}
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "views"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"views",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("views")}
{orderBy[0] === "views" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "downloads"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"downloads",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("downloads")}
{orderBy[0] === "downloads" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "score"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"score",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("price")}
{orderBy[0] === "score" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
{t("autoExpire")}
</TableCell>
<TableCell style={{ minWidth: 120 }}>
{t("owner")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("createdAt")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{shares.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
<Link
target={"_blank"}
color="inherit"
href={
"/s/" +
ids[row.ID] +
(row.Password === ""
? ""
: "?password=" +
row.Password)
}
>
{row.SourceName}
</Link>
</TableCell>
<TableCell>
{row.Password === ""
? t("public")
: t("private")}
</TableCell>
<TableCell align={"right"}>
{row.Views}
</TableCell>
<TableCell align={"right"}>
{row.Downloads}
</TableCell>
<TableCell align={"right"}>
{row.Score}
</TableCell>
<TableCell>
{row.RemainDownloads > -1 &&
t("afterNDownloads", {
num: row.RemainDownloads,
})}
{row.RemainDownloads === -1 &&
t("none")}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("node.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,83 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Link,
} from "@material-ui/core";
import { Link as RouterLink } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({}));
export default function Aria2Helper(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "task" });
const { t: tCommon } = useTranslation("common");
const classes = useStyles();
return (
<Dialog
open={props.open}
onClose={props.onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{t("howToConfigAria2")}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{t("aria2Des")}
<ul>
<li>
<Trans
ns={"dashboard"}
i18nKey={"task.masterAria2Des"}
components={[
<Link
component={RouterLink}
to={"/admin/node/edit/1"}
key={0}
/>,
]}
/>
</li>
<li>
<Trans
ns={"dashboard"}
i18nKey={"task.slaveAria2Des"}
components={[
<Link
component={RouterLink}
to={"/admin/node/add"}
key={0}
/>,
]}
/>
</li>
</ul>
<Trans
ns={"dashboard"}
i18nKey={"task.editGroupDes"}
components={[
<Link
component={RouterLink}
to={"/admin/group"}
key={0}
/>,
]}
/>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} color="primary" autoFocus>
{tCommon("close")}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,444 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import ShareFilter from "../Dialogs/ShareFilter";
import { sizeToString } from "../../../utils";
import { formatLocalTime } from "../../../utils/datetime";
import Aria2Helper from "./Aria2Helper";
import HelpIcon from "@material-ui/icons/Help";
import { Link as RouterLink } from "react-router-dom";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Download() {
const { t } = useTranslation("dashboard", { keyPrefix: "task" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [downloads, setDownloads] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [helperOpen, setHelperOpen] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/download/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setDownloads(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/download/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/download/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = downloads.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<Aria2Helper
open={helperOpen}
onClose={() => setHelperOpen(false)}
/>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => setHelperOpen(true)}
>
<HelpIcon /> {" "}
{t("howToConfigAria2")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < downloads.length
}
checked={
downloads.length > 0 &&
selected.length === downloads.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{t("srcURL")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{tDashboard("user.status")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("node")}
</TableCell>
<TableCell
style={{ minWidth: 150 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "total_size"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"total_size",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{tDashboard("file.size")}
{orderBy[0] === "total_size" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("createdBy")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{tDashboard("file.createdAt")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{downloads.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{row.Source}
</TableCell>
<TableCell>
{row.Status === 0 && t("ready")}
{row.Status === 1 && t("downloading")}
{row.Status === 2 && t("paused")}
{row.Status === 3 && t("error")}
{row.Status === 4 && t("finished")}
{row.Status === 5 && t("canceled")}
{row.Status === 6 && t("unknown")}
{row.Status === 7 && t("seeding")}
</TableCell>
<TableCell>
{row.NodeID <= 1 && (
<Link
component={RouterLink}
to={"/admin/node/edit/1"}
>
{tDashboard("node.master")}
</Link>
)}
{row.NodeID > 1 && (
<Link
component={RouterLink}
to={
"/admin/node/edit/" +
row.NodeID
}
>
{tDashboard("node.slave")}#
{row.NodeID}
</Link>
)}
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.TotalSize)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: tDashboard(
"file.unknownUploader"
)}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,389 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import IconButton from "@material-ui/core/IconButton";
import { Delete } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import ShareFilter from "../Dialogs/ShareFilter";
import { getTaskProgress, getTaskStatus, getTaskType } from "../../../config";
import { formatLocalTime } from "../../../utils/datetime";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Task() {
const { t } = useTranslation("dashboard", { keyPrefix: "task" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [tasks, setTasks] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/task/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setTasks(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/task/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/task/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("taskDeleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = tasks.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const getError = (error) => {
if (error === "") {
return "-";
}
try {
const res = JSON.parse(error);
return res.msg;
} catch (e) {
return t("unknown");
}
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{tDashboard("user.selectedObjects", {
num: selected.length,
})}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < tasks.length
}
checked={
tasks.length > 0 &&
selected.length === tasks.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
{tDashboard("policy.type")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{tDashboard("user.status")}
</TableCell>
<TableCell style={{ minWidth: 90 }}>
{t("lastProgress")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{t("errorMsg")}
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{t("createdBy")}
</TableCell>
<TableCell style={{ minWidth: 150 }}>
{tDashboard("file.createdAt")}
</TableCell>
<TableCell style={{ minWidth: 80 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tasks.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{getTaskType(row.Type)}
</TableCell>
<TableCell>
{getTaskStatus(row.Status)}
</TableCell>
<TableCell>
{getTaskProgress(
row.Type,
row.Progress
)}
</TableCell>
<TableCell className={classes.noWrap}>
{getError(row.Error)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: t("unknown")}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(row.CreatedAt)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("policy.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import UserForm from "./UserForm";
import { toggleSnackbar } from "../../../redux/explorer";
export default function EditUserPreload() {
const [user, setUser] = useState({});
const { id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setUser({});
API.get("/admin/user/" + id)
.then((response) => {
// 整型转换
["Status", "GroupID", "Score"].forEach((v) => {
response.data[v] = response.data[v].toString();
});
setUser(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return <div>{user.ID !== undefined && <UserForm user={user} />}</div>;
}

View File

@@ -0,0 +1,541 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Paper from "@material-ui/core/Paper";
import Button from "@material-ui/core/Button";
import TableContainer from "@material-ui/core/TableContainer";
import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import { sizeToString } from "../../../utils";
import TableBody from "@material-ui/core/TableBody";
import TablePagination from "@material-ui/core/TablePagination";
import { useHistory } from "react-router";
import IconButton from "@material-ui/core/IconButton";
import { Block, Delete, Edit, FilterList } from "@material-ui/icons";
import Tooltip from "@material-ui/core/Tooltip";
import Checkbox from "@material-ui/core/Checkbox";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { lighten } from "@material-ui/core";
import Link from "@material-ui/core/Link";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import UserFilter from "../Dialogs/UserFilter";
import Badge from "@material-ui/core/Badge";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Group() {
const { t } = useTranslation("dashboard", { keyPrefix: "user" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const history = useHistory();
const theme = useTheme();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/user/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/user/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "用户已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/user/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", t("deleted"), "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const block = (id) => {
setLoading(true);
API.patch("/admin/user/ban/" + id)
.then((response) => {
setUsers(
users.map((v) => {
if (v.ID === id) {
const newUser = { ...v, Status: response.data };
return newUser;
}
return v;
})
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = users.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<UserFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
style={{ alignSelf: "center" }}
color={"primary"}
onClick={() => history.push("/admin/user/add")}
variant={"contained"}
>
{t("new")}
</Button>
<div className={classes.headerRight}>
<Tooltip title={t("filter")}>
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
{tDashboard("policy.refresh")}
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
{t("selectedObjects", { num: selected.length })}
</Typography>
<Tooltip title={tDashboard("policy.delete")}>
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < users.length
}
checked={
users.length > 0 &&
selected.length === users.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 59 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{tDashboard("node.#")}
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
<TableSortLabel
active={orderBy[0] === "nick"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"nick",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("nick")}
{orderBy[0] === "nick" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 170 }}>
<TableSortLabel
active={orderBy[0] === "email"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"email",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("email")}
{orderBy[0] === "email" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 70 }}>
{t("group")}
</TableCell>
<TableCell style={{ minWidth: 50 }}>
{t("status")}
</TableCell>
<TableCell
align={"right"}
style={{ minWidth: 80 }}
>
<TableSortLabel
active={orderBy[0] === "storage"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"storage",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
{t("usedStorage")}
{orderBy[0] === "storage" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 100 }}>
{tDashboard("policy.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Nick}</TableCell>
<TableCell>{row.Email}</TableCell>
<TableCell>
<Link
href={
"/admin/group/edit/" +
row.Group.ID
}
>
{row.Group.Name}
</Link>
</TableCell>
<TableCell>
{row.Status === 0 && (
<Typography
style={{
color: theme.palette.success
.main,
}}
variant={"body2"}
>
{t("active")}
</Typography>
)}
{row.Status === 1 && (
<Typography
color={"textSecondary"}
variant={"body2"}
>
{t("notActivated")}
</Typography>
)}
{row.Status === 2 && (
<Typography
color={"error"}
variant={"body2"}
>
{t("banned")}
</Typography>
)}
{row.Status === 3 && (
<Typography
color={"error"}
variant={"body2"}
>
{t("bannedBySys")}
</Typography>
)}
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.Storage)}
</TableCell>
<TableCell>
<Tooltip
title={tDashboard("node.edit")}
>
<IconButton
onClick={() =>
history.push(
"/admin/user/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title={t("toggleBan")}>
<IconButton
disabled={loading}
onClick={() => block(row.ID)}
size={"small"}
>
<Block />
</IconButton>
</Tooltip>
<Tooltip
title={tDashboard("node.delete")}
>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

View File

@@ -0,0 +1,273 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../redux/explorer";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function UserForm(props) {
const { t } = useTranslation("dashboard", { keyPrefix: "user" });
const { t: tDashboard } = useTranslation("dashboard");
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [user, setUser] = useState(
props.user
? props.user
: {
ID: 0,
Email: "",
Nick: "",
Password: "", // 为空时只读
Status: "0", // 转换类型
GroupID: "2", // 转换类型
Score: "0", // 转换类型
TwoFactor: "",
}
);
const [groups, setGroups] = useState([]);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const handleChange = (name) => (event) => {
setUser({
...user,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
const userCopy = { ...user };
// 整型转换
["Status", "GroupID", "Score"].forEach((v) => {
userCopy[v] = parseInt(userCopy[v]);
});
setLoading(true);
API.post("/admin/user", {
user: userCopy,
password: userCopy.Password,
})
.then(() => {
history.push("/admin/user");
ToggleSnackbar(
"top",
"right",
props.user ? t("saved") : t("added"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const groupSelections = useMemo(
() =>
groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
}),
[groups]
);
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{user.ID === 0 && t("new")}
{user.ID !== 0 && t("editUser", { nick: user.Nick })}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("email")}
</InputLabel>
<Input
value={user.Email}
type={"email"}
onChange={handleChange("Email")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("nick")}
</InputLabel>
<Input
value={user.Nick}
onChange={handleChange("Nick")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("password")}
</InputLabel>
<Input
type={"password"}
value={user.Password}
onChange={handleChange("Password")}
required={user.ID === 0}
/>
<FormHelperText id="component-helper-text">
{user.ID !== 0 && t("passwordDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("group")}
</InputLabel>
<Select
value={user.GroupID}
onChange={handleChange("GroupID")}
required
>
{groupSelections}
</Select>
<FormHelperText id="component-helper-text">
{t("groupDes")}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("status")}
</InputLabel>
<Select
value={user.Status}
onChange={handleChange("Status")}
required
>
<MenuItem value={"0"}>
{t("active")}
</MenuItem>
<MenuItem value={"1"}>
{t("notActivated")}
</MenuItem>
<MenuItem value={"2"}>
{t("banned")}
</MenuItem>
<MenuItem value={"3"}>
{t("bannedBySys")}
</MenuItem>
</Select>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{tDashboard("vas.credits")}
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={user.Score}
onChange={handleChange("Score")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
{t("2FASecret")}
</InputLabel>
<Input
value={user.TwoFactor}
onChange={handleChange("TwoFactor")}
/>
</FormControl>
<FormHelperText id="component-helper-text">
{t("2FASecretDes")}
</FormHelperText>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
{tDashboard("settings.save")}
</Button>
</div>
</form>
</div>
);
}