diff --git a/robotframework_dashboard/css/styling.css b/robotframework_dashboard/css/styling.css
index 6c0f9ce..7bedffd 100644
--- a/robotframework_dashboard/css/styling.css
+++ b/robotframework_dashboard/css/styling.css
@@ -1,3 +1,10 @@
+#overview,
+#dashboard,
+#unified,
+#compare,
+#tables {
+ margin-bottom: 35vh;
+}
/* LIGHT MODE STYLING */
body {
background-color: #eee;
diff --git a/robotframework_dashboard/js/eventlisteners.js b/robotframework_dashboard/js/eventlisteners.js
index cd0b3a9..c618e0d 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);
@@ -748,11 +754,73 @@ function attach_run_card_version_listener(versionElement, projectName, projectVe
});
}
+function setup_overview_order_filters() {
+ const parseProjectId = (selectId) => selectId.replace(/SectionOrder$/i, "");
+ const parseRunStatsFromCard = (cardEl) => {
+ const text = cardEl.innerText || "";
+ const runMatch = text.match(/#\s*(\d+)/); // e.g., "#8"
+ const passedMatch = text.match(/Passed:\s*(\d+)/i);
+ const failedMatch = text.match(/Failed:\s*(\d+)/i);
+ const skippedMatch = text.match(/Skipped:\s*(\d+)/i);
+ return {
+ runNumber: runMatch ? parseInt(runMatch[1]) : 0,
+ passed: passedMatch ? parseInt(passedMatch[1]) : 0,
+ failed: failedMatch ? parseInt(failedMatch[1]) : 0,
+ skipped: skippedMatch ? parseInt(skippedMatch[1]) : 0,
+ };
+ };
+
+ const reorderProjectCards = (projectId, order) => {
+ // Determine correct container for both overview and project sections
+ const containerId = `${projectId}RunCardsContainer`;
+ const container = document.getElementById(containerId);
+ if (!container) return; // guard against missing containers
+ const cards = Array.from(container.querySelectorAll('.overview-card'));
+ if (cards.length === 0) return;
+ const enriched = cards.map(card => ({ el: card, stats: parseRunStatsFromCard(card) }));
+
+ const cmpDesc = (a, b, key) => (b.stats[key] - a.stats[key]);
+ const cmpAsc = (a, b, key) => (a.stats[key] - b.stats[key]);
+
+ if (order === "oldest" || order.toLowerCase() === "oldest run") {
+ enriched.sort((a, b) => cmpAsc(a, b, 'runNumber'));
+ } else if (order === "most failed") {
+ enriched.sort((a, b) => cmpDesc(a, b, 'failed'));
+ } else if (order === "most skipped") {
+ enriched.sort((a, b) => cmpDesc(a, b, 'skipped'));
+ } else if (order === "most passed") {
+ enriched.sort((a, b) => cmpDesc(a, b, 'passed'));
+ } else {
+ enriched.sort((a, b) => cmpDesc(a, b, 'runNumber'));
+ }
+ const fragment = document.createDocumentFragment();
+ enriched.forEach(item => fragment.appendChild(item.el));
+ container.innerHTML = '';
+ container.appendChild(fragment);
+ };
+
+ document.querySelectorAll('.section-order-filter').forEach(select => {
+ const selectId = select.id;
+ if (selectId === "overviewStatisticsSectionOrder") {
+ select.addEventListener('change', (e) => {
+ create_overview_statistics_graphs();
+ });
+ } else {
+ const projectId = parseProjectId(selectId);
+ select.addEventListener('change', (e) => {
+ const order = (e.target.value || '').toLowerCase();
+ reorderProjectCards(projectId, order);
+ });
+ }
+ });
+}
+
export {
setup_filter_modal,
setup_settings_modal,
setup_sections_filters,
setup_graph_view_buttons,
setup_collapsables,
- attach_run_card_version_listener
+ attach_run_card_version_listener,
+ setup_overview_order_filters
};
\ No newline at end of file
diff --git a/robotframework_dashboard/js/graph_creation/all.js b/robotframework_dashboard/js/graph_creation/all.js
index 8bdcbeb..8084245 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 011b6af..5f5d73a 100644
--- a/robotframework_dashboard/js/graph_creation/overview.js
+++ b/robotframework_dashboard/js/graph_creation/overview.js
@@ -7,7 +7,6 @@ import {
import { update_menu } from '../menu.js';
import {
setup_collapsables,
- setup_filter_checkbox_handler_listeners,
attach_run_card_version_listener
} from '../eventlisteners.js';
import { clockDarkSVG, clockLightSVG, arrowRight } from '../variables/svg.js';
@@ -32,10 +31,11 @@ import {
areGroupedProjectsPrepared
} from '../variables/globals.js';
import { runs, use_logs } from '../variables/data.js';
-import { clear_all_filters, update_filter_active_indicator } from '../filter.js';
+import { clear_all_filters, update_filter_active_indicator, setup_filter_checkbox_handler_listeners } from '../filter.js';
// function to create overview statistics blocks in the overview section
function create_overview_statistics_graphs(preFilteredRuns = null) {
+ const order = document.getElementById("overviewStatisticsSectionOrder").value;
const overviewCardsContainer = document.getElementById("overviewRunCardsContainer");
overviewCardsContainer.innerHTML = '';
const allProjects = { ...projects_by_name, ...projects_by_tag };
@@ -44,13 +44,32 @@ function create_overview_statistics_graphs(preFilteredRuns = null) {
const durations = projectRuns.map(r => r.elapsed_s);
durationsByProject[projectName] = durations;
}
- const latestRunByProject = {};
+ let latestRunByProject = {};
if (!preFilteredRuns) {
settings.switch.runName && Object.assign(latestRunByProject, latestRunByProjectName);
settings.switch.runTags && Object.assign(latestRunByProject, latestRunByProjectTag);
} else { // if called by version filter listener
Object.assign(latestRunByProject, preFilteredRuns);
}
+ // default order by newest (keep current insertion order)
+ if (order === 'oldest') {
+ // Reverse current order while preserving the same key->value pairs
+ latestRunByProject = Object.fromEntries(
+ Object.entries(latestRunByProject).reverse()
+ );
+ } else if (order === 'most failed') {
+ latestRunByProject = Object.fromEntries(
+ Object.entries(latestRunByProject).sort(([, runA], [, runB]) => runB.failed - runA.failed)
+ );
+ } else if (order === 'most skipped') {
+ latestRunByProject = Object.fromEntries(
+ Object.entries(latestRunByProject).sort(([, runA], [, runB]) => runB.skipped - runA.skipped)
+ );
+ } else if (order === 'most passed') {
+ latestRunByProject = Object.fromEntries(
+ Object.entries(latestRunByProject).sort(([, runA], [, runB]) => runB.passed - runA.passed)
+ );
+ }
for (const [projectName, latestRun] of Object.entries(latestRunByProject)) {
const projectRuns = allProjects[projectName];
const totalRunsAmount = projectRuns.length;
@@ -71,9 +90,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 +141,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 +180,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 = `
-
+
@@ -216,7 +250,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 +409,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 6544830..1fc1ac9 100644
--- a/robotframework_dashboard/js/layout.js
+++ b/robotframework_dashboard/js/layout.js
@@ -21,40 +21,76 @@ 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");
+
+ // expand only the top section in the overview page
+ const overviewBars = document.querySelectorAll("#overview .overview-bar");
+ overviewBars.forEach((bar, i) => {
+ const btn = bar.querySelector(".collapse-icon");
+ const isExpanded = !!btn.querySelector(".lucide-chevron-down-icon"); // ▼
+ const isCollapsed = !!btn.querySelector(".lucide-chevron-right-icon"); // ▶
+ if (i === 0) {
+ if (isCollapsed) {
+ btn.click();
+ }
+ return;
+ }
+ if (isExpanded) {
+ btn.click();
+ }
+ });
+
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 +374,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 +390,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 +431,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 +470,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 2e98165..d4d9e20 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 8da9c45..f46f3bc 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 2547517..e11668a 100644
--- a/robotframework_dashboard/js/menu.js
+++ b/robotframework_dashboard/js/menu.js
@@ -1,19 +1,87 @@
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_graph_view_buttons, setup_overview_order_filters } from "./eventlisteners.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";
+// Track overview nav listeners so we can cleanly remove them when leaving Overview
+let __overviewNavStore = {
+ scrollHandler: null,
+ resizeHandler: null,
+};
+
+// ---- Shared helpers for menu buttons ----
+function get_sticky_height() {
+ const stickyTop = document.getElementById("navigation");
+ return stickyTop ? stickyTop.offsetHeight : 0;
+}
+
+function expand_and_scroll_to(targetEl) {
+ const stickyHeight = get_sticky_height();
+ const performScroll = () => {
+ const targetTop = targetEl.getBoundingClientRect().top + window.pageYOffset;
+ const top = targetTop < 200 ? 0 : targetTop - stickyHeight - 8;
+ window.scrollTo({ top: top - 7, behavior: "auto" });
+ };
+ const collapseBtn = targetEl.querySelector(".collapse-icon");
+ if (collapseBtn) {
+ const svg = collapseBtn.querySelector("svg");
+ const isExpanded = svg && (svg.classList.contains("lucide-chevron-down-icon") || svg.classList.contains("lucide-chevron-down"));
+ if (!isExpanded) {
+ collapseBtn.click();
+ requestAnimationFrame(() => setTimeout(performScroll, 50));
+ return;
+ }
+ }
+ performScroll();
+}
+
+function compute_best_visible_index(sections) {
+ const viewportHeight = window.innerHeight;
+ const viewportTop = viewportHeight * 0.2;
+ const viewportBottom = viewportHeight * 0.5;
+ let bestIndex = 0;
+ let bestAmount = -Infinity;
+ sections.forEach((section, idx) => {
+ const rect = section.getBoundingClientRect();
+ const top = Math.max(rect.top, viewportTop);
+ const bottom = Math.min(rect.bottom, viewportBottom);
+ const overlap = bottom - top;
+ if (overlap > bestAmount) {
+ bestAmount = overlap;
+ bestIndex = idx;
+ }
+ });
+ return bestIndex;
+}
+
+function neighbor_indices(bestIndex, length) {
+ const indices = [];
+ const pushIfValid = (i) => { if (i >= 0 && i < length) indices.push(i); };
+ if (bestIndex <= 0) {
+ pushIfValid(0); pushIfValid(1); pushIfValid(2);
+ } else if (bestIndex >= length - 1) {
+ pushIfValid(length - 3);
+ pushIfValid(length - 2);
+ pushIfValid(length - 1);
+ } else {
+ pushIfValid(bestIndex - 1);
+ pushIfValid(bestIndex);
+ pushIfValid(bestIndex + 1);
+ }
+ return indices;
+}
+
// function to update the section (menu) buttons with the correct eventlisteners
// also sets up the automatic highlighting of the section that is most visible in the top
// 20-50% percent of the screen
-function setup_section_menu_buttons() {
+function setup_dashboard_section_menu_buttons() {
const sectionButtons = [
document.getElementById("runStatisticsSectionNav"),
document.getElementById("suiteStatisticsSectionNav"),
@@ -37,23 +105,8 @@ function setup_section_menu_buttons() {
const sections = Object.keys(sectionMap).map(id => document.getElementById(id));
function update_active_section() {
- const viewportHeight = window.innerHeight;
- const viewportTop = viewportHeight * 0.2;
- const viewportBottom = viewportHeight * 0.5;
- let bestMatch = null;
- let bestMatchAmount = 0;
- sections.forEach(section => {
- const rect = section.getBoundingClientRect();
- // Calculate overlap in the 20%-50% viewport vertical range
- const top = Math.max(rect.top, viewportTop);
- const bottom = Math.min(rect.bottom, viewportBottom);
- const overlap = bottom - top;
- if (overlap > bestMatchAmount) {
- bestMatch = section;
- bestMatchAmount = overlap;
- }
- });
-
+ const bestIndex = compute_best_visible_index(sections);
+ const bestMatch = sections[bestIndex];
// Highlight the matching button
sectionButtons.forEach(btn => btn.classList.remove("active"));
if (bestMatch && sectionMap[bestMatch.id]) {
@@ -69,17 +122,165 @@ function setup_section_menu_buttons() {
btn.addEventListener("click", () => {
const target = document.getElementById(btn.id.slice(0, -3));
if (target) {
+ expand_and_scroll_to(target);
+ }
+ });
+ });
+}
+
+// function to create and manage overview section buttons that highlight dynamically
+// Shows at most 3 buttons: the most visible section in the 20%-50% viewport band
+// plus one above and one below it. At edges, show first or last 3 accordingly.
+function setup_overview_section_menu_buttons() {
+ // Only render when Overview menu is active; otherwise remove any existing dynamic buttons
+ const isOverviewActive = !!(settings.menu && settings.menu.overview);
+ const navbar = document.querySelector(".navbar-nav");
+ const overviewMenuLink = document.getElementById("menuOverview");
+ if (!navbar || !overviewMenuLink) return;
+
+ // Cleanup any previously created overview dynamic buttons
+ const existingOverviewButtons = Array.from(navbar.querySelectorAll('a[id^="overview-"][id$="Nav"]'));
+ if (!isOverviewActive) {
+ existingOverviewButtons.forEach(el => el.remove());
+ // Detach listeners if still attached
+ if (__overviewNavStore.scrollHandler) {
+ window.removeEventListener("scroll", __overviewNavStore.scrollHandler);
+ __overviewNavStore.scrollHandler = null;
+ }
+ if (__overviewNavStore.resizeHandler) {
+ window.removeEventListener("resize", __overviewNavStore.resizeHandler);
+ __overviewNavStore.resizeHandler = null;
+ }
+ return;
+ }
+
+ const sections = Array.from(document.querySelectorAll("#overview .overview-bar"))
+ .filter(el => el.offsetParent !== null);
+ if (sections.length === 0) return;
+
+ // Helper to insert after Overview menu item
+ const insertAfter = (newNode, referenceNode) => {
+ if (referenceNode.nextSibling) {
+ referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
+ } else {
+ referenceNode.parentNode.appendChild(newNode);
+ }
+ };
+
+ // Build a map of sectionId -> button (anchor) in navbar
+ const buttonMap = new Map();
+ const makeButtonForSection = (sectionEl) => {
+ const sectionId = sectionEl.id || "";
+ let baseName = sectionId.replace(/Section$/i, "");
+ if (baseName === "overviewStatistics") baseName = "Overview Statistics";
+ const btnId = `overview-${sectionId}Nav`;
+ let btn = document.getElementById(btnId);
+ if (!btn) {
+ btn = document.createElement("a");
+ btn.id = btnId;
+ btn.className = "nav-item nav-link";
+ const label = document.createElement("i");
+ label.textContent = baseName;
+ btn.appendChild(label);
+ insertAfter(btn, overviewMenuLink);
+
+ btn.addEventListener("click", (e) => {
+ e.preventDefault();
const stickyTop = document.getElementById("navigation");
const stickyHeight = stickyTop ? stickyTop.offsetHeight : 0;
- const targetTop = target.getBoundingClientRect().top + window.pageYOffset;
- const top = targetTop < 200 ? 0 : targetTop - stickyHeight - 8; // exception for the section at the top, scroll to 0
- window.scrollTo({
- top: top - 7,
- behavior: "auto"
- });
+
+ const performScroll = () => {
+ const targetTop = sectionEl.getBoundingClientRect().top + window.pageYOffset;
+ const top = targetTop < 200 ? 0 : targetTop - stickyHeight - 8;
+ window.scrollTo({ top: top - 7, behavior: "auto" });
+ };
+
+ // Expand the section if it is currently collapsed, then scroll
+ const collapseBtn = sectionEl.querySelector(".collapse-icon");
+ if (collapseBtn) {
+ const svg = collapseBtn.querySelector("svg");
+ const isExpanded = svg && (svg.classList.contains("lucide-chevron-down-icon") || svg.classList.contains("lucide-chevron-down"));
+ if (!isExpanded) {
+ // Trigger expansion and wait a tick for layout update
+ collapseBtn.click();
+ requestAnimationFrame(() => setTimeout(performScroll, 50));
+ return;
+ }
+ }
+ performScroll();
+ });
+ }
+ buttonMap.set(sectionId, btn);
+ };
+
+ sections.forEach(makeButtonForSection);
+
+ // Before attaching new listeners, remove any previous ones to avoid stale closures
+ if (__overviewNavStore.scrollHandler) {
+ window.removeEventListener("scroll", __overviewNavStore.scrollHandler);
+ __overviewNavStore.scrollHandler = null;
+ }
+ if (__overviewNavStore.resizeHandler) {
+ window.removeEventListener("resize", __overviewNavStore.resizeHandler);
+ __overviewNavStore.resizeHandler = null;
+ }
+
+ const updateVisibleButtons = () => {
+ // If not on overview anymore, cleanup and stop
+ const stillOverview = !!(settings.menu && settings.menu.overview);
+ const showButtons = stillOverview;
+ if (!stillOverview) {
+ const toRemove = Array.from(navbar.querySelectorAll('a[id^="overview-"][id$="Nav"]'));
+ toRemove.forEach(el => el.remove());
+ if (__overviewNavStore.scrollHandler) {
+ window.removeEventListener("scroll", __overviewNavStore.scrollHandler);
+ __overviewNavStore.scrollHandler = null;
+ }
+ if (__overviewNavStore.resizeHandler) {
+ window.removeEventListener("resize", __overviewNavStore.resizeHandler);
+ __overviewNavStore.resizeHandler = null;
}
+ return;
+ }
+ // Determine most visible section and neighboring indices
+ const bestIndex = compute_best_visible_index(sections);
+ const indices = neighbor_indices(bestIndex, sections.length);
+
+ // Desired left-to-right order: highest (above) on the left, then current, then below
+ const desiredOrder = indices.slice(); // already in [best-1, best, best+1] or edges
+
+ // Reorder buttons in the navbar immediately after the Overview link
+ let last = overviewMenuLink;
+ desiredOrder.forEach(idx => {
+ const section = sections[idx];
+ const btn = buttonMap.get(section.id);
+ if (!btn) return;
+ // Ensure visibility before positioning
+ btn.hidden = !showButtons ? true : false;
+ // Move button to desired position
+ if (last.nextSibling === btn) {
+ // Already in place
+ } else {
+ insertAfter(btn, last);
+ }
+ last = btn;
});
- });
+
+ // Update active state and hide non-selected buttons
+ sections.forEach((section, idx) => {
+ const btn = buttonMap.get(section.id);
+ if (!btn) return;
+ const shouldShow = showButtons && desiredOrder.includes(idx);
+ btn.hidden = !shouldShow;
+ btn.classList.toggle("active", showButtons && idx === bestIndex);
+ });
+ };
+
+ window.addEventListener("scroll", updateVisibleButtons, { passive: true });
+ window.addEventListener("resize", updateVisibleButtons);
+ __overviewNavStore.scrollHandler = updateVisibleButtons;
+ __overviewNavStore.resizeHandler = updateVisibleButtons;
+ updateVisibleButtons();
}
function get_most_visible_section() {
@@ -129,9 +330,8 @@ 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";
});
+ document.getElementById("filters").hidden = (item === "menuOverview");
setup_data_and_graphs(true, item === "menuOverview" && !areGroupedProjectsPrepared);
}
@@ -189,6 +389,9 @@ function setup_data_and_graphs(menuUpdate = false, prepareOverviewProjectData =
requestAnimationFrame(() => {
if (prepareOverviewProjectData) {
prepare_overview();
+ setup_overview_localstorage();
+ setup_overview_section_layout_buttons();
+ setup_overview_order_filters();
update_overview_version_select_list();
update_overview_statistics_heading();
}
@@ -203,7 +406,8 @@ function setup_data_and_graphs(menuUpdate = false, prepareOverviewProjectData =
// then load the graphs
requestAnimationFrame(() => {
setup_spinner(true);
- setup_section_menu_buttons(); // sections have to be visible to update highlighting correctly
+ setup_dashboard_section_menu_buttons();
+ setup_overview_section_menu_buttons();
setup_dashboard_graphs();
document.dispatchEvent(new Event("graphs-finalized"));
@@ -231,14 +435,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()
@@ -251,5 +453,6 @@ export {
setup_menu,
setup_data_and_graphs,
setup_spinner,
- update_menu
+ update_menu,
+ setup_overview_section_menu_buttons
};
\ No newline at end of file
diff --git a/robotframework_dashboard/js/variables/graphs.js b/robotframework_dashboard/js/variables/graphs.js
index e0689a7..cb4a8c3 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 ec992ea..e644845 100644
--- a/robotframework_dashboard/templates/dashboard.html
+++ b/robotframework_dashboard/templates/dashboard.html
@@ -90,7 +90,8 @@
-