mirror of
https://git.v0l.io/Kieran/void.cat.git
synced 2025-03-29 16:01:43 +01:00
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 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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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,21 +20,20 @@ export function Admin() {
|
||||
alert("Failed to delete file!");
|
||||
}
|
||||
}
|
||||
e.target.disabled = false;
|
||||
}
|
||||
|
||||
|
||||
if (!auth) {
|
||||
return <Navigate to="/login"/>;
|
||||
} 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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
@ -24,4 +30,5 @@
|
||||
.page-buttons > small {
|
||||
line-height: 32px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
|
Loading…
x
Reference in New Issue
Block a user