SQL expression rewrite, Pagination, some frontend stuff
This commit is contained in:
@@ -11,7 +11,11 @@ import {LocaleContext} from "shared/locale";
|
||||
import './res/adminlte.min.css';
|
||||
|
||||
// views
|
||||
import View404 from "./views/404";
|
||||
const Overview = lazy(() => import('./views/overview'));
|
||||
const UserListView = lazy(() => import('./views/user-list'));
|
||||
const GroupListView = lazy(() => import('./views/group-list'));
|
||||
const EditGroupView = lazy(() => import('./views/group-edit'));
|
||||
|
||||
|
||||
export default function AdminDashboard(props) {
|
||||
@@ -22,14 +26,21 @@ export default function AdminDashboard(props) {
|
||||
|
||||
const {currentLocale, requestModules, translate: L} = useContext(LocaleContext);
|
||||
|
||||
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
|
||||
setDialog({ show: true, message: message, title: title, options: options, onOption: onOption });
|
||||
}, []);
|
||||
|
||||
const hideDialog = useCallback(() => {
|
||||
setDialog({show: false});
|
||||
}, []);
|
||||
|
||||
const showDialog = useCallback((message, title, options=["Close"], onOption = null) => {
|
||||
setDialog({
|
||||
show: true, message:
|
||||
message,
|
||||
title: title,
|
||||
options: options,
|
||||
onOption: onOption,
|
||||
onClose: hideDialog
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(api, ["general", "admin"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
@@ -52,7 +63,12 @@ export default function AdminDashboard(props) {
|
||||
<section className={"content"}>
|
||||
<Suspense fallback={<div>{L("general.loading")}... </div>}>
|
||||
<Routes>
|
||||
<Route path={"/admin"} element={<Overview {...controlObj} />}/>
|
||||
<Route path={"/admin/dashboard"} element={<Overview {...controlObj} />}/>
|
||||
<Route path={"/admin/users"} element={<UserListView {...controlObj} />}/>
|
||||
<Route path={"/admin/groups"} element={<GroupListView {...controlObj} />}/>
|
||||
<Route path={"/admin/group/:groupId"} element={<EditGroupView {...controlObj} />}/>
|
||||
<Route path={"*"} element={<View404 />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
{/*<Route exact={true} path={"/admin/users"}><UserOverview {...this.controlObj} /></Route>
|
||||
|
||||
@@ -85,8 +85,6 @@ export default function App() {
|
||||
|
||||
*/
|
||||
|
||||
console.log(loaded, user, api.loggedIn);
|
||||
|
||||
if (!loaded) {
|
||||
if (error) {
|
||||
return <Alert severity={"error"} title={L("general.error_occurred")}>
|
||||
|
||||
@@ -92,7 +92,9 @@ export default function Sidebar(props) {
|
||||
<div className={"os-content"} style={{padding: "0px 0px", height: "100%", width: "100%"}}>
|
||||
<div className="user-panel mt-3 pb-3 mb-3 d-flex">
|
||||
<div className="info">
|
||||
<a href="#" className="d-block">Logged in as: {api.user.name}</a>
|
||||
<a href="#" className="d-block">{L("account.logged_in_as")}:
|
||||
<Link to={"/admin/user/" + api.user.id}>{api.user.name}</Link>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav className={"mt-2"}>
|
||||
|
||||
5
react/admin-panel/src/views/404.js
Normal file
5
react/admin-panel/src/views/404.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export default function View404(props) {
|
||||
return <b>Not found</b>
|
||||
}
|
||||
35
react/admin-panel/src/views/group-edit.js
Normal file
35
react/admin-panel/src/views/group-edit.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import {useNavigate, useParams} from "react-router-dom";
|
||||
|
||||
|
||||
export default function EditGroupView(props) {
|
||||
|
||||
// const [groupId, setGroupId] = useState(props?.match?.groupId !== "new" ? parseInt(props.match.groupId) : null);
|
||||
|
||||
const { groupId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [fetchGroup, setFetchGroup] = useState(groupId !== "new");
|
||||
const [group, setGroup] = useState(null);
|
||||
|
||||
const onFetchGroup = useCallback((force = false) => {
|
||||
if (force || fetchGroup) {
|
||||
setFetchGroup(false);
|
||||
props.api.getGroup(groupId).then(res => {
|
||||
if (!res.success) {
|
||||
props.showDialog(res.msg, "Error fetching group");
|
||||
navigate("/admin/groups");
|
||||
} else {
|
||||
setGroup(res.group);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onFetchGroup();
|
||||
}, []);
|
||||
|
||||
return <></>
|
||||
|
||||
}
|
||||
80
react/admin-panel/src/views/group-list.js
Normal file
80
react/admin-panel/src/views/group-list.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import {Link, useNavigate} from "react-router-dom";
|
||||
import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||
import {Button, IconButton} from "@material-ui/core";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
|
||||
export default function GroupListView(props) {
|
||||
|
||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
alert(data.msg);
|
||||
}
|
||||
});
|
||||
}, [currentLocale]);
|
||||
|
||||
const onFetchGroups = useCallback(async (page, count, orderBy, sortOrder) => {
|
||||
let res = await props.api.fetchGroups(page, count, orderBy, sortOrder);
|
||||
if (res.success) {
|
||||
return Promise.resolve([res.groups, res.pagination]);
|
||||
} else {
|
||||
props.showAlert("Error fetching groups", res.msg);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const actionColumn = (() => {
|
||||
let column = new DataColumn(L("general.actions"), null, false);
|
||||
column.renderData = (entry) => <>
|
||||
<IconButton size={"small"} title={L("general.edit")} onClick={() => navigate("/admin/group/" + entry.id)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
return column;
|
||||
})();
|
||||
|
||||
const columnDefinitions = [
|
||||
new NumericColumn(L("general.id"), "id"),
|
||||
new StringColumn(L("group.name"), "name"),
|
||||
new NumericColumn(L("group.member_count"), "memberCount"),
|
||||
actionColumn,
|
||||
];
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>Users</h1>
|
||||
</div>
|
||||
<div className={"col-sm-6"}>
|
||||
<ol className={"breadcrumb float-sm-right"}>
|
||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||
<li className="breadcrumb-item active">Groups</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<Link to="/admin/group/new">
|
||||
<Button variant={"outlined"} startIcon={<AddIcon />} size={"small"}>
|
||||
{L("general.create_new")}
|
||||
</Button>
|
||||
</Link>
|
||||
<DataTable className={"table table-striped"}
|
||||
fetchData={onFetchGroups}
|
||||
placeholder={"No groups to display"}
|
||||
columns={columnDefinitions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -1,9 +1,33 @@
|
||||
import * as React from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {format, getDaysInMonth} from "date-fns";
|
||||
import {Collapse} from "react-collapse";
|
||||
import {Bar} from "react-chartjs-2";
|
||||
import {CircularProgress, Icon} from "@material-ui/core";
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
|
||||
export default function Overview(props) {
|
||||
|
||||
const [fetchStats, setFetchStats] = useState(true);
|
||||
const [stats, setStats] = useState(null);
|
||||
|
||||
const onFetchStats = useCallback((force = false) => {
|
||||
if (force || fetchStats) {
|
||||
setFetchStats(false);
|
||||
props.api.getStats().then((res) => {
|
||||
if (res.success) {
|
||||
setStats(res.data);
|
||||
} else {
|
||||
props.showDialog("Error fetching stats: " + res.msg, "Error fetching stats");
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [fetchStats]);
|
||||
|
||||
useEffect(() => {
|
||||
onFetchStats();
|
||||
}, []);
|
||||
|
||||
const today = new Date();
|
||||
const numDays = getDaysInMonth(today);
|
||||
|
||||
@@ -46,6 +70,8 @@ export default function Overview(props) {
|
||||
}
|
||||
*/
|
||||
|
||||
console.log(stats);
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
@@ -63,57 +89,28 @@ export default function Overview(props) {
|
||||
</div>
|
||||
</div>
|
||||
<section className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row"}>
|
||||
<div className={"col-lg-3 col-6"}>
|
||||
<div className="small-box bg-info">
|
||||
<div className={"inner"}>
|
||||
{stats ?
|
||||
<>
|
||||
<h3>{stats.userCount}</h3>
|
||||
<p>Users registered</p>
|
||||
</> : <CircularProgress variant={"determinate"} />
|
||||
}
|
||||
</div>
|
||||
<div className="icon">
|
||||
<Icon icon={"users"} />
|
||||
</div>
|
||||
<Link to={"/admin/users"} className="small-box-footer">
|
||||
More info <Icon icon={"arrow-circle-right"}/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
}
|
||||
|
||||
/*
|
||||
export default class Overview extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
chartVisible : true,
|
||||
statusVisible : true,
|
||||
userCount: 0,
|
||||
notificationCount: 0,
|
||||
visitorsTotal: 0,
|
||||
visitors: { },
|
||||
server: { load_avg: ["Unknown"] },
|
||||
errors: []
|
||||
}
|
||||
}
|
||||
|
||||
removeError(i) {
|
||||
if (i >= 0 && i < this.state.errors.length) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.splice(i, 1);
|
||||
this.setState({...this.state, errors: errors});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.parent.api.getStats().then((res) => {
|
||||
if(!res.success) {
|
||||
let errors = this.state.errors.slice();
|
||||
errors.push({ message: res.msg, title: "Error fetching Stats" });
|
||||
this.setState({ ...this.state, errors: errors });
|
||||
} else {
|
||||
this.setState({
|
||||
...this.state,
|
||||
userCount: res.userCount,
|
||||
pageCount: res.pageCount,
|
||||
visitors: res.visitors,
|
||||
visitorsTotal: res.visitorsTotal,
|
||||
server: res.server
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
}
|
||||
}*/
|
||||
90
react/admin-panel/src/views/user-list.js
Normal file
90
react/admin-panel/src/views/user-list.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import {Link, Navigate, useNavigate} from "react-router-dom";
|
||||
import {useCallback, useContext, useEffect} from "react";
|
||||
import {LocaleContext} from "shared/locale";
|
||||
import {DataColumn, DataTable, NumericColumn, StringColumn} from "shared/elements/data-table";
|
||||
import {Button, IconButton} from "@material-ui/core";
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import {Chip} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
|
||||
export default function UserListView(props) {
|
||||
|
||||
const {translate: L, requestModules, currentLocale} = useContext(LocaleContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
requestModules(props.api, ["general", "account"], currentLocale).then(data => {
|
||||
if (!data.success) {
|
||||
props.showDialog("Error fetching translations: " + data.msg);
|
||||
}
|
||||
});
|
||||
}, [currentLocale]);
|
||||
|
||||
const onFetchUsers = useCallback(async (page, count, orderBy, sortOrder) => {
|
||||
let res = await props.api.fetchUsers(page, count, orderBy, sortOrder);
|
||||
if (res.success) {
|
||||
return Promise.resolve([res.users, res.pagination]);
|
||||
} else {
|
||||
props.showDialog(res.msg, "Error fetching users");
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const groupColumn = (() => {
|
||||
let column = new DataColumn(L("account.groups"), "groups");
|
||||
column.renderData = (entry) => {
|
||||
return Object.values(entry.groups).map(group => <Chip key={"group-" + group.id} label={group.name}/>)
|
||||
}
|
||||
return column;
|
||||
})();
|
||||
|
||||
const actionColumn = (() => {
|
||||
let column = new DataColumn(L("general.actions"), null, false);
|
||||
column.renderData = (entry) => <>
|
||||
<IconButton size={"small"} title={L("general.edit")} onClick={() => navigate("/admin/user/" + entry.id)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
return column;
|
||||
})();
|
||||
|
||||
const columnDefinitions = [
|
||||
new NumericColumn(L("general.id"), "id"),
|
||||
new StringColumn(L("account.username"), "name"),
|
||||
new StringColumn(L("account.email"), "email"),
|
||||
groupColumn,
|
||||
new StringColumn(L("account.full_name"), "full_name"),
|
||||
actionColumn,
|
||||
];
|
||||
|
||||
return <>
|
||||
<div className={"content-header"}>
|
||||
<div className={"container-fluid"}>
|
||||
<div className={"row mb-2"}>
|
||||
<div className={"col-sm-6"}>
|
||||
<h1 className={"m-0 text-dark"}>Users</h1>
|
||||
</div>
|
||||
<div className={"col-sm-6"}>
|
||||
<ol className={"breadcrumb float-sm-right"}>
|
||||
<li className={"breadcrumb-item"}><Link to={"/admin/dashboard"}>Home</Link></li>
|
||||
<li className="breadcrumb-item active">Users</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"content"}>
|
||||
<div className={"container-fluid"}>
|
||||
<Link to="/admin/user/new">
|
||||
<Button variant={"outlined"} startIcon={<AddIcon />} size={"small"}>
|
||||
{L("general.create_new")}
|
||||
</Button>
|
||||
</Link>
|
||||
<DataTable className={"table table-striped"}
|
||||
fetchData={onFetchUsers}
|
||||
placeholder={"No users to display"} columns={columnDefinitions} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
Reference in New Issue
Block a user