mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-22 04:32:49 +00:00
feat(dashboard): the cars and rockets table has the models table's columns
The UGC page's assemblies (cars and rockets) list had its own columns. It now has the models' columns and sorts, minus Saved: ID and Owner (the newest build and its creator, and how many other owners), State, Made, Took, CPU and RAM (the latest make of any build, and the cost of the make the UGC server did for the combination; builds that shared the made icon cost nothing), Size (the module count), File (the build type and its modules) and Where (how many builds and owners use it, opening References). The gallery sorts the same way. Check: UGC page, Cars and rockets, List: every column sorts both ways on the server, and the gallery's sort list has the same sorts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,15 +33,33 @@ namespace UgcAssemblies {
|
||||
IUgc::eProcessState state{ IUgc::eProcessState::PENDING };
|
||||
LWOOBJID iconBuild{}; // a build whose icon is made (the newest made one, else the newest)
|
||||
std::string error; // a failed build's error, when none is made
|
||||
// The newest build's creator
|
||||
LWOOBJID characterId{};
|
||||
std::string characterName;
|
||||
uint32_t accountId{};
|
||||
std::string accountName;
|
||||
// The combination's last make: when (the latest any build of it was made or attempted) and what it cost (the
|
||||
// build the UGC server made it for; the builds that shared it after it was made cost nothing)
|
||||
int64_t processedAt{};
|
||||
uint32_t processMs{};
|
||||
uint32_t processCpuMs{};
|
||||
uint32_t processMemoryKb{};
|
||||
};
|
||||
|
||||
enum class eSort : uint8_t { NEWEST, OLDEST, REFERENCES, NAME };
|
||||
// The models list's sorts (IUgcLookup::eSort) that assemblies have: MODULES is the Size column (the most modules first)
|
||||
enum class eSort : uint8_t { NEWEST, OLDEST, REFERENCES, NAME, OWNER, MADE, SLOWEST, CPU, MEMORY, MODULES };
|
||||
|
||||
inline std::optional<eSort> ParseSort(std::string_view text) {
|
||||
if (text.empty() || text == "newest") return eSort::NEWEST;
|
||||
if (text == "oldest") return eSort::OLDEST;
|
||||
if (text == "references" || text == "uses") return eSort::REFERENCES;
|
||||
if (text == "name") return eSort::NAME;
|
||||
if (text == "owner") return eSort::OWNER;
|
||||
if (text == "made") return eSort::MADE;
|
||||
if (text == "slowest") return eSort::SLOWEST;
|
||||
if (text == "cpu") return eSort::CPU;
|
||||
if (text == "memory") return eSort::MEMORY;
|
||||
if (text == "modules" || text == "bricks") return eSort::MODULES;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -94,6 +112,22 @@ namespace UgcAssemblies {
|
||||
}
|
||||
}
|
||||
if (!made) assembly.iconBuild = assembly.builds.front();
|
||||
const auto& newest = *byId[assembly.builds.front()];
|
||||
assembly.characterId = newest.characterId;
|
||||
assembly.characterName = newest.characterName;
|
||||
assembly.accountId = newest.accountId;
|
||||
assembly.accountName = newest.accountName;
|
||||
int64_t costAt = -1;
|
||||
for (const auto id : assembly.builds) {
|
||||
const auto& build = *byId[id];
|
||||
assembly.processedAt = std::max(assembly.processedAt, build.processedAt);
|
||||
if (build.processMs > 0 && build.processedAt > costAt) {
|
||||
costAt = build.processedAt;
|
||||
assembly.processMs = build.processMs;
|
||||
assembly.processCpuMs = build.processCpuMs;
|
||||
assembly.processMemoryKb = build.processMemoryKb;
|
||||
}
|
||||
}
|
||||
assembly.state = made ? IUgc::eProcessState::DONE : waiting ? IUgc::eProcessState::PENDING : failed ? IUgc::eProcessState::FAILED : IUgc::eProcessState::EMPTY;
|
||||
if (made) assembly.error.clear();
|
||||
out.push_back(std::move(assembly));
|
||||
@@ -141,14 +175,25 @@ namespace UgcAssemblies {
|
||||
|
||||
// reverse: the sort's other direction (the order is flipped as a whole)
|
||||
inline void Sort(std::vector<Assembly>& list, eSort sort, const std::map<uint32_t, ModuleInfo>& modules, bool reverse = false) {
|
||||
// The most first, ties the newest first
|
||||
const auto most = [](auto x, auto y, const Assembly& a, const Assembly& b) { return x != y ? x > y : a.builds.front() > b.builds.front(); };
|
||||
std::stable_sort(list.begin(), list.end(), [&](const Assembly& a, const Assembly& b) {
|
||||
switch (sort) {
|
||||
case eSort::OLDEST: return a.builds.back() < b.builds.back();
|
||||
case eSort::REFERENCES: return a.builds.size() != b.builds.size() ? a.builds.size() > b.builds.size() : a.builds.front() > b.builds.front();
|
||||
case eSort::REFERENCES: return most(a.builds.size(), b.builds.size(), a, b);
|
||||
case eSort::NAME: {
|
||||
const auto an = Lower(Name(a, modules)), bn = Lower(Name(b, modules));
|
||||
return an != bn ? an < bn : a.key < b.key;
|
||||
}
|
||||
case eSort::OWNER: {
|
||||
const auto an = Lower(a.characterName), bn = Lower(b.characterName);
|
||||
return an != bn ? an < bn : a.builds.front() > b.builds.front();
|
||||
}
|
||||
case eSort::MADE: return most(a.processedAt, b.processedAt, a, b);
|
||||
case eSort::SLOWEST: return most(a.processMs, b.processMs, a, b);
|
||||
case eSort::CPU: return most(a.processCpuMs, b.processCpuMs, a, b);
|
||||
case eSort::MEMORY: return most(a.processMemoryKb, b.processMemoryKb, a, b);
|
||||
case eSort::MODULES: return most(a.lots.size(), b.lots.size(), a, b);
|
||||
default: return a.builds.front() > b.builds.front();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -283,8 +283,9 @@ namespace UgcRoutes {
|
||||
"A page of player models (kind=model) or of car and rocket assemblies (kind=modular: one per combination of modules, however many builds use "
|
||||
"it). Query: q= (\"state:\", \"kind:\"/\"type:\" (a build type, e.g. build6), \"owner:\", \"account:\", \"property:\", \"name:\", "
|
||||
"\"lot:\"/\"module:\" (a LOT or a module's name), \"id:\", or plain text across names, owners and ids), state=, type=, sort=newest|oldest|owner|name|"
|
||||
"bricks|triangles|slowest|made|cpu|memory|savings (models) or newest|oldest|references|name (assemblies), reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, "
|
||||
"kinds, ugcPublicUrl, canManage}",
|
||||
"bricks|triangles|slowest|made|cpu|memory|savings (models) or newest|oldest|owner|name|modules|made|slowest|cpu|memory|references (assemblies), "
|
||||
"reverse=1 (the sort's other direction), page= (from 0), size= (1-200). {items, total, page, size, counts, totals, kinds, ugcPublicUrl, "
|
||||
"canManage}",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const bool modular = QueryValue(context.queryString, "kind") == "modular";
|
||||
auto parsed = ParseListSearch(QueryValue(context.queryString, "q").substr(0, 100));
|
||||
@@ -294,10 +295,10 @@ namespace UgcRoutes {
|
||||
const auto size = std::clamp(GeneralUtils::TryParse<uint32_t>(QueryValue(context.queryString, "size")).value_or(PAGE_SIZE), 1u, 200u);
|
||||
const auto sortText = QueryValue(context.queryString, "sort");
|
||||
const bool reverse = QueryValue(context.queryString, "reverse") == "1";
|
||||
nlohmann::json items = nlohmann::json::array();
|
||||
uint64_t total = 0;
|
||||
const auto kinds = IconKinds();
|
||||
if (!modular) {
|
||||
|
||||
// A page of models, and how many match
|
||||
const auto models = [&](uint32_t offset, uint32_t limit) {
|
||||
IUgcLookup::UgcListQuery query;
|
||||
query.search = parsed.search;
|
||||
query.state = parsed.state;
|
||||
@@ -305,12 +306,12 @@ namespace UgcRoutes {
|
||||
{ "owner", IUgcLookup::eSort::OWNER }, { "name", IUgcLookup::eSort::NAME }, { "bricks", IUgcLookup::eSort::BRICKS }, { "triangles", IUgcLookup::eSort::TRIANGLES }, { "slowest", IUgcLookup::eSort::SLOWEST }, { "made", IUgcLookup::eSort::MADE }, { "cpu", IUgcLookup::eSort::CPU }, { "memory", IUgcLookup::eSort::MEMORY }, { "savings", IUgcLookup::eSort::SAVINGS } };
|
||||
if (const auto it = SORTS.find(sortText); it != SORTS.end()) query.sort = it->second;
|
||||
query.reverse = reverse;
|
||||
query.offset = page * size;
|
||||
query.limit = size;
|
||||
const auto [entries, count] = Database::Get()->ListUgc(IUgcLookup::eUgcKind::MODEL, query);
|
||||
total = count;
|
||||
for (const auto& entry : entries) items.push_back(EntryJson(entry));
|
||||
} else {
|
||||
query.offset = offset;
|
||||
query.limit = limit;
|
||||
return Database::Get()->ListUgc(IUgcLookup::eUgcKind::MODEL, query);
|
||||
};
|
||||
// The assemblies matching the search and filters, sorted
|
||||
const auto assemblies = [&]() {
|
||||
UgcAssemblies::Filter filter;
|
||||
filter.state = parsed.state;
|
||||
if (!parsed.kind.empty()) {
|
||||
@@ -336,12 +337,23 @@ namespace UgcRoutes {
|
||||
if (parsed.search.number) filter.moduleLot = static_cast<uint32_t>(*parsed.search.number);
|
||||
}
|
||||
}
|
||||
auto assemblies = *CachedAssemblies();
|
||||
std::erase_if(assemblies, [&](const auto& a) { return !UgcAssemblies::Matches(a, filter, modules); });
|
||||
UgcAssemblies::Sort(assemblies, UgcAssemblies::ParseSort(sortText).value_or(UgcAssemblies::eSort::NEWEST), modules, reverse);
|
||||
total = assemblies.size();
|
||||
for (size_t i = static_cast<size_t>(page) * size; i < assemblies.size() && i < static_cast<size_t>(page + 1) * size; i++) {
|
||||
const auto& a = assemblies[i];
|
||||
auto list = *CachedAssemblies();
|
||||
std::erase_if(list, [&](const auto& a) { return !UgcAssemblies::Matches(a, filter, modules); });
|
||||
UgcAssemblies::Sort(list, UgcAssemblies::ParseSort(sortText).value_or(UgcAssemblies::eSort::NEWEST), modules, reverse);
|
||||
return list;
|
||||
};
|
||||
|
||||
nlohmann::json items = nlohmann::json::array();
|
||||
uint64_t total = 0;
|
||||
if (!modular) {
|
||||
const auto [entries, count] = models(page * size, size);
|
||||
total = count;
|
||||
for (const auto& entry : entries) items.push_back(EntryJson(entry));
|
||||
} else {
|
||||
const auto list = assemblies();
|
||||
total = list.size();
|
||||
for (size_t i = static_cast<size_t>(page) * size; i < list.size() && i < static_cast<size_t>(page + 1) * size; i++) {
|
||||
const auto& a = list[i];
|
||||
std::string label, kind;
|
||||
for (const auto& k : kinds) {
|
||||
if (k.contains("buildType") && k["buildType"] == a.buildType) {
|
||||
@@ -354,7 +366,9 @@ namespace UgcRoutes {
|
||||
items.push_back({ { "id", a.key }, { "key", a.key }, { "modules", ldf }, { "moduleList", ModulesJson(a.lots) }, { "buildType", a.buildType },
|
||||
{ "kind", kind }, { "kindLabel", label }, { "state", IUgc::ProcessStateName(a.state) }, { "error", a.error }, { "uses", a.builds.size() },
|
||||
{ "owners", a.owners.size() }, { "iconBuild", std::to_string(a.iconBuild) }, { "newestBuild", std::to_string(a.builds.front()) },
|
||||
{ "storageId", std::to_string(UgcModularKey::StorageId(a.key)) } });
|
||||
{ "storageId", std::to_string(UgcModularKey::StorageId(a.key)) }, { "characterId", std::to_string(a.characterId) },
|
||||
{ "characterName", a.characterName }, { "accountId", a.accountId }, { "accountName", a.accountName }, { "processedAt", a.processedAt },
|
||||
{ "processMs", a.processMs }, { "processCpuMs", a.processCpuMs }, { "processMemoryKb", a.processMemoryKb } });
|
||||
}
|
||||
}
|
||||
JsonSuccess(reply, { { "counts", { { "model", Counts(Database::Get()->GetUgcProcessCounts()) }, { "modular", Counts(Database::Get()->GetModularBuildProcessCounts()) } } },
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
var STATES = { pending: ['Waiting', 'secondary'], done: ['Made', 'success'], failed: ['Failed', 'danger'], empty: ['Empty', 'light'] };
|
||||
var SORTS = {
|
||||
model: [['newest', 'Newest'], ['oldest', 'Oldest'], ['bricks', 'Most bricks'], ['triangles', 'Most triangles'], ['made', 'Recently made'], ['slowest', 'Slowest to make'], ['cpu', 'Most CPU'], ['memory', 'Most RAM'], ['savings', 'Most triangles saved'], ['owner', 'Owner'], ['name', 'File name']],
|
||||
modular: [['newest', 'Newest'], ['oldest', 'Oldest'], ['references', 'Most builds'], ['name', 'Name']]
|
||||
modular: [['newest', 'Newest'], ['oldest', 'Oldest'], ['references', 'Most builds'], ['modules', 'Most modules'], ['made', 'Recently made'], ['slowest', 'Slowest to make'], ['cpu', 'Most CPU'], ['memory', 'Most RAM'], ['owner', 'Owner'], ['name', 'Name']]
|
||||
};
|
||||
// The list's state, as in the address: ?kind=&q=&state=&type=&sort=&page= (from 1)&view=, and the open item (&item=, &build=)
|
||||
var list = { kind: 'model', q: '', state: '', type: '', sort: 'newest', page: 0, view: 'gallery' };
|
||||
@@ -157,6 +157,15 @@
|
||||
return { data: null, title: title, orderable: !!sort, sortKey: sort, desc: !!desc, orderSequence: desc ? ['desc', 'asc'] : ['asc', 'desc'],
|
||||
render: function (x, type, i) { return render(i); } };
|
||||
}
|
||||
// When it was made and what that cost: the same columns for models and assemblies
|
||||
function madeColumns() {
|
||||
return [
|
||||
column('Made', function (i) { return '<span class="small">' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + '</span>'; }, 'made', true),
|
||||
column('Took', function (i) { return '<span class="small">' + (i.processMs ? esc(duration(i.processMs)) : '') + '</span>'; }, 'slowest', true),
|
||||
column('CPU', function (i) { return '<span class="small">' + (i.processCpuMs ? esc(duration(i.processCpuMs)) : '') + '</span>'; }, 'cpu', true),
|
||||
column('RAM (est.)', function (i) { return '<span class="small">' + (i.processMemoryKb ? '~' + esc(megabytes(i.processMemoryKb)) : '') + '</span>'; }, 'memory', true)
|
||||
];
|
||||
}
|
||||
var COLUMNS = {
|
||||
model: [
|
||||
column('Icon', function (i) { return iconImg(i, 48); }),
|
||||
@@ -164,11 +173,8 @@
|
||||
column('Owner', owner, 'owner'),
|
||||
column('State', function (i) {
|
||||
return badge(i.state) + (i.attempts ? ' <span class="small text-body-secondary">' + esc(i.attempts) + ' attempt' + (i.attempts === 1 ? '' : 's') + '</span>' : '') + waitBadge(i);
|
||||
}),
|
||||
column('Made', function (i) { return '<span class="small">' + (i.processedAt ? esc(fmt.unix(i.processedAt)) : '') + '</span>'; }, 'made', true),
|
||||
column('Took', function (i) { return '<span class="small">' + (i.processMs ? esc(duration(i.processMs)) : '') + '</span>'; }, 'slowest', true),
|
||||
column('CPU', function (i) { return '<span class="small">' + (i.processCpuMs ? esc(duration(i.processCpuMs)) : '') + '</span>'; }, 'cpu', true),
|
||||
column('RAM (est.)', function (i) { return '<span class="small">' + (i.processMemoryKb ? '~' + esc(megabytes(i.processMemoryKb)) : '') + '</span>'; }, 'memory', true),
|
||||
})
|
||||
].concat(madeColumns(), [
|
||||
column('Saved', function (i) {
|
||||
if (!i.trianglesBefore) return '';
|
||||
var removed = i.trianglesBefore - i.triangles, share = removed / i.trianglesBefore * 100;
|
||||
@@ -178,17 +184,27 @@
|
||||
column('Size', function (i) { return '<span class="small">' + (i.bricks ? esc(i.bricks) + ' bricks<br>' + esc(i.triangles.toLocaleString()) + ' triangles' : '') + '</span>'; }, 'bricks', true),
|
||||
column('File', function (i) { return '<span class="small" title="' + esc(i.detail || '') + '">' + esc(fileName(i)) + '</span>' + errorText(i); }, 'name'),
|
||||
column('', function (i) { return '<div class="text-end text-nowrap">' + actions(i, i.id) + '</div>'; })
|
||||
],
|
||||
]),
|
||||
// The models' columns but Saved: an assembly's ID and owner are its newest build's, its size its modules, its
|
||||
// file its type and modules, and it is where the builds that use it are (References)
|
||||
modular: [
|
||||
column('Icon', function (i) { return iconImg(i, 48); }),
|
||||
column('Newest build', function (i) { return '<span class="small">' + esc(i.newestBuild) + '</span>'; }, 'newest', true),
|
||||
column('Type', function (i) { return esc(i.kindLabel || i.buildType); }),
|
||||
column('Modules', function (i) { return '<span class="small">' + esc(moduleNames(i)) + '</span><div><code>' + esc(i.key) + '</code></div>' + errorText(i); }, 'name'),
|
||||
column('State', function (i) { return badge(i.state); }),
|
||||
column('Builds', function (i) { return esc(i.uses); }, 'references', true),
|
||||
column('Owners', function (i) { return esc(i.owners); }),
|
||||
column('ID', function (i) { return '<span class="small" title="The newest build">' + esc(i.newestBuild) + '</span>'; }, 'newest', true),
|
||||
column('Owner', function (i) {
|
||||
return owner(i) + (i.owners > 1 ? '<div class="small text-body-secondary">and ' + esc(i.owners - 1) + ' other' + (i.owners === 2 ? '' : 's') + '</div>' : '');
|
||||
}, 'owner'),
|
||||
column('State', function (i) { return badge(i.state); })
|
||||
].concat(madeColumns(), [
|
||||
column('Size', function (i) { return '<span class="small">' + esc((i.moduleList || []).length) + ' modules</span>'; }, 'modules', true),
|
||||
column('File', function (i) {
|
||||
return '<span class="small">' + esc(i.kindLabel || i.buildType) + ': ' + esc(moduleNames(i)) + '</span><div><code class="small">' + esc(i.key) + '</code></div>' + errorText(i);
|
||||
}, 'name'),
|
||||
column('Where', function (i) {
|
||||
return '<span class="small">Used by ' + esc(i.uses) + ' build' + (i.uses === 1 ? '' : 's') + ' of ' + esc(i.owners) + ' owner' + (i.owners === 1 ? '' : 's') +
|
||||
'</span><div><button type="button" class="btn btn-link btn-sm p-0" data-preview="' + esc(i.id) + '">References</button></div>';
|
||||
}, 'references', true),
|
||||
column('', function (i) { return '<div class="text-end text-nowrap">' + actions(i, i.iconBuild) + '</div>'; })
|
||||
]
|
||||
])
|
||||
};
|
||||
// The sort a DataTables order asks for, as /api/ugc's sort= and reverse=
|
||||
function orderQuery(columns, order) {
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<div id="modelTableBox"><table class="table table-sm align-middle mb-0 w-100 table-stack" id="ugcModelTable">
|
||||
<thead><tr><th>Icon</th><th>ID</th><th>Owner</th><th>State</th><th>Made</th><th>Took</th><th>CPU</th><th>RAM (est.)</th><th>Saved</th><th>Size</th><th>File</th><th></th></tr></thead></table></div>
|
||||
<div id="assemblyTableBox" class="d-none"><table class="table table-sm align-middle mb-0 w-100 table-stack" id="ugcAssemblyTable">
|
||||
<thead><tr><th>Icon</th><th>Newest build</th><th>Type</th><th>Modules</th><th>State</th><th>Builds</th><th>Owners</th><th></th></tr></thead></table></div>
|
||||
<thead><tr><th>Icon</th><th>ID</th><th>Owner</th><th>State</th><th>Made</th><th>Took</th><th>CPU</th><th>RAM (est.)</th><th>Size</th><th>File</th><th>Where</th><th></th></tr></thead></table></div>
|
||||
</div></div>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mt-2" id="pagerBar">
|
||||
<nav aria-label="Pages"><ul class="pagination pagination-sm mb-0" id="pager"></ul></nav>
|
||||
|
||||
Reference in New Issue
Block a user