admin page improvements

This commit is contained in:
Kieran 2022-06-13 21:26:42 +01:00
parent 7ea99de160
commit 1907e3261b
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 106 additions and 41 deletions

View File

@ -13,14 +13,16 @@ public class AdminController : Controller
private readonly IFileMetadataStore _fileMetadata;
private readonly IFileInfoManager _fileInfo;
private readonly IUserStore _userStore;
private readonly IUserUploadsStore _userUploads;
public AdminController(IFileStore fileStore, IUserStore userStore, IFileInfoManager fileInfo,
IFileMetadataStore fileMetadata)
IFileMetadataStore fileMetadata, IUserUploadsStore userUploads)
{
_fileStore = fileStore;
_userStore = userStore;
_fileInfo = fileInfo;
_fileMetadata = fileMetadata;
_userUploads = userUploads;
}
/// <summary>
@ -63,9 +65,23 @@ public class AdminController : Controller
/// <returns></returns>
[HttpPost]
[Route("user")]
public async Task<RenderedResults<PrivateVoidUser>> ListUsers([FromBody] PagedRequest request)
public async Task<RenderedResults<AdminListedUser>> ListUsers([FromBody] PagedRequest request)
{
var result = await _userStore.ListUsers(request);
return await result.GetResults();
var ret = await result.Results.SelectAwait(async a =>
{
var uploads = await _userUploads.ListFiles(a.Id, new(0, int.MaxValue));
return new AdminListedUser(a, uploads.TotalResults);
}).ToListAsync();
return new()
{
PageSize = request.PageSize,
Page = request.Page,
TotalResults = result.TotalResults,
Results = ret
};
}
public record AdminListedUser(PrivateVoidUser User, int Uploads);
}

View File

@ -4,9 +4,31 @@
margin-right: auto;
}
.admin h2 {
background-color: #222;
padding: 10px;
}
.admin table {
width: 100%;
word-break: keep-all;
text-overflow: ellipsis;
white-space: nowrap;
border-collapse: collapse;
}
.admin table th {
background-color: #222;
text-align: start;
}
.admin table tr:nth-child(2n) {
background-color: #111;
}
.admin .btn {
padding: 5px 8px;
border-radius: 3px;
font-size: small;
margin: 2px;
}

View File

@ -4,6 +4,7 @@ import {FileList} from "../FileList";
import {UserList} from "./UserList";
import {Navigate} from "react-router-dom";
import {useApi} from "../Api";
import {VoidButton} from "../VoidButton";
export function Admin() {
const auth = useSelector((state) => state.login.jwt);
@ -11,7 +12,6 @@ export function Admin() {
async function deleteFile(e, id) {
e.target.disabled = true;
if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
let req = await AdminApi.deleteFile(id);
if (req.ok) {
@ -20,7 +20,6 @@ export function Admin() {
alert("Failed to delete file!");
}
}
e.target.disabled = false;
}
if (!auth) {
@ -28,13 +27,13 @@ export function Admin() {
} else {
return (
<div className="admin">
<h4>Users</h4>
<h2>Users</h2>
<UserList/>
<h4>Files</h4>
<h2>Files</h2>
<FileList loadPage={AdminApi.fileList} actions={(i) => {
return <td>
<button onClick={(e) => deleteFile(e, i.id)}>Delete</button>
<VoidButton onClick={(e) => deleteFile(e, i.id)}>Delete</VoidButton>
</td>
}}/>
</div>

View File

@ -5,6 +5,7 @@ import {useApi} from "../Api";
import {logout} from "../LoginState";
import {PageSelector} from "../PageSelector";
import moment from "moment";
import {VoidButton} from "../VoidButton";
export function UserList() {
const {AdminApi} = useApi();
@ -19,7 +20,7 @@ export function UserList() {
page: page,
pageSize,
sortBy: PagedSortBy.Date,
sortOrder: PageSortOrder.Asc
sortOrder: PageSortOrder.Dsc
};
let req = await AdminApi.userList(pageReq);
if (req.ok) {
@ -31,17 +32,17 @@ export function UserList() {
}
}
function renderUser(u) {
function renderUser(obj) {
const user = obj.user;
return (
<tr key={u.id}>
<td><a href={`/u/${u.id}`}>{u.id.substring(0, 4)}..</a></td>
<td>{moment(u.created).fromNow()}</td>
<td>{moment(u.lastLogin).fromNow()}</td>
<td>0</td>
<td>{u.roles.join(", ")}</td>
<tr key={user.id}>
<td><a href={`/u/${user.id}`}>{user.displayName}</a></td>
<td>{moment(user.created).fromNow()}</td>
<td>{moment(user.lastLogin).fromNow()}</td>
<td>{obj.uploads}</td>
<td>
<button>Delete</button>
<button>SetRoles</button>
<VoidButton>Delete</VoidButton>
<VoidButton>SetRoles</VoidButton>
</td>
</tr>
);
@ -59,12 +60,11 @@ export function UserList() {
<table>
<thead>
<tr>
<td>Id</td>
<td>Created</td>
<td>Last Login</td>
<td>Files</td>
<td>Roles</td>
<td>Actions</td>
<th>Name</th>
<th>Created</th>
<th>Last Login</th>
<th>Files</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -73,8 +73,11 @@ export function UserList() {
<tbody>
<tr>
<td>
{users ? <PageSelector onSelectPage={(x) => setPage(x)} page={page} total={users.totalResults}
pageSize={pageSize}/> : null}
{users ? <PageSelector
onSelectPage={(x) => setPage(x)}
page={page}
total={users.totalResults}
pageSize={pageSize}/> : null}
</td>
</tr>
</tbody>

View File

@ -3,4 +3,14 @@ table.file-list {
word-break: keep-all;
text-overflow: ellipsis;
white-space: nowrap;
border-collapse: collapse;
}
table.file-list tr:nth-child(2n) {
background-color: #111;
}
table.file-list th {
background-color: #222;
text-align: start;
}

View File

@ -14,7 +14,7 @@ export function FileList(props) {
const dispatch = useDispatch();
const [files, setFiles] = useState();
const [page, setPage] = useState(0);
const pageSize = 10;
const pageSize = 20;
const [accessDenied, setAccessDenied] = useState(false);
async function loadFileList() {
@ -62,16 +62,18 @@ export function FileList(props) {
<table className="file-list">
<thead>
<tr>
<td>Id</td>
<td>Name</td>
<td>Uploaded</td>
<td>Size</td>
<td>Egress</td>
{actions ? <td>Actions</td> : null}
<th>Id</th>
<th>Name</th>
<th>Uploaded</th>
<th>Size</th>
<th>Egress</th>
{actions ? <th>Actions</th> : null}
</tr>
</thead>
<tbody>
{files ? files.results.map(a => renderItem(a)) : <tr><td colSpan={99}>No files</td></tr>}
{files ? files.results.map(a => renderItem(a)) : <tr>
<td colSpan={99}>No files</td>
</tr>}
</tbody>
<tbody>
<tr>

View File

@ -86,9 +86,10 @@ export function FileUpload(props) {
* @param segment {ArrayBuffer}
* @param id {string}
* @param editSecret {string?}
* @param fullDigest {string?} Full file hash
* @returns {Promise<any>}
*/
async function xhrSegment(segment, id, editSecret) {
async function xhrSegment(segment, id, editSecret, fullDigest) {
setUState(UploadState.Hashing);
const digest = await crypto.subtle.digest(DigestAlgo, segment);
setUState(UploadState.Uploading);
@ -116,6 +117,7 @@ export function FileUpload(props) {
req.setRequestHeader("V-Content-Type", props.file.type);
req.setRequestHeader("V-Filename", props.file.name);
req.setRequestHeader("V-Digest", buf2hex(digest));
req.setRequestHeader("V-Full-Digest", fullDigest);
if (auth) {
req.setRequestHeader("Authorization", `Bearer ${auth}`);
}
@ -134,12 +136,14 @@ export function FileUpload(props) {
// upload file in segments of 50MB
const UploadSize = 50_000_000;
let digest = await crypto.subtle.digest(DigestAlgo, await props.file.arrayBuffer());
let xhr = null;
const segments = props.file.size / UploadSize;
for (let s = 0; s < segments; s++) {
let offset = s * UploadSize;
let slice = props.file.slice(offset, offset + UploadSize, props.file.type);
xhr = await xhrSegment(await slice.arrayBuffer(), xhr?.file?.id, xhr?.file?.metadata?.editSecret);
let segment = await slice.arrayBuffer();
xhr = await xhrSegment(segment, xhr?.file?.id, xhr?.file?.metadata?.editSecret, buf2hex(digest));
if (!xhr.ok) {
break;
}

View File

@ -2,6 +2,7 @@
display: grid;
grid-auto-flow: column;
width: min-content;
margin-top: 10px;
}
.page-buttons > div {
@ -11,6 +12,11 @@
cursor: pointer;
}
.page-buttons > div.active {
background-color: #333;
font-weight: bold;
}
.page-buttons > div:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
@ -25,3 +31,4 @@
line-height: 32px;
margin-left: 10px;
}

View File

@ -6,7 +6,7 @@ export function PageSelector(props) {
const page = props.page;
const onSelectPage = props.onSelectPage;
const options = {
showPages: 2,
showPages: 3,
...(props.options || {})
};
@ -17,7 +17,9 @@ export function PageSelector(props) {
let buttons = [];
for (let x = first; x <= last; x++) {
buttons.push(<div onClick={(e) => onSelectPage(x)} key={x}>{x+1}</div>);
buttons.push(<div onClick={(e) => onSelectPage(x)} key={x} className={page === x ? "active" : null}>
{x + 1}
</div>);
}
return (