SQL expression rewrite, Pagination, some frontend stuff

This commit is contained in:
2023-01-05 22:47:17 +01:00
parent 4bfd6754cf
commit 99bfd7e505
61 changed files with 1745 additions and 570 deletions

View File

@@ -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>

View File

@@ -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")}>

View File

@@ -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")}:&nbsp;
<Link to={"/admin/user/" + api.user.id}>{api.user.name}</Link>
</a>
</div>
</div>
<nav className={"mt-2"}>

View File

@@ -0,0 +1,5 @@
export default function View404(props) {
return <b>Not found</b>
}

View 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 <></>
}

View 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>
</>
}

View File

@@ -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() {
}
}*/

View 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>
</>
}