forked from Kieran/void.cat
admin page improvements
This commit is contained in:
parent
7ea99de160
commit
1907e3261b
@ -13,14 +13,16 @@ public class AdminController : Controller
|
|||||||
private readonly IFileMetadataStore _fileMetadata;
|
private readonly IFileMetadataStore _fileMetadata;
|
||||||
private readonly IFileInfoManager _fileInfo;
|
private readonly IFileInfoManager _fileInfo;
|
||||||
private readonly IUserStore _userStore;
|
private readonly IUserStore _userStore;
|
||||||
|
private readonly IUserUploadsStore _userUploads;
|
||||||
|
|
||||||
public AdminController(IFileStore fileStore, IUserStore userStore, IFileInfoManager fileInfo,
|
public AdminController(IFileStore fileStore, IUserStore userStore, IFileInfoManager fileInfo,
|
||||||
IFileMetadataStore fileMetadata)
|
IFileMetadataStore fileMetadata, IUserUploadsStore userUploads)
|
||||||
{
|
{
|
||||||
_fileStore = fileStore;
|
_fileStore = fileStore;
|
||||||
_userStore = userStore;
|
_userStore = userStore;
|
||||||
_fileInfo = fileInfo;
|
_fileInfo = fileInfo;
|
||||||
_fileMetadata = fileMetadata;
|
_fileMetadata = fileMetadata;
|
||||||
|
_userUploads = userUploads;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -63,9 +65,23 @@ public class AdminController : Controller
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("user")]
|
[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);
|
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);
|
||||||
}
|
}
|
@ -4,9 +4,31 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin h2 {
|
||||||
|
background-color: #222;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.admin table {
|
.admin table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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;
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {FileList} from "../FileList";
|
|||||||
import {UserList} from "./UserList";
|
import {UserList} from "./UserList";
|
||||||
import {Navigate} from "react-router-dom";
|
import {Navigate} from "react-router-dom";
|
||||||
import {useApi} from "../Api";
|
import {useApi} from "../Api";
|
||||||
|
import {VoidButton} from "../VoidButton";
|
||||||
|
|
||||||
export function Admin() {
|
export function Admin() {
|
||||||
const auth = useSelector((state) => state.login.jwt);
|
const auth = useSelector((state) => state.login.jwt);
|
||||||
@ -11,7 +12,6 @@ export function Admin() {
|
|||||||
|
|
||||||
|
|
||||||
async function deleteFile(e, id) {
|
async function deleteFile(e, id) {
|
||||||
e.target.disabled = true;
|
|
||||||
if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
|
if (window.confirm(`Are you sure you want to delete: ${id}?`)) {
|
||||||
let req = await AdminApi.deleteFile(id);
|
let req = await AdminApi.deleteFile(id);
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
@ -20,21 +20,20 @@ export function Admin() {
|
|||||||
alert("Failed to delete file!");
|
alert("Failed to delete file!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.target.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return <Navigate to="/login"/>;
|
return <Navigate to="/login"/>;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className="admin">
|
<div className="admin">
|
||||||
<h4>Users</h4>
|
<h2>Users</h2>
|
||||||
<UserList/>
|
<UserList/>
|
||||||
|
|
||||||
<h4>Files</h4>
|
<h2>Files</h2>
|
||||||
<FileList loadPage={AdminApi.fileList} actions={(i) => {
|
<FileList loadPage={AdminApi.fileList} actions={(i) => {
|
||||||
return <td>
|
return <td>
|
||||||
<button onClick={(e) => deleteFile(e, i.id)}>Delete</button>
|
<VoidButton onClick={(e) => deleteFile(e, i.id)}>Delete</VoidButton>
|
||||||
</td>
|
</td>
|
||||||
}}/>
|
}}/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import {useApi} from "../Api";
|
|||||||
import {logout} from "../LoginState";
|
import {logout} from "../LoginState";
|
||||||
import {PageSelector} from "../PageSelector";
|
import {PageSelector} from "../PageSelector";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import {VoidButton} from "../VoidButton";
|
||||||
|
|
||||||
export function UserList() {
|
export function UserList() {
|
||||||
const {AdminApi} = useApi();
|
const {AdminApi} = useApi();
|
||||||
@ -19,7 +20,7 @@ export function UserList() {
|
|||||||
page: page,
|
page: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
sortBy: PagedSortBy.Date,
|
sortBy: PagedSortBy.Date,
|
||||||
sortOrder: PageSortOrder.Asc
|
sortOrder: PageSortOrder.Dsc
|
||||||
};
|
};
|
||||||
let req = await AdminApi.userList(pageReq);
|
let req = await AdminApi.userList(pageReq);
|
||||||
if (req.ok) {
|
if (req.ok) {
|
||||||
@ -31,17 +32,17 @@ export function UserList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUser(u) {
|
function renderUser(obj) {
|
||||||
|
const user = obj.user;
|
||||||
return (
|
return (
|
||||||
<tr key={u.id}>
|
<tr key={user.id}>
|
||||||
<td><a href={`/u/${u.id}`}>{u.id.substring(0, 4)}..</a></td>
|
<td><a href={`/u/${user.id}`}>{user.displayName}</a></td>
|
||||||
<td>{moment(u.created).fromNow()}</td>
|
<td>{moment(user.created).fromNow()}</td>
|
||||||
<td>{moment(u.lastLogin).fromNow()}</td>
|
<td>{moment(user.lastLogin).fromNow()}</td>
|
||||||
<td>0</td>
|
<td>{obj.uploads}</td>
|
||||||
<td>{u.roles.join(", ")}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<button>Delete</button>
|
<VoidButton>Delete</VoidButton>
|
||||||
<button>SetRoles</button>
|
<VoidButton>SetRoles</VoidButton>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@ -59,12 +60,11 @@ export function UserList() {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Id</td>
|
<th>Name</th>
|
||||||
<td>Created</td>
|
<th>Created</th>
|
||||||
<td>Last Login</td>
|
<th>Last Login</th>
|
||||||
<td>Files</td>
|
<th>Files</th>
|
||||||
<td>Roles</td>
|
<th>Actions</th>
|
||||||
<td>Actions</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -73,8 +73,11 @@ export function UserList() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{users ? <PageSelector onSelectPage={(x) => setPage(x)} page={page} total={users.totalResults}
|
{users ? <PageSelector
|
||||||
pageSize={pageSize}/> : null}
|
onSelectPage={(x) => setPage(x)}
|
||||||
|
page={page}
|
||||||
|
total={users.totalResults}
|
||||||
|
pageSize={pageSize}/> : null}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -3,4 +3,14 @@ table.file-list {
|
|||||||
word-break: keep-all;
|
word-break: keep-all;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
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;
|
||||||
}
|
}
|
@ -14,7 +14,7 @@ export function FileList(props) {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [files, setFiles] = useState();
|
const [files, setFiles] = useState();
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const pageSize = 10;
|
const pageSize = 20;
|
||||||
const [accessDenied, setAccessDenied] = useState(false);
|
const [accessDenied, setAccessDenied] = useState(false);
|
||||||
|
|
||||||
async function loadFileList() {
|
async function loadFileList() {
|
||||||
@ -62,16 +62,18 @@ export function FileList(props) {
|
|||||||
<table className="file-list">
|
<table className="file-list">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Id</td>
|
<th>Id</th>
|
||||||
<td>Name</td>
|
<th>Name</th>
|
||||||
<td>Uploaded</td>
|
<th>Uploaded</th>
|
||||||
<td>Size</td>
|
<th>Size</th>
|
||||||
<td>Egress</td>
|
<th>Egress</th>
|
||||||
{actions ? <td>Actions</td> : null}
|
{actions ? <th>Actions</th> : null}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -86,9 +86,10 @@ export function FileUpload(props) {
|
|||||||
* @param segment {ArrayBuffer}
|
* @param segment {ArrayBuffer}
|
||||||
* @param id {string}
|
* @param id {string}
|
||||||
* @param editSecret {string?}
|
* @param editSecret {string?}
|
||||||
|
* @param fullDigest {string?} Full file hash
|
||||||
* @returns {Promise<any>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
async function xhrSegment(segment, id, editSecret) {
|
async function xhrSegment(segment, id, editSecret, fullDigest) {
|
||||||
setUState(UploadState.Hashing);
|
setUState(UploadState.Hashing);
|
||||||
const digest = await crypto.subtle.digest(DigestAlgo, segment);
|
const digest = await crypto.subtle.digest(DigestAlgo, segment);
|
||||||
setUState(UploadState.Uploading);
|
setUState(UploadState.Uploading);
|
||||||
@ -116,6 +117,7 @@ export function FileUpload(props) {
|
|||||||
req.setRequestHeader("V-Content-Type", props.file.type);
|
req.setRequestHeader("V-Content-Type", props.file.type);
|
||||||
req.setRequestHeader("V-Filename", props.file.name);
|
req.setRequestHeader("V-Filename", props.file.name);
|
||||||
req.setRequestHeader("V-Digest", buf2hex(digest));
|
req.setRequestHeader("V-Digest", buf2hex(digest));
|
||||||
|
req.setRequestHeader("V-Full-Digest", fullDigest);
|
||||||
if (auth) {
|
if (auth) {
|
||||||
req.setRequestHeader("Authorization", `Bearer ${auth}`);
|
req.setRequestHeader("Authorization", `Bearer ${auth}`);
|
||||||
}
|
}
|
||||||
@ -134,12 +136,14 @@ export function FileUpload(props) {
|
|||||||
// upload file in segments of 50MB
|
// upload file in segments of 50MB
|
||||||
const UploadSize = 50_000_000;
|
const UploadSize = 50_000_000;
|
||||||
|
|
||||||
|
let digest = await crypto.subtle.digest(DigestAlgo, await props.file.arrayBuffer());
|
||||||
let xhr = null;
|
let xhr = null;
|
||||||
const segments = props.file.size / UploadSize;
|
const segments = props.file.size / UploadSize;
|
||||||
for (let s = 0; s < segments; s++) {
|
for (let s = 0; s < segments; s++) {
|
||||||
let offset = s * UploadSize;
|
let offset = s * UploadSize;
|
||||||
let slice = props.file.slice(offset, offset + UploadSize, props.file.type);
|
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) {
|
if (!xhr.ok) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
width: min-content;
|
width: min-content;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-buttons > div {
|
.page-buttons > div {
|
||||||
@ -11,6 +12,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-buttons > div.active {
|
||||||
|
background-color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.page-buttons > div:first-child {
|
.page-buttons > div:first-child {
|
||||||
border-top-left-radius: 3px;
|
border-top-left-radius: 3px;
|
||||||
border-bottom-left-radius: 3px;
|
border-bottom-left-radius: 3px;
|
||||||
@ -24,4 +30,5 @@
|
|||||||
.page-buttons > small {
|
.page-buttons > small {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ export function PageSelector(props) {
|
|||||||
const page = props.page;
|
const page = props.page;
|
||||||
const onSelectPage = props.onSelectPage;
|
const onSelectPage = props.onSelectPage;
|
||||||
const options = {
|
const options = {
|
||||||
showPages: 2,
|
showPages: 3,
|
||||||
...(props.options || {})
|
...(props.options || {})
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,7 +17,9 @@ export function PageSelector(props) {
|
|||||||
|
|
||||||
let buttons = [];
|
let buttons = [];
|
||||||
for (let x = first; x <= last; x++) {
|
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 (
|
return (
|
||||||
|
Loading…
Reference in New Issue
Block a user