mirror of
https://github.com/psforever/PSFPortal.git
synced 2026-01-19 18:14:45 +00:00
Enable sorting of account list by column
This commit is contained in:
parent
5c11393dae
commit
c54b163be2
27
api/admin.js
27
api/admin.js
|
|
@ -1,6 +1,6 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import * as db from './db.js'
|
import * as db from './db.js'
|
||||||
import { get_pagination, fetch_user_middleware } from './util.js'
|
import { get_pagination, get_filter, get_sort, fetch_user_middleware } from './util.js'
|
||||||
|
|
||||||
const api = express.Router();
|
const api = express.Router();
|
||||||
|
|
||||||
|
|
@ -8,9 +8,32 @@ api.param("user", fetch_user_middleware);
|
||||||
|
|
||||||
api.get('/users', async (req, res, next) => {
|
api.get('/users', async (req, res, next) => {
|
||||||
const pagination = get_pagination(req);
|
const pagination = get_pagination(req);
|
||||||
|
const filter = get_filter(req,
|
||||||
|
{
|
||||||
|
param : 'filter',
|
||||||
|
default : 'all',
|
||||||
|
types : {
|
||||||
|
'gm' : { [db.ACCOUNT.ADMIN] : true },
|
||||||
|
'banned' : { [db.ACCOUNT.BANNED] : true },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const sort = get_sort(req,
|
||||||
|
{
|
||||||
|
param : 'sort',
|
||||||
|
default : 'created_desc',
|
||||||
|
types : {
|
||||||
|
'id' : db.ACCOUNT.ID,
|
||||||
|
'created' : db.ACCOUNT.CREATED,
|
||||||
|
'username' : db.ACCOUNT.USER,
|
||||||
|
'last_login' : db.ACCOUNT.LAST_LOGIN,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accounts = await db.get_accounts_login_info(pagination, db.ACCOUNT.CREATED, db.SQL_ORDER.DESCENDING);
|
const accounts = await db.get_accounts_login_info(pagination, sort, filter);
|
||||||
res.status(200).json({ users: accounts, page: pagination})
|
res.status(200).json({ users: accounts, page: pagination})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
|
|
|
||||||
89
api/db.js
89
api/db.js
|
|
@ -39,6 +39,9 @@ export const ACCOUNT = Object.freeze({
|
||||||
MODIFIED: Symbol("last_modified"),
|
MODIFIED: Symbol("last_modified"),
|
||||||
BANNED: Symbol("inactive"),
|
BANNED: Symbol("inactive"),
|
||||||
ADMIN: Symbol("gm"),
|
ADMIN: Symbol("gm"),
|
||||||
|
|
||||||
|
// A derived table column
|
||||||
|
LAST_LOGIN: Symbol("last_login"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CHARACTER = Object.freeze({
|
export const CHARACTER = Object.freeze({
|
||||||
|
|
@ -63,7 +66,8 @@ export const LOGIN = Object.freeze({
|
||||||
});
|
});
|
||||||
|
|
||||||
function to_sql(symbol) {
|
function to_sql(symbol) {
|
||||||
assert(typeof symbol == 'symbol')
|
assert(typeof symbol == 'symbol',
|
||||||
|
`symbol expected got ${typeof symbol}`)
|
||||||
return String(symbol).slice(7,-1);
|
return String(symbol).slice(7,-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +76,9 @@ function to_sql_kv(fields, idx=1) {
|
||||||
let values = [];
|
let values = [];
|
||||||
|
|
||||||
// This will ONLY get Symbols in the field dict
|
// This will ONLY get Symbols in the field dict
|
||||||
assert(Object.getOwnPropertySymbols(fields).length > 0, "to_sql_kv must have at least one field")
|
if (!fields || Object.getOwnPropertySymbols(fields).length == 0) {
|
||||||
|
return { sql : [], next_idx: idx, values: [] }
|
||||||
|
}
|
||||||
|
|
||||||
Object.getOwnPropertySymbols(fields).forEach(key => {
|
Object.getOwnPropertySymbols(fields).forEach(key => {
|
||||||
assert(typeof key == 'symbol')
|
assert(typeof key == 'symbol')
|
||||||
|
|
@ -89,22 +95,64 @@ function to_sql_kv(fields, idx=1) {
|
||||||
|
|
||||||
function build_SET(fields, idx=1) {
|
function build_SET(fields, idx=1) {
|
||||||
const kv = to_sql_kv(fields, idx);
|
const kv = to_sql_kv(fields, idx);
|
||||||
kv.sql = Symbol(kv.sql.join(", "));
|
|
||||||
|
assert(kv.sql.length > 0, "SET MUST have at least one kv pair")
|
||||||
|
|
||||||
|
kv.sql = Symbol(`SET ${kv.sql.join(", ")}`);
|
||||||
|
|
||||||
return kv;
|
return kv;
|
||||||
}
|
}
|
||||||
|
|
||||||
function build_WHERE(fields, idx=1) {
|
function build_WHERE(filter, idx=1) {
|
||||||
const kv = to_sql_kv(fields, idx);
|
const kv = to_sql_kv(filter.fields, idx);
|
||||||
kv.sql = Symbol(kv.sql.join(" AND "));
|
|
||||||
|
if (kv.sql.length > 1) {
|
||||||
|
assert(filter.binop !== undefined,
|
||||||
|
"binary operand required in WHERE with multiple terms")
|
||||||
|
const binop = filter.binop == "AND" ? Symbol("AND") : Symbol("OR");
|
||||||
|
kv.sql = Symbol(`WHERE ${kv.sql.join(` ${to_sql(binop)} `)}`);
|
||||||
|
} else if (kv.sql.length == 1)
|
||||||
|
kv.sql = Symbol(`WHERE ${kv.sql[0]}`);
|
||||||
|
else
|
||||||
|
kv.sql = Symbol("")
|
||||||
|
|
||||||
return kv;
|
return kv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function build_ORDER_BY(sort) {
|
||||||
|
// This will ONLY get Symbols in the sort dict
|
||||||
|
if (!sort || Object.getOwnPropertySymbols(sort).length == 0) {
|
||||||
|
return Symbol("");
|
||||||
|
}
|
||||||
|
|
||||||
|
let SQL = []
|
||||||
|
Object.getOwnPropertySymbols(sort).forEach(key => {
|
||||||
|
const value = sort[key];
|
||||||
|
assert(typeof key == 'symbol')
|
||||||
|
assert(typeof value == 'symbol')
|
||||||
|
SQL.push(`${to_sql(key)} ${to_sql(value)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Symbol(`ORDER BY ${SQL.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_OFFSET(offset, limit, idx=1) {
|
||||||
|
assert(typeof offset == 'number', "offset must be a number");
|
||||||
|
assert(typeof limit == 'number', "limit must be a number");
|
||||||
|
|
||||||
|
return {
|
||||||
|
sql: Symbol(`OFFSET $${idx++} LIMIT $${idx++}`),
|
||||||
|
next_idx: idx,
|
||||||
|
values: [offset, limit],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function get_row_count(table, filter=undefined) {
|
async function get_row_count(table, filter=undefined) {
|
||||||
let resp;
|
let resp;
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const where = build_WHERE(filter);
|
const where = build_WHERE(filter);
|
||||||
resp = await pool.query(`SELECT COUNT(*) FROM ${to_sql(table)} WHERE ${to_sql(where.sql)}`,
|
resp = await pool.query(`SELECT COUNT(*) FROM ${to_sql(table)} ${to_sql(where.sql)}`,
|
||||||
where.values);
|
where.values);
|
||||||
} else {
|
} else {
|
||||||
resp = await pool.query(`SELECT COUNT(*) FROM ${to_sql(table)}`);
|
resp = await pool.query(`SELECT COUNT(*) FROM ${to_sql(table)}`);
|
||||||
|
|
@ -171,17 +219,21 @@ export async function get_accounts(pagination, sort, order) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_accounts_login_info(pagination, sort, order) {
|
export async function get_accounts_login_info(pagination, sort, filter) {
|
||||||
const start_id = (pagination.page-1)*pagination.items_per_page;
|
const start_id = (pagination.page-1)*pagination.items_per_page;
|
||||||
const values = [start_id, pagination.items_per_page];
|
const values = [start_id, pagination.items_per_page];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const account_count = await get_row_count(ACCOUNT.THIS);
|
const account_count = await get_row_count(ACCOUNT.THIS, filter);
|
||||||
|
const where = build_WHERE(filter);
|
||||||
|
const offset = build_OFFSET(start_id, pagination.items_per_page, where.next_idx);
|
||||||
|
const values = [].concat(where.values, offset.values);
|
||||||
|
const order = build_ORDER_BY(sort);
|
||||||
|
|
||||||
// this was a really hard query to get right...
|
// this was a really hard query to get right...
|
||||||
// https://www.gab.lc/articles/better_faster_subqueries_postgresql/
|
// https://www.gab.lc/articles/better_faster_subqueries_postgresql/
|
||||||
const accounts = await pool.query(
|
const accounts = await pool.query(
|
||||||
'SELECT accounts.*, lastLogin as login_time, l2.ip_address, l2.canonical_hostname FROM accounts' +
|
'SELECT accounts.*, COALESCE(l.lastLogin, TIMESTAMP \'epoch\') as last_login, l2.ip_address, l2.canonical_hostname FROM accounts' +
|
||||||
' LEFT OUTER JOIN (' +
|
' LEFT OUTER JOIN (' +
|
||||||
' SELECT MAX(id) as loginId, account_id, MAX(login_time) as lastLogin' +
|
' SELECT MAX(id) as loginId, account_id, MAX(login_time) as lastLogin' +
|
||||||
' FROM logins' +
|
' FROM logins' +
|
||||||
|
|
@ -189,7 +241,9 @@ export async function get_accounts_login_info(pagination, sort, order) {
|
||||||
' ) l ON l.account_id = accounts.id' +
|
' ) l ON l.account_id = accounts.id' +
|
||||||
' LEFT OUTER JOIN logins l2' +
|
' LEFT OUTER JOIN logins l2' +
|
||||||
' ON l2.id = l.loginId' +
|
' ON l2.id = l.loginId' +
|
||||||
` ORDER BY COALESCE(l.lastLogin, TIMESTAMP \'epoch\') ${to_sql(order)} OFFSET $1 LIMIT $2`, values);
|
` ${to_sql(where.sql)}` +
|
||||||
|
` ${to_sql(order)}` +
|
||||||
|
` ${to_sql(offset.sql)}`, values);
|
||||||
|
|
||||||
pagination.item_count = account_count;
|
pagination.item_count = account_count;
|
||||||
pagination.page_count = Math.ceil(pagination.item_count / pagination.items_per_page);
|
pagination.page_count = Math.ceil(pagination.item_count / pagination.items_per_page);
|
||||||
|
|
@ -198,9 +252,9 @@ export async function get_accounts_login_info(pagination, sort, order) {
|
||||||
r.name = r.username;
|
r.name = r.username;
|
||||||
r.admin = r.gm;
|
r.admin = r.gm;
|
||||||
|
|
||||||
if (r.login_time !== null) {
|
if (r.ip_address !== null) {
|
||||||
r.last_login = {
|
r.last_login = {
|
||||||
time : r.login_time,
|
time : r.last_login,
|
||||||
hostname : r.canonical_hostname,
|
hostname : r.canonical_hostname,
|
||||||
ip : r.ip_address,
|
ip : r.ip_address,
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +262,6 @@ export async function get_accounts_login_info(pagination, sort, order) {
|
||||||
r.last_login = {}
|
r.last_login = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete r.login_time;
|
|
||||||
delete r.canonical_hostname;
|
delete r.canonical_hostname;
|
||||||
delete r.ip_address;
|
delete r.ip_address;
|
||||||
delete r.passhash;
|
delete r.passhash;
|
||||||
|
|
@ -285,7 +338,7 @@ export async function update_account(account_id, fields) {
|
||||||
set.values.push(account_id)
|
set.values.push(account_id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const update_result = await pool.query(`UPDATE accounts SET ${to_sql(set.sql)} WHERE id=$${set.next_idx}`, set.values);
|
const update_result = await pool.query(`UPDATE accounts ${to_sql(set.sql)} WHERE id=$${set.next_idx}`, set.values);
|
||||||
return update_result.rowCount;
|
return update_result.rowCount;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code)
|
if (e.code)
|
||||||
|
|
@ -324,7 +377,11 @@ export async function get_account_logins(account_id, pagination) {
|
||||||
const values = [account_id, start_id, pagination.items_per_page];
|
const values = [account_id, start_id, pagination.items_per_page];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const login_count = await get_row_count(LOGIN.THIS, { [LOGIN.ACCOUNT_ID] : account_id });
|
const login_count = await get_row_count(LOGIN.THIS, {
|
||||||
|
fields : {
|
||||||
|
[LOGIN.ACCOUNT_ID] : account_id,
|
||||||
|
}
|
||||||
|
});
|
||||||
const logins = await pool.query('SELECT * FROM logins WHERE account_id=$1 ORDER by login_time DESC ' + ` OFFSET $2 LIMIT $3`, values);
|
const logins = await pool.query('SELECT * FROM logins WHERE account_id=$1 ORDER by login_time DESC ' + ` OFFSET $2 LIMIT $3`, values);
|
||||||
|
|
||||||
pagination.item_count = login_count;
|
pagination.item_count = login_count;
|
||||||
|
|
|
||||||
100
api/util.js
100
api/util.js
|
|
@ -1,8 +1,8 @@
|
||||||
import * as db from './db.js'
|
import * as db from './db.js'
|
||||||
|
import assert from 'assert'
|
||||||
|
|
||||||
export function get_pagination(req) {
|
export function get_pagination(req) {
|
||||||
let page = parseInt(req.query.page);
|
let page = parseInt(req.query.page);
|
||||||
let order = req.query.order; // TODO
|
|
||||||
|
|
||||||
if (!page || page < 1) {
|
if (!page || page < 1) {
|
||||||
page = 1;
|
page = 1;
|
||||||
|
|
@ -11,10 +11,106 @@ export function get_pagination(req) {
|
||||||
return {
|
return {
|
||||||
page: page,
|
page: page,
|
||||||
items_per_page: 40,
|
items_per_page: 40,
|
||||||
//order: order, // TODO
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function get_sort_param(param) {
|
||||||
|
const tokens = param.split("_");
|
||||||
|
let order = db.SQL_ORDER.ASCENDING;
|
||||||
|
|
||||||
|
// default to ascending if malformed
|
||||||
|
if (tokens[tokens.length-1] == "desc")
|
||||||
|
order = db.SQL_ORDER.DESCENDING;
|
||||||
|
else if (tokens[tokens.length-1] == "asc")
|
||||||
|
order = db.SQL_ORDER.ASCENDING;
|
||||||
|
|
||||||
|
return [tokens.slice(0, tokens.length-1).join("_"), order]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_sort(req, sort_spec) {
|
||||||
|
assert(sort_spec && sort_spec.param && sort_spec.default && sort_spec.types,
|
||||||
|
"Invalid filter specification");
|
||||||
|
assert(sort_spec.default, "Default sort specification required");
|
||||||
|
|
||||||
|
Object.keys(sort_spec.types).forEach((k) => {
|
||||||
|
assert(typeof k == 'string', "Sort key spec must be a string")
|
||||||
|
assert(typeof sort_spec.types[k] == 'symbol', "Sort value MUST be a symbolic table column")
|
||||||
|
});
|
||||||
|
|
||||||
|
const default_sort_parsed = get_sort_param(sort_spec.default);
|
||||||
|
assert(default_sort_parsed[0] in sort_spec.types, "Default sort type MUST be in spec")
|
||||||
|
|
||||||
|
const default_sort = { [sort_spec.types[default_sort_parsed[0]]] : default_sort_parsed[1] };
|
||||||
|
|
||||||
|
const sort_query = req.query[sort_spec.param];
|
||||||
|
|
||||||
|
// return default
|
||||||
|
if (!sort_query) {
|
||||||
|
return default_sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = {}
|
||||||
|
const types = sort_query.split(",");
|
||||||
|
|
||||||
|
for (let i = 0; i < types.length; i++) {
|
||||||
|
const t = types[i];
|
||||||
|
const sparsed = get_sort_param(t);
|
||||||
|
|
||||||
|
if (!(sparsed[0] in sort_spec.types)) {
|
||||||
|
console.log("WARNING: unknown sort type " + t);
|
||||||
|
return default_sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
const column_name = sort_spec.types[sparsed[0]];
|
||||||
|
sort[column_name] = sparsed[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_filter(req, filter_spec) {
|
||||||
|
assert(filter_spec && filter_spec.param && filter_spec.default && filter_spec.types,
|
||||||
|
"Invalid filter specification");
|
||||||
|
|
||||||
|
const filter_query = req.query[filter_spec.param];
|
||||||
|
|
||||||
|
if (!filter_query) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const types = filter_query.split(",");
|
||||||
|
|
||||||
|
// No filtering needed, everything displayed
|
||||||
|
if (filter_spec.default in types) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
binop : "OR", // TODO: support more complex queries
|
||||||
|
fields : {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < types.length; i++) {
|
||||||
|
const t = types[i];
|
||||||
|
|
||||||
|
if (!(t in filter_spec.types)) {
|
||||||
|
console.log("WARNING: unknown filter type " + t);
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraint = filter_spec.types[t];
|
||||||
|
|
||||||
|
|
||||||
|
Object.getOwnPropertySymbols(constraint).forEach(key => {
|
||||||
|
assert(Object.keys(constraint) == 0 && typeof key == 'symbol',
|
||||||
|
"filter constraint keys MUST be symbolic table columns")
|
||||||
|
filter.fields[key] = constraint[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetch_user_middleware(req, res, next, id) {
|
export async function fetch_user_middleware(req, res, next, id) {
|
||||||
id = parseInt(id);
|
id = parseInt(id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,11 @@
|
||||||
let fetching = false;
|
let fetching = false;
|
||||||
let pagination = { page: 1 };
|
let pagination = { page: 1 };
|
||||||
|
|
||||||
export async function refresh() {
|
export async function refresh(newpage) {
|
||||||
await list_fetch(pagination.page)
|
if (newpage !== undefined && typeof newpage == "number")
|
||||||
|
await list_fetch(newpage)
|
||||||
|
else
|
||||||
|
await list_fetch(pagination.page)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
|
|
||||||
|
|
@ -100,4 +100,4 @@
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ActionModal on:action={refresh} />
|
<ActionModal on:action={() => refresh()} />
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,93 @@
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import { onMount } from 'svelte'
|
||||||
import AccountLink from '../components/AccountLink'
|
import AccountLink from '../components/AccountLink'
|
||||||
import PaginatedList from '../components/PaginatedList'
|
import PaginatedList from '../components/PaginatedList'
|
||||||
import ActionButtons from '../components/ActionButtons'
|
import ActionButtons from '../components/ActionButtons'
|
||||||
import ActionModal from '../components/ActionModal.svelte'
|
import ActionModal from '../components/ActionModal.svelte'
|
||||||
|
import { formToJSON } from '../util/form.js'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
|
||||||
export let setURLParam = false;
|
export let setURLParam = false;
|
||||||
export let appAlert
|
export let appAlert;
|
||||||
let userList;
|
let userList;
|
||||||
|
let filter = "";
|
||||||
|
let sort = ""
|
||||||
|
|
||||||
|
function handleSort(event) {
|
||||||
|
const ele = event.target;
|
||||||
|
const classes = ele.classList;
|
||||||
|
const thead = ele.parentElement;
|
||||||
|
|
||||||
|
if (!classes.contains("sortable")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let found, next = "";
|
||||||
|
let sortstate = {
|
||||||
|
"both" : "asc",
|
||||||
|
"asc" : "desc",
|
||||||
|
"desc" : "asc"
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(sortstate);
|
||||||
|
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const k = keys[i];
|
||||||
|
if (classes.contains(k)) {
|
||||||
|
if (!ele.dataset.sort)
|
||||||
|
break;
|
||||||
|
|
||||||
|
found = k;
|
||||||
|
sort = `sort=${ele.dataset.sort}_${sortstate[k]}&`
|
||||||
|
ele.classList.remove(k);
|
||||||
|
ele.classList.add(sortstate[k]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid sort
|
||||||
|
if (!found) return;
|
||||||
|
|
||||||
|
// clear other sorts
|
||||||
|
Array.from(thead.children).forEach((child) =>{
|
||||||
|
if (child == ele)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (child.classList.contains("sortable")) {
|
||||||
|
child.classList.remove("asc")
|
||||||
|
child.classList.remove("desc")
|
||||||
|
child.classList.add("both")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
userList.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeFilter(event) {
|
||||||
|
const filters = formToJSON(event.target.form);
|
||||||
|
|
||||||
|
if (Object.keys(filters).length === 0) {
|
||||||
|
filter = "";
|
||||||
|
userList.refresh()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ufilter = Object.keys(filters).join(",");
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('ufilter', ufilter);
|
||||||
|
history.replaceState(null, null,
|
||||||
|
url.pathname + url.search + url.hash)
|
||||||
|
|
||||||
|
filter = `filter=${ufilter}&`
|
||||||
|
// Filter changes go to the first page
|
||||||
|
userList.refresh(1)
|
||||||
|
}
|
||||||
|
|
||||||
async function fetch(page) {
|
async function fetch(page) {
|
||||||
try {
|
try {
|
||||||
const resp = await axios.get("/api/users?page="+page)
|
const resp = await axios.get(`/api/users?${filter}${sort}page=${page}`)
|
||||||
appAlert.message("")
|
appAlert.message("")
|
||||||
return [resp.data.users, resp.data.page];
|
return [resp.data.users, resp.data.page];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -25,15 +100,37 @@
|
||||||
<PaginatedList {setURLParam} bind:this={userList} bind:fetch={fetch} let:data={users} let:pagination={pagination}>
|
<PaginatedList {setURLParam} bind:this={userList} bind:fetch={fetch} let:data={users} let:pagination={pagination}>
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
<p>{pagination.item_count.toLocaleString()} users in the database</p>
|
<p>{pagination.item_count.toLocaleString()} users in the database</p>
|
||||||
|
|
||||||
|
<div class="dropdown float-right">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
Filter
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||||
|
<form on:change={changeFilter} class="px-4 py-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" name="gm" id="userFilterGM">
|
||||||
|
<label class="form-check-label" for="userFilterGM">
|
||||||
|
GM
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" name="banned" id="userFilterBanned">
|
||||||
|
<label class="form-check-label" for="userFilterBanned">
|
||||||
|
Banned
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table slot="body" class="table table-sm table-dark table-responsive-md table-striped table-hover">
|
<table slot="body" class="table table-sm table-dark table-responsive-md table-striped table-hover">
|
||||||
<thead class="thead-light">
|
<thead on:click={handleSort} class="thead-light">
|
||||||
<th>ID</th>
|
<th data-sort="id" class="sortable both">ID</th>
|
||||||
<th>Username</th>
|
<th data-sort="username" class="sortable both">Username</th>
|
||||||
<th>User Created</th>
|
<th data-sort="created" class="sortable both">User Created</th>
|
||||||
<th>Last Login</th>
|
<th data-sort="last_login" class="sortable both">Last Login</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each users as user, i}
|
{#each users as user, i}
|
||||||
|
|
@ -55,4 +152,4 @@
|
||||||
</table>
|
</table>
|
||||||
</PaginatedList>
|
</PaginatedList>
|
||||||
|
|
||||||
<ActionModal on:action={userList.refresh} />
|
<ActionModal on:action={() => userList.refresh()} />
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,27 @@ button:focus {
|
||||||
.form-control-plaintext {
|
.form-control-plaintext {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table thead {
|
||||||
|
// Stolen from https://github.com/wenzhixin/bootstrap-table/blob/develop/src/themes/_theme.scss#L103
|
||||||
|
.sortable {
|
||||||
|
cursor: pointer;
|
||||||
|
background-position: right;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
padding-right: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.both {
|
||||||
|
// This was too light...
|
||||||
|
//background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAkElEQVQoz7X QMQ5AQBCF4dWQSJxC5wwax1Cq1e7BAdxD5SL+Tq/QCM1oNiJidwox0355mXnG/DrEtIQ6azioNZQxI0ykPhTQIwhCR+BmBYtlK7kLJYwWCcJA9M4qdrZrd8pPjZWPtOqdRQy320YSV17OatFC4euts6z39GYMKRPCTKY9UnPQ6P+GtMRfGtPnBCiqhAeJPmkqAAAAAElFTkSuQmCC');
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAQAAADYWf5HAAAAAmJLR0QA/4ePzL8AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfjDB8SGB+BP4v2AAAAzklEQVQoz2NgoCmYyFUyYSIbuigTusCr7KtZb1IJKOvSupX3j/VOQbcyHmUTGZ8Vf5JhYHin8jJvIiNOZV8dHgRCWLcSv1njUNbHe7/2uyCE/ZP3Qc1EdqzKPoY8tWb5AYOPHT8EkRxMSA5tjDy5gPEfjPefyTSjcT4WSwXWye7/wwGD0kf512B1W/5PhRb2zxA253vF5qLPOAKE66ga1BqF9dwHcIZb/n/xSUJ3GBj4nkj15v/H66fabM9f1aXooizoAiKztVXFptI2FQIAo85H84//KTgAAAAASUVORK5CYII=');
|
||||||
|
}
|
||||||
|
|
||||||
|
.asc {
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZ0lEQVQ4y2NgGLKgquEuFxBPAGI2ahhWCsS/gDibUoO0gPgxEP8H4ttArEyuQYxAPBdqEAxPBImTY5gjEL9DM+wTENuQahAvEO9DMwiGdwAxOymGJQLxTyD+jgWDxCMZRsEoGAVoAADeemwtPcZI2wAAAABJRU5ErkJggg==');
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAATCAYAAAByUDbMAAAAZUlEQVQ4y2NgGAWjYBSggaqGu5FA/BOIv2PBIPFEUgxjB+IdQPwfC94HxLykus4GiD+hGfQOiB3J8SojEE9EM2wuSJzcsFMG4ttQgx4DsRalkZENxL+AuJQaMcsGxBOAmGvopk8AVz1sLZgg0bsAAAAASUVORK5CYII= ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue