From 8f99ed930b9d7403462a72a0df14a3a57021cfd5 Mon Sep 17 00:00:00 2001 From: Tom Bishop Date: Tue, 28 May 2024 21:01:39 -0400 Subject: [PATCH] CLDR-17658 Improve Dashboard performance and UI; work in progress (#3752) --- tools/cldr-apps/js/src/esm/cldrDash.mjs | 170 +++++++++++++--- tools/cldr-apps/js/src/views/AboutPanel.vue | 9 + .../js/src/views/DashboardWidget.vue | 53 ++++- tools/cldr-apps/js/test/TestCldrDash.mjs | 184 ++++++++++++++++++ .../java/org/unicode/cldr/web/Dashboard.java | 3 - 5 files changed, 381 insertions(+), 38 deletions(-) diff --git a/tools/cldr-apps/js/src/esm/cldrDash.mjs b/tools/cldr-apps/js/src/esm/cldrDash.mjs index 7ab063dfb02..fffc331d4be 100644 --- a/tools/cldr-apps/js/src/esm/cldrDash.mjs +++ b/tools/cldr-apps/js/src/esm/cldrDash.mjs @@ -10,6 +10,13 @@ import * as cldrStatus from "./cldrStatus.mjs"; import * as cldrSurvey from "./cldrSurvey.mjs"; import * as XLSX from "xlsx"; +/** + * Notifications in these categories are combined so that there is not more than one per page. + * These categories can have well over 10,000 notifications, causing performance problems on + * the front end. + */ +const CATS_ONE_PER_PAGE = ["Abstained" /* , "Missing" */]; + class DashData { /** * Construct a new DashData object @@ -24,6 +31,8 @@ class DashData { // An object whose keys are xpstrid (xpath hex IDs like "db7b4f2df0427e4"), and whose values are DashEntry objects this.pathIndex = {}; this.hiddenObject = null; + this.pageCombinedEntries = {}; // map page to array of notification xpstrid on that page + this.updatingPath = false; } addEntriesFromJson(notifications) { @@ -44,32 +53,107 @@ class DashData { * @param {Object} e (entry in old format, from json) */ addEntry(cat, group, e) { - this.addCategory(cat); - this.catSize[cat]++; - if (!this.catFirst[cat]) { - this.catFirst[cat] = e.xpstrid; + try { + this.addCategory(cat); + this.catSize[cat]++; + if (!this.catFirst[cat]) { + this.catFirst[cat] = e.xpstrid; + } + if (CATS_ONE_PER_PAGE.includes(cat)) { + this.addCombinedEntry(cat, group, e); + } else if (this.pathIndex[e.xpstrid]) { + this.updateEntry(cat, group, e); + } else { + this.addNewEntry(cat, group, e); + } + } catch (err) { + console.error("Error in addEntry: " + err); } - if (this.pathIndex[e.xpstrid]) { - const dashEntry = this.pathIndex[e.xpstrid]; - dashEntry.addCategory(cat); - dashEntry.setWinning(e.winning); - if (e.comment) { - dashEntry.setComment(e.comment); + } + + addCombinedEntry(cat, group, e) { + try { + const page = group.page; + if (!this.pageCombinedEntries[cat]) { + this.pageCombinedEntries[cat] = {}; } - if (e.subtype) { - dashEntry.setSubtype(e.subtype); + if (!this.pageCombinedEntries[cat][page]?.length) { + this.pageCombinedEntries[cat][page] = new Array(e.xpstrid); + this.addNewEntry(cat, group, e); + } else { + // TODO: make this work. unshift instead of push may be appropriate + // during an update (following a vote), since the combined notification + // is temporarily removed then added back, and in this case it may belong + // at the start of the array, not the end. + // Reference: https://unicode-org.atlassian.net/browse/CLDR-17658 + if (this.updatingPath) { + this.pageCombinedEntries[cat][page].unshift(e.xpstrid); + } else { + this.pageCombinedEntries[cat][page].push(e.xpstrid); + } + // Use the FIRST item in the array as the representative, + // with its comment indicating the size of the array + const xpstrid = this.pageCombinedEntries[cat][page][0]; + if (!xpstrid) { + console.error( + "Existing xpstrid not found in addCombinedEntries for cat = " + + cat + + ", page = " + + page + ); + return; + } + const dashEntry = this.pathIndex[xpstrid]; + if (!dashEntry) { + console.error( + "Existing entry not found in addCombinedEntry for cat = " + + cat + + ", page = " + + page + ); + return; + } + this.setComment(dashEntry, cat, page, null); } - } else { - const dashEntry = new DashEntry(e.xpstrid, e.code, e.english); - dashEntry.setSectionPageHeader(group.section, group.page, group.header); - dashEntry.addCategory(cat); - dashEntry.setWinning(e.winning); - dashEntry.setPreviousEnglish(e.previousEnglish); - dashEntry.setComment(e.comment); + } catch (err) { + console.error("Error in addCombinedEntry: " + err); + } + } + + updateEntry(cat, group, e) { + const dashEntry = this.pathIndex[e.xpstrid]; + dashEntry.addCategory(cat); + dashEntry.setWinning(e.winning); + this.setComment(dashEntry, cat, group.page, e.comment); + if (e.subtype) { dashEntry.setSubtype(e.subtype); - dashEntry.setChecked(this.itemIsChecked(e)); - this.entries.push(dashEntry); - this.pathIndex[e.xpstrid] = dashEntry; + } + } + + addNewEntry(cat, group, e) { + const dashEntry = new DashEntry(e.xpstrid, e.code, e.english); + dashEntry.setSectionPageHeader(group.section, group.page, group.header); + dashEntry.addCategory(cat); + dashEntry.setWinning(e.winning); + dashEntry.setPreviousEnglish(e.previousEnglish); + this.setComment(dashEntry, cat, group.page, e.comment); + dashEntry.setSubtype(e.subtype); + dashEntry.setChecked(this.itemIsChecked(e)); + this.entries.push(dashEntry); + this.pathIndex[e.xpstrid] = dashEntry; + return e.xpstrid; + } + + setComment(dashEntry, cat, page, comment) { + if (CATS_ONE_PER_PAGE.includes(cat)) { + dashEntry.setComment( + "Total " + + cat + + " entries on this page: " + + this.pageCombinedEntries[cat][page].length + ); + } else { + dashEntry.setComment(comment); } } @@ -150,9 +234,13 @@ class DashData { } removeEntry(dashEntry) { + const xpstrid = dashEntry.xpstrid; this.removeEntryCats(dashEntry); - const index = this.entries.indexOf(dashEntry); - this.entries.splice(index, 1); + // Changed xpstrid means the entry wasn't really removed but was kept as representative of combined category + if (xpstrid === dashEntry.xpstrid) { + const index = this.entries.indexOf(dashEntry); + this.entries.splice(index, 1); + } } removeEntryCats(dashEntry) { @@ -162,12 +250,42 @@ class DashData { this.cats.delete(cat); delete this.catSize[cat]; } - if (this.catFirst[cat] === dashEntry.xpstrid) { + if (CATS_ONE_PER_PAGE.includes(cat)) { + this.removeCombinedEntryCat(dashEntry, cat); + } else if (this.catFirst[cat] === dashEntry.xpstrid) { this.findCatFirst(cat); } }); } + removeCombinedEntryCat(dashEntry, cat) { + const page = dashEntry.page; + this.pageCombinedEntries[cat][page].shift(); + if (this.pageCombinedEntries[cat][page].length > 0) { + if (this.catFirst[cat] === dashEntry.xpstrid) { + const nextXpstrid = this.pageCombinedEntries[cat][page][0]; + // If this is the only category for this entry, then we can revise the + // entry to use the next xpstrid for the page -- unless that next xpstrid + // is already present for a different category... + if (dashEntry.cats.size === 1 && !this.pathIndex[nextXpstrid]) { + dashEntry.xpstrid = nextXpstrid; + delete this.pathIndex.xpstrid; + this.pathIndex[nextXpstrid] = dashEntry; + this.catFirst[cat] = nextXpstrid; + this.setComment(dashEntry, cat, page, null); + } else { + // TODO: fix this; work in progress + // Reference: https://unicode-org.atlassian.net/browse/CLDR-17658 + // Remove this cat from the existing entry + dashEntry.cats.delete(cat); + // if (dashEntry.cats.size === 0) { + // delete this.pathIndex.xpstrid; + // } + } + } + } + } + findCatFirst(cat) { for (let dashEntry of this.entries) { if (dashEntry.cat === cat) { @@ -335,6 +453,7 @@ function convertData(json) { * containing notifications for a single path (old format) */ function updatePath(dashData, json) { + this.updatingPath = true; try { if (json.xpstrid in dashData.pathIndex) { // We already have an entry for this path @@ -352,6 +471,7 @@ function updatePath(dashData, json) { } catch (e) { cldrNotify.exception(e, "updating path for Dashboard"); } + this.updatingPath = false; return dashData; // for unit test } diff --git a/tools/cldr-apps/js/src/views/AboutPanel.vue b/tools/cldr-apps/js/src/views/AboutPanel.vue index 07ff532c331..6e611658fbd 100644 --- a/tools/cldr-apps/js/src/views/AboutPanel.vue +++ b/tools/cldr-apps/js/src/views/AboutPanel.vue @@ -42,12 +42,19 @@ + + Vue version + + {{ vueVersion }} + +