From c4491c3ec06462a50f7d4919172cf566be93c518 Mon Sep 17 00:00:00 2001 From: Tim de Groot Date: Fri, 12 Dec 2025 01:33:20 +0100 Subject: [PATCH 1/4] Implementation of changed order overview bars --- robotframework_dashboard/js/eventlisteners.js | 10 +- .../js/graph_creation/all.js | 2 +- .../js/graph_creation/overview.js | 22 +-- robotframework_dashboard/js/layout.js | 162 +++++++++++------- robotframework_dashboard/js/localstorage.js | 40 ++++- robotframework_dashboard/js/main.js | 4 +- robotframework_dashboard/js/menu.js | 10 +- .../js/variables/graphs.js | 2 +- .../templates/dashboard.html | 15 +- 9 files changed, 173 insertions(+), 94 deletions(-) diff --git a/robotframework_dashboard/js/eventlisteners.js b/robotframework_dashboard/js/eventlisteners.js index cd0b3a96..6891e0a5 100644 --- a/robotframework_dashboard/js/eventlisteners.js +++ b/robotframework_dashboard/js/eventlisteners.js @@ -33,6 +33,7 @@ import { import { create_overview_statistics_graphs, update_overview_statistics_heading, + update_projectbar_visibility, set_filter_show_current_project, set_filter_show_current_version, } from "./graph_creation/overview.js"; @@ -722,8 +723,13 @@ function setup_graph_view_buttons() { } // function to setup collapse buttons and icons -function setup_collapsables(elementToSearch = document) { - elementToSearch.querySelectorAll(".collapse-icon").forEach(icon => { +function setup_collapsables() { + document.querySelectorAll(".collapse-icon").forEach(origIcon => { + // Replace the element with a clone to remove existing listeners + // required to readd collapsables for overview project sections + const icon = origIcon.cloneNode(true); + origIcon.replaceWith(icon); + const sectionId = icon.id.replace("collapse", ""); const update_icon = () => { const section = document.getElementById(sectionId); diff --git a/robotframework_dashboard/js/graph_creation/all.js b/robotframework_dashboard/js/graph_creation/all.js index 8bdcbebe..80842451 100644 --- a/robotframework_dashboard/js/graph_creation/all.js +++ b/robotframework_dashboard/js/graph_creation/all.js @@ -58,7 +58,7 @@ import { function setup_dashboard_graphs() { if (settings.menu.overview) { create_overview_statistics_graphs(); - update_donut_charts("projectOverviewData"); + update_donut_charts(); } else if (settings.menu.dashboard) { create_run_statistics_graph(); create_run_donut_graph(); diff --git a/robotframework_dashboard/js/graph_creation/overview.js b/robotframework_dashboard/js/graph_creation/overview.js index ea2e9eb0..9f345507 100644 --- a/robotframework_dashboard/js/graph_creation/overview.js +++ b/robotframework_dashboard/js/graph_creation/overview.js @@ -71,9 +71,8 @@ function create_overview_statistics_graphs(preFilteredRuns = null) { } } -function update_donut_charts(scopeElement) { - const donutContainer = document.getElementById(scopeElement); - donutContainer.querySelectorAll(".overview-canvas").forEach(canvas => { +function update_donut_charts() { + document.querySelectorAll(".overview-canvas").forEach(canvas => { const chart = canvas.querySelector("canvas").chartInstance; if (chart) chart.update(); }); @@ -123,15 +122,13 @@ function prepare_projects_grouped_data() { } function create_project_overview() { - const projectOverviewData = document.getElementById("projectOverviewData"); - projectOverviewData.innerHTML = ""; const projectData = { ...projects_by_name, ...projects_by_tag }; // create run cards for each project Object.keys(projectData).sort().forEach(projectName => { create_project_cards_container(projectName, projectData[projectName]); }); // setup collapsables specifically for overview project sections - setup_collapsables(projectOverviewData); + setup_collapsables(); // set project bar visibility based on switch settings update_projectbar_visibility(); } @@ -164,7 +161,7 @@ function create_project_bar(projectName, projectRuns, totalRunsAmount, passRate) - Clicking on the run card applies a filter for that project and switches to dashboard - Clicking on the version element within the run card additionally applies a filter for that version`; const projectCard = ` -
+
@@ -208,6 +205,10 @@ function create_project_bar(projectName, projectRuns, totalRunsAmount, passRate)
+
+ + +
@@ -216,7 +217,8 @@ function create_project_bar(projectName, projectRuns, totalRunsAmount, passRate)
`; - projectOverviewData.appendChild(document.createRange().createContextualFragment(projectCard)); + const overview = document.getElementById("overview") + overview.appendChild(document.createRange().createContextualFragment(projectCard)); // percentage selector const projectPercentageSelector = document.getElementById(`${projectName}DurationPercentage`); projectPercentageSelector.addEventListener('change', () => { @@ -374,9 +376,7 @@ function create_overview_run_donut(run, chartElementPostfix, projectName) { // hide project bars based on switch config function update_projectbar_visibility() { - const container = document.getElementById("projectOverviewData"); - if (!container) return; - const bars = container.querySelectorAll('[id$="Card"]'); + const bars = document.querySelectorAll('.overview-project-card'); const tagged = []; const untagged = []; for (const el of bars) { diff --git a/robotframework_dashboard/js/layout.js b/robotframework_dashboard/js/layout.js index 65448303..b21e30ed 100644 --- a/robotframework_dashboard/js/layout.js +++ b/robotframework_dashboard/js/layout.js @@ -21,40 +21,58 @@ function setup_section_order() { document.getElementById("dashboard").hidden = !(settings.menu.dashboard && !settings.show.unified); document.getElementById("compare").hidden = !settings.menu.compare; document.getElementById("tables").hidden = !settings.menu.tables; - let prevId = "#topSection"; - - for (const section of settings.view.dashboard.sections.show) { - const sectionId = space_to_camelcase(section + "Section"); - const sectionEl = document.getElementById(sectionId); - sectionEl.hidden = false; - $(`#${sectionId}`).insertAfter(prevId); - prevId = `#${sectionId}`; - } - for (const section of settings.view.dashboard.sections.hide) { - const sectionId = space_to_camelcase(section + "Section"); - const sectionEl = document.getElementById(sectionId); - if (gridEditMode) { - sectionEl.hidden = false; + const order_sections = (sectionsConfig, topAnchorId) => { + let prevId = `#${topAnchorId}`; + // Show + for (const section of sectionsConfig.show) { + let sectionId; + if (section === "Overview Statistics" || topAnchorId !== "topOverviewSection") { + // 1. Keep overview statistics as-is (camel-cased id) + sectionId = space_to_camelcase(section + "Section"); + } else { + // 2. For overview non-defaults, use raw id pattern: section+"Section" + sectionId = section + "Section"; + } + const sectionEl = document.getElementById(sectionId); + if (!sectionEl) continue; + if (topAnchorId === "topDashboardSection") { + sectionEl.hidden = false; + } $(`#${sectionId}`).insertAfter(prevId); prevId = `#${sectionId}`; - } else { - sectionEl.hidden = true; } - } + // Hide + for (const section of sectionsConfig.hide) { + const sectionId = space_to_camelcase(section + "Section"); + const sectionEl = document.getElementById(sectionId); + if (!sectionEl) continue; + if (gridEditMode) { + sectionEl.hidden = false; + $(`#${sectionId}`).insertAfter(prevId); + prevId = `#${sectionId}`; + } else { + sectionEl.hidden = true; + } + } + }; + + order_sections(settings.view.dashboard.sections, "topDashboardSection"); + order_sections(settings.view.overview.sections, "topOverviewSection"); if (gridEditMode) { document.querySelectorAll(".move-up-section").forEach(btn => { btn.hidden = false }) document.querySelectorAll(".move-down-section").forEach(btn => { btn.hidden = false }) document.querySelectorAll(".shown-section, .hidden-section").forEach(btn => { const prefix = btn.id.slice(0, 3); - const label = prefix.charAt(0).toUpperCase() - var shouldShow = false; - for (const section of settings.view.dashboard.sections.show) { - if (section.startsWith(label)) { - shouldShow = true; - break - } - } + const label = prefix.charAt(0).toUpperCase(); + // Decide context: dashboard or overview based on the containing section/card + const isOverview = !!btn.closest('#overview'); + const showList = isOverview + ? settings.view.overview.sections.show + : settings.view.dashboard.sections.show; + + let shouldShow = showList.some(section => section.startsWith(label)); + if (btn.classList.contains("shown-section")) { btn.hidden = !shouldShow; } else if (btn.classList.contains("hidden-section")) { @@ -338,13 +356,13 @@ function save_layout() { set_local_storage_item("view.tables.graphs.hide", hiddenTables) } // save dashboard section layout - const shownDashboardSections = [...document.querySelectorAll(".shown-section:not([hidden])")] + const shownDashboardSections = [...document.querySelectorAll("#dashboard .shown-section:not([hidden])")] .map(el => { var key = el.id.replace("SectionShown", ""); key = String(key).charAt(0).toUpperCase() + String(key).slice(1); return `${key} Statistics` }); - const hiddenDashboardSections = [...document.querySelectorAll(".hidden-section:not([hidden])")] + const hiddenDashboardSections = [...document.querySelectorAll("#dashboard .hidden-section:not([hidden])")] .map(el => { var key = el.id.replace("SectionHidden", ""); key = String(key).charAt(0).toUpperCase() + String(key).slice(1); @@ -354,6 +372,17 @@ function save_layout() { set_local_storage_item("view.dashboard.sections.show", shownDashboardSections) set_local_storage_item("view.dashboard.sections.hide", hiddenDashboardSections) } + // save overview section layout always shown + const shownOverviewSections = [...document.querySelectorAll("#overview .move-up-section")] + .map(el => { + if (el.id === "overviewSectionMoveUp") { + return "Overview Statistics" + } + return el.id.replace("SectionMoveUp", ""); + }); + if (shownOverviewSections.length > 0) { + set_local_storage_item("view.overview.sections.show", shownOverviewSections) + } add_alert("Layout has been updated and saved to settings in local storage!", "success") } @@ -384,52 +413,38 @@ function setup_edit_mode_icons(hidden) { } } -// function to add the layout eventlisteners -function setup_layout() { - document.addEventListener("graphs-finalized", () => { - if (gridEditMode) { - setup_edit_mode_icons(true); - } else { - setup_edit_mode_icons(false); - } - }); - document.getElementById("customizeLayout").addEventListener("click", (e) => { - gridEditMode = !gridEditMode; - customize_layout(); - setup_data_and_graphs(); - }); - document.getElementById("saveLayout").addEventListener("click", (e) => { - gridEditMode = !gridEditMode; - save_layout() - setup_data_and_graphs(); - }); - // disable or enable sections - document.querySelectorAll(".shown-section").forEach(btn => { +// Reusable handler to wire show/hide and move controls within a container +function attach_section_order_buttons(containerId) { + const root = `#${containerId}`; + // Toggle shown/hidden buttons + document.querySelectorAll(`${root} .shown-section`).forEach(btn => { btn.addEventListener("click", () => { btn.hidden = true; - document.getElementById(`${btn.id.replace("Shown", "Hidden")}`).hidden = false; - }) + const target = document.getElementById(`${btn.id.replace("Shown", "Hidden")}`); + if (target) target.hidden = false; + }); }); - document.querySelectorAll(".hidden-section").forEach(btn => { + document.querySelectorAll(`${root} .hidden-section`).forEach(btn => { btn.addEventListener("click", () => { btn.hidden = true; - document.getElementById(`${btn.id.replace("Hidden", "Shown")}`).hidden = false; - }) + const target = document.getElementById(`${btn.id.replace("Hidden", "Shown")}`); + if (target) target.hidden = false; + }); }); - // move sections up or down - document.querySelectorAll(".move-up-section").forEach(btn => { + // Move cards up/down within the container + document.querySelectorAll(`${root} .move-up-section`).forEach(btn => { btn.addEventListener("click", () => { const card = btn.closest(".card"); - const previousCard = card.previousElementSibling; + const previousCard = card?.previousElementSibling; if (previousCard && !previousCard.hidden && previousCard.classList.contains("card")) { card.parentNode.insertBefore(card, previousCard); } }); }); - document.querySelectorAll(".move-down-section").forEach(btn => { + document.querySelectorAll(`${root} .move-down-section`).forEach(btn => { btn.addEventListener("click", () => { const card = btn.closest(".card"); - const nextCard = card.nextElementSibling; + const nextCard = card?.nextElementSibling; if (nextCard && !nextCard.hidden && nextCard.classList.contains("card")) { card.parentNode.insertBefore(nextCard, card); } @@ -437,9 +452,38 @@ function setup_layout() { }); } +// function to add the layout eventlisteners +function setup_dashboard_section_layout_buttons() { + document.addEventListener("graphs-finalized", () => { + if (gridEditMode) { + setup_edit_mode_icons(true); + } else { + setup_edit_mode_icons(false); + } + }); + document.getElementById("customizeLayout").addEventListener("click", (e) => { + gridEditMode = !gridEditMode; + customize_layout(); + setup_data_and_graphs(); + }); + document.getElementById("saveLayout").addEventListener("click", (e) => { + gridEditMode = !gridEditMode; + save_layout() + setup_data_and_graphs(); + }); + attach_section_order_buttons("dashboard"); +} + +// function to separately add the eventlisteners for overview section layout buttons +// this happens on first render of overview section only +function setup_overview_section_layout_buttons() { + attach_section_order_buttons("overview"); +} + export { setup_section_order, setup_graph_order, setup_tables, - setup_layout, + setup_dashboard_section_layout_buttons, + setup_overview_section_layout_buttons, }; \ No newline at end of file diff --git a/robotframework_dashboard/js/localstorage.js b/robotframework_dashboard/js/localstorage.js index 2e981656..d4d9e206 100644 --- a/robotframework_dashboard/js/localstorage.js +++ b/robotframework_dashboard/js/localstorage.js @@ -1,6 +1,8 @@ import { add_alert } from "./common.js"; +import { overviewSections } from "./variables/graphs.js"; import { settings } from "./variables/settings.js"; import { force_json_config, json_config, admin_json_config } from "./variables/data.js"; +import { projects_by_name, projects_by_tag } from "./variables/globals.js"; // function to setup localstorage on first load function setup_local_storage() { @@ -127,7 +129,8 @@ function merge_view(localView, defaultView) { result[page] = { sections: merge_view_section_or_graph( localPage.sections || {}, - defaultPage.sections + defaultPage.sections, + page ), graphs: merge_view_section_or_graph( localPage.graphs || {}, @@ -139,8 +142,9 @@ function merge_view(localView, defaultView) { } // function to merge view sections or graphs from localstorage with defaults from settings -function merge_view_section_or_graph(local, defaults) { +function merge_view_section_or_graph(local, defaults, page = null) { const result = { show: [], hide: [] }; + const isOverview = page === "overview"; const allowed = new Set([ ...defaults.show, ...defaults.hide @@ -148,11 +152,17 @@ function merge_view_section_or_graph(local, defaults) { const localShow = new Set(local.show || []); const localHide = new Set(local.hide || []); // 1. Remove values not in defaults (allowed) + // For overview, preserve additional dynamic items in SHOW (added later), + // but still clean up HIDE to avoid hiding unknown entries. for (const val of [...localShow]) { - if (!allowed.has(val)) localShow.delete(val); + if (!allowed.has(val) && !isOverview) { + localShow.delete(val); + } } for (const val of [...localHide]) { - if (!allowed.has(val)) localHide.delete(val); + if (!allowed.has(val)) { + localHide.delete(val); + } } // 2. Add missing defaults: always added to SHOW for (const val of allowed) { @@ -229,10 +239,30 @@ function update_graph_type(graph, type) { set_local_storage_item('graphTypes', settings.graphTypes); } +// function to setup the overview sections that are dynamically created +function setup_overview_localstorage() { + if (Object.keys(projects_by_name).length > 0) { + Object.keys(projects_by_name).forEach(projectName => { + overviewSections.push(projectName) + }); + } + if (Object.keys(projects_by_tag).length > 0) { + Object.keys(projects_by_tag).forEach(tagName => { + overviewSections.push(tagName) + }); + } + // on first load without localstorage only overview sections is present + // if more items are available, set them in localstorage, previous order is lost + if (settings.view.overview.sections.show.length < overviewSections.length) { + set_local_storage_item("view.overview.sections.show", overviewSections) + } +} + export { setup_local_storage, set_local_storage_item, set_nested_setting, update_switch_local_storage, - update_graph_type + update_graph_type, + setup_overview_localstorage }; \ No newline at end of file diff --git a/robotframework_dashboard/js/main.js b/robotframework_dashboard/js/main.js index 8da9c45b..f46f3bc0 100644 --- a/robotframework_dashboard/js/main.js +++ b/robotframework_dashboard/js/main.js @@ -1,6 +1,6 @@ import { setup_local_storage } from "./localstorage.js"; import { setup_database_stats } from "./database.js"; -import { setup_layout } from "./layout.js"; +import { setup_dashboard_section_layout_buttons } from "./layout.js"; import { setup_sections_filters, setup_collapsables, @@ -14,7 +14,7 @@ import { setup_menu } from "./menu.js"; function main() { setup_local_storage(); setup_database_stats(); - setup_layout(); + setup_dashboard_section_layout_buttons(); setup_sections_filters(); setup_collapsables(); setup_filter_modal(); diff --git a/robotframework_dashboard/js/menu.js b/robotframework_dashboard/js/menu.js index 2547517e..3d9433c1 100644 --- a/robotframework_dashboard/js/menu.js +++ b/robotframework_dashboard/js/menu.js @@ -1,12 +1,12 @@ import { setup_filtered_data_and_filters, update_overview_version_select_list } from "./filter.js"; import { areGroupedProjectsPrepared } from "./variables/globals.js"; import { space_to_camelcase } from "./common.js"; -import { set_local_storage_item } from "./localstorage.js"; +import { set_local_storage_item, setup_overview_localstorage } from "./localstorage.js"; import { setup_dashboard_graphs } from "./graph_creation/all.js"; import { settings } from "./variables/settings.js"; import { setup_theme } from "./theme.js"; import { setup_graph_view_buttons } from "./eventlisteners.js"; -import { setup_section_order, setup_graph_order } from "./layout.js"; +import { setup_section_order, setup_graph_order, setup_overview_section_layout_buttons } from "./layout.js"; import { setup_information_popups } from "./information.js"; import { update_overview_statistics_heading, prepare_overview } from "./graph_creation/overview.js"; @@ -129,8 +129,6 @@ function update_menu(item) { }); ["menuOverview", "menuDashboard", "menuCompare", "menuTables"].forEach(id => { document.getElementById(id).classList.toggle("active", id === item); - document.getElementById("customizeLayout").hidden = item == "menuOverview"; - document.getElementById("projectOverview").hidden = item !== "menuOverview"; }); setup_data_and_graphs(true, item === "menuOverview" && !areGroupedProjectsPrepared); } @@ -189,6 +187,8 @@ function setup_data_and_graphs(menuUpdate = false, prepareOverviewProjectData = requestAnimationFrame(() => { if (prepareOverviewProjectData) { prepare_overview(); + setup_overview_localstorage(); + setup_overview_section_layout_buttons(); update_overview_version_select_list(); update_overview_statistics_heading(); } @@ -231,14 +231,12 @@ function setup_spinner(hide) { // Instant transition - hide spinner and show all content immediately $("#loading").fadeOut(200); $("#overview").fadeIn(200); - $("#projectOverview").fadeIn(200); $("#unified").fadeIn(200); $("#dashboard").fadeIn(200); $("#compare").fadeIn(200); $("#tables").fadeIn(200); } else { $("#overview").hide() - $("#projectOverview").hide() $("#unified").hide() $("#dashboard").hide() $("#compare").hide() diff --git a/robotframework_dashboard/js/variables/graphs.js b/robotframework_dashboard/js/variables/graphs.js index e0689a7b..cb4a8c37 100644 --- a/robotframework_dashboard/js/variables/graphs.js +++ b/robotframework_dashboard/js/variables/graphs.js @@ -34,7 +34,7 @@ graphVars.forEach(name => { const compareRunIds = ['compareRun1', 'compareRun2', 'compareRun3', 'compareRun4'] // customize view lists -const overviewSections = ["Overview"] +const overviewSections = ["Overview Statistics"] const dashboardSections = ["Run Statistics", "Suite Statistics", "Test Statistics", "Keyword Statistics",] const unifiedSections = ["Dashboard Statistics"] const compareSections = ["Compare Statistics"] diff --git a/robotframework_dashboard/templates/dashboard.html b/robotframework_dashboard/templates/dashboard.html index ec992eaf..362ce91c 100644 --- a/robotframework_dashboard/templates/dashboard.html +++ b/robotframework_dashboard/templates/dashboard.html @@ -90,6 +90,7 @@ @@ -166,11 +171,6 @@

-