diff --git a/docs/filtering.md b/docs/filtering.md index c138297b..9aeeaa33 100644 --- a/docs/filtering.md +++ b/docs/filtering.md @@ -10,12 +10,13 @@ RobotFramework Dashboard provides flexible filtering options across different pa ## Overview Page -The **Overview** page does not have global filters. However, it offers a few **display settings**: +The **Overview** page uses the same **global filters** as the Dashboard page (see below): open the filter modal from the top navigation bar and every project bar, the Latest Runs bar and the Total Stats bar reflect the filtered set of runs. A dot on the filter icon indicates a filter is active. -- **Display By Name** – Toggle whether to display the names of projects in the statistics. -- **Display By Tag** – Toggle whether to use custom project tags if defined in your test run metadata. (See [Advanced CLI & Examples](advanced-cli-examples.md#project-tagging) for more information on Tags!) -- **Duration Percentage Threshold** – Adjust the percentage threshold used to color-code durations (faster/slower runs). -- **Select Versions** – Filter the displayed runs by their associated versions. +In addition, the Overview offers a few **display settings** under **Settings > Overview**: + +- **Projects by Name** – Toggle whether to display the names of projects in the statistics. +- **Projects by Tag** – Toggle whether to use custom project tags if defined in your test run metadata. (See [Advanced CLI & Examples](advanced-cli-examples.md#project-tagging) for more information on Tags!) +- **Duration comparison percentage** – Adjust the percentage threshold used to color-code durations (faster/slower runs). > These settings affect only the way the statistics are presented on the Overview page. @@ -52,7 +53,8 @@ Global filters are applied to the entire dashboard, affecting all sections and g - **All** (ticked by default) means no version filter is applied. - **None** covers runs that have no version label set. - A dot next to the label indicates the filter is active. -- Use the search box inside the dropdown to quickly find a version by name. +- Typing in the search box inside the dropdown selects every matching version and unselects the rest (e.g. `1.` selects all `1.x` versions in one go). +- Click the **X** in the search box to clear it while keeping the current selection. Deleting characters one by one instead re-runs the matching on every keystroke. #### 4. From Date / From Time @@ -214,7 +216,7 @@ The **Tables** page allows for detailed inspection of raw test data and uses the | Page | Filter support | |------|---------------| -| **Overview** | Display-only settings (name, tag, duration threshold, versions) | +| **Overview** | Same global filters as Dashboard + display settings (name, tag, duration threshold) | | **Dashboard** | Full global filters + section-specific filters + Filter Profiles | | **Compare** | Run selection dropdowns + suite paths toggle | | **Tables** | Same global filters as Dashboard | diff --git a/docs/graphs-tables.md b/docs/graphs-tables.md index f42a2c66..8806c118 100644 --- a/docs/graphs-tables.md +++ b/docs/graphs-tables.md @@ -32,8 +32,7 @@ The Overview page supports several display toggles (configured in [Settings - Ov | **Projects by Name** | Groups runs by their project name. | | **Projects by Tag** | Groups runs by `project_` tags instead of name. See [Project Tagging](/advanced-cli-examples#project-tagging). | | **Prefixes** | Shows or hides the `project_` prefix text on tag-based names. | -| **Percentage Filters** | Enables the duration percentage threshold control for color-coding. | -| **Version Filters** | Enables per-project version filtering with checkbox selectors. | +| **Duration comparison percentage** | Threshold used to color-code run durations (faster/slower than average). | | **Sort Filters** | Enables sort controls on the Overview page. | ## Graph Switch Persistence diff --git a/docs/settings.md b/docs/settings.md index 014f6776..658c6ea9 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -95,7 +95,7 @@ This allows you to include or exclude specific libraries based on your dashboard ## Overview Settings (Overview Tab) -The **Overview** tab controls which sections and filters are visible on the Overview page. These toggles let you tailor the Overview layout to your needs. +The **Overview** tab controls which sections are visible on the Overview page and how projects are grouped and color-coded. These toggles let you tailor the Overview layout to your needs. ### Details @@ -106,8 +106,7 @@ The **Overview** tab controls which sections and filters are visible on the Over | **Projects by Name** | On | Group and display projects by their run name on the Overview. | | **Projects by Tag** | Off | Group and display projects by custom `project_` tags. See [Project Tagging](/advanced-cli-examples#project-tagging). | | **Display Prefixes** | On | Show the `project_` prefix text on tag-based project names. | -| **Percentage Filters** | On | Show the duration percentage threshold filter for color-coding run durations. | -| **Version Filters** | On | Show the version filter allowing per-project version selection. | +| **Duration comparison percentage** | 20% | Threshold for color-coding run durations on all project bars: green if a run is at least X% faster than average, red if X% slower. | | **Sort Filters** | On | Show the sort filter controls on the Overview. | ### Saving Overview Settings diff --git a/robotframework_dashboard/css/components.css b/robotframework_dashboard/css/components.css index ddec1228..4106097b 100644 --- a/robotframework_dashboard/css/components.css +++ b/robotframework_dashboard/css/components.css @@ -115,6 +115,37 @@ display: block; } +.filter-search-wrapper { + position: relative; +} + +.filter-search-wrapper .form-control { + padding-right: 1.5rem; +} + +.filter-search-clear { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0 0.15rem 0; + border: none; + background: none; + color: inherit; + opacity: 0.6; + font-size: 1.4rem; + line-height: 1; + cursor: pointer; +} + +.filter-search-clear:hover { + opacity: 1; +} + .filter-profile-bar { border-bottom: 1px solid var(--bs-border-color); } diff --git a/robotframework_dashboard/js/eventlisteners.js b/robotframework_dashboard/js/eventlisteners.js index 5664b6c4..e6c3e87e 100644 --- a/robotframework_dashboard/js/eventlisteners.js +++ b/robotframework_dashboard/js/eventlisteners.js @@ -28,7 +28,6 @@ import { setup_project_versions_in_select_filter_buttons, setup_suite_path_navigator, setup_custom_filters_in_select_filter_buttons, - update_overview_version_select_list, setup_metadata_filter, build_profile_from_checks, apply_filter_profile, @@ -59,6 +58,7 @@ import { set_filter_show_current_project, set_filter_show_current_version, update_overview_filter_visibility, + update_duration_comparison_for_all_projects, } from "./graph_creation/overview.js"; import { update_run_donut_total_graph, update_run_heatmap_graph } from "./graph_creation/run.js"; import { @@ -158,6 +158,14 @@ function update_merge_result_preview() { // ---- end of merge profiles helpers ---- +function update_filters_button_indicator() { + const indicator = document.getElementById("filtersActiveIndicator"); + if (!indicator) return; + const anyActive = [...document.querySelectorAll("#filtersModal .version-selected-dot")] + .some(el => el.style.display !== "none"); + indicator.style.display = anyActive ? "inline-block" : "none"; +} + // function to setup filter modal eventlisteners function setup_filter_modal() { // eventlistener to catch the closing of the filter modal @@ -317,6 +325,7 @@ function setup_filter_modal() { add_alert(`Filter profile "${name}" applied`, "success"); update_profile_select_display(); populate_filter_profile_select(); + update_filters_button_indicator(); } } }); @@ -402,13 +411,21 @@ function setup_filter_modal() { add_alert(`Filter profile "${newName}" saved!`, "success"); bootstrap.Modal.getInstance(document.getElementById("mergeProfilesModal")).hide(); }); - // Listen for filter changes to update the profile display + // Show indicator dot when a specific run is selected in the dropdown + document.getElementById("runs").addEventListener("change", function () { + const indicator = document.getElementById("filterRunSelectedIndicator"); + if (indicator) indicator.style.display = this.value !== "All" ? "inline-block" : "none"; + update_filters_button_indicator(); + }); + // Listen for filter changes to update the profile display and the navbar dot const filterModal = document.getElementById("filtersModal"); filterModal.addEventListener("change", function () { update_profile_select_display(); + update_filters_button_indicator(); }); filterModal.addEventListener("input", function () { update_profile_select_display(); + update_filters_button_indicator(); }); } @@ -500,11 +517,15 @@ function setup_settings_modal() { { key: "show.convertTimezone", elementId: "toggleTimezone" }, { key: "show.suitesSelectionInSuiteStats", elementId: "toggleSuitesSelectionInSuiteStats", datatype: "string", event: "change" }, { key: "show.suitesSelectionInTestStats", elementId: "toggleSuitesSelectionInTestStats", datatype: "string", event: "change" }, + { key: "show.overviewDurationPercentage", elementId: "overviewDurationPercentage", datatype: "number", event: "change" }, ].forEach(def => { const handler = create_toggle_handler(def); handler(true); document.getElementById(def.elementId).addEventListener(def.event || "click", () => handler()); }); + document.getElementById("overviewDurationPercentage").addEventListener("change", () => { + if (settings.menu.overview) update_duration_comparison_for_all_projects(); + }); // Re-populate the date filter pickers when either timezone toggle changes, // since the displayed timestamps change and the defaults need to match. document.getElementById("toggleTimezone").addEventListener("click", () => setup_lowest_highest_dates()); @@ -757,8 +778,6 @@ function setup_sections_filters() { update_switch_local_storage("switch.runName", settings.switch.runName, true); update_switch_local_storage("switch.totalStats", settings.switch.totalStats, true); update_switch_local_storage("switch.latestRuns", settings.switch.latestRuns, true); - update_switch_local_storage("switch.percentageFilters", settings.switch.percentageFilters, true); - update_switch_local_storage("switch.versionFilters", settings.switch.versionFilters, true); update_switch_local_storage("switch.sortFilters", settings.switch.sortFilters, true); document.getElementById("switchRunTags").addEventListener("click", function () { settings.switch.runTags = !settings.switch.runTags @@ -773,7 +792,6 @@ function setup_sections_filters() { update_overview_total_heading(); update_overview_sections_visibility(); // update all tagged bars - update_overview_version_select_list(); update_projectbar_visibility(); hide_loading_overlay(); }); @@ -792,7 +810,6 @@ function setup_sections_filters() { update_overview_total_heading(); update_overview_sections_visibility(); // update all named project bars - update_overview_version_select_list(); update_projectbar_visibility(); hide_loading_overlay(); }); @@ -808,21 +825,11 @@ function setup_sections_filters() { update_switch_local_storage("switch.totalStats", settings.switch.totalStats); update_overview_sections_visibility(); }); - document.getElementById("switchPercentageFilters").addEventListener("click", function () { - settings.switch.percentageFilters = !settings.switch.percentageFilters - update_switch_local_storage("switch.percentageFilters", settings.switch.percentageFilters); - update_overview_filter_visibility(); - }); document.getElementById("switchSortFilters").addEventListener("click", function () { settings.switch.sortFilters = !settings.switch.sortFilters update_switch_local_storage("switch.sortFilters", settings.switch.sortFilters); update_overview_filter_visibility(); }); - document.getElementById("switchVersionFilters").addEventListener("click", function () { - settings.switch.versionFilters = !settings.switch.versionFilters - update_switch_local_storage("switch.versionFilters", settings.switch.versionFilters); - update_overview_filter_visibility(); - }); document.getElementById("suiteSelectSuites").addEventListener("change", () => { const mostGraphIds = settings.switch.sectionFiltersApplySuite ? ["suiteMostFailedGraph", "suiteMostTimeConsumingGraph"] diff --git a/robotframework_dashboard/js/filter.js b/robotframework_dashboard/js/filter.js index 79b1a102..f93c4c87 100644 --- a/robotframework_dashboard/js/filter.js +++ b/robotframework_dashboard/js/filter.js @@ -705,7 +705,7 @@ function setup_project_versions_in_select_filter_buttons() { allVersionsCheckBox.checked = true; const filterVersionSelectedIndicatorId = "filterVersionSelectedIndicator"; setup_filter_active_indicator(allVersionsCheckBox, filterVersionSelectedIndicatorId); - setup_filter_checkbox_subfilter("projectVersionCheckBoxes"); + setup_filter_checkbox_subfilter("projectVersionCheckBoxes", true); setup_filter_checkbox_handler_listeners(projectVersionList, allVersionsCheckBox, filterVersionSelectedIndicatorId); } @@ -903,69 +903,6 @@ function unselect_checkboxes(checkBoxesToUnselect) { } } -function handle_overview_latest_version_selection(overviewVersionSelectorList, latestRunByProject) { - show_loading_overlay(); - requestAnimationFrame(() => { - requestAnimationFrame(() => { - const selectedOptions = Array.from( - overviewVersionSelectorList.querySelectorAll("input:checked") - ).map(inputElement => inputElement.value); - if (selectedOptions.includes("All")) { - create_overview_latest_graphs(latestRunByProject); - } else { - const filteredLatestRunByProject = Object.fromEntries( - Object.entries(latestRunByProject) - .filter(([projectName, run]) => selectedOptions.includes(run.project_version ?? "None")) - ); - create_overview_latest_graphs(filteredLatestRunByProject); - } - update_overview_latest_heading(); - hide_loading_overlay(); - }); - }); -} - -// this function updates the version select list in the latest runs bar -function update_overview_version_select_list() { - const overviewLatestVersionSelectorList = document.getElementById("overviewLatestVersionSelectorList"); - if (overviewLatestVersionSelectorList) { - overviewLatestVersionSelectorList.innerHTML = ''; - if (settings.switch.runName || settings.switch.runTags) { - const filteredLatestRunByProject = {}; - settings.switch.runName && Object.assign(filteredLatestRunByProject, latestRunByProjectName); - settings.switch.runTags && Object.assign(filteredLatestRunByProject, latestRunByProjectTag); - const runAmountByVersion = {}; - for (const run of Object.values(filteredLatestRunByProject)) { - const projectVersion = run.project_version ?? "None"; - runAmountByVersion[projectVersion] ??= 0; - runAmountByVersion[projectVersion]++; - } - const allVersionAmountInFilter = Object.keys(runAmountByVersion).length; - const versionFilterListItemAllHtml = generate_version_filter_list_item_html("All", "overviewLatest", "checked", allVersionAmountInFilter, "version"); - const specificVersionFilterListItemHtml = Object.keys(runAmountByVersion) - .sort() - .reverse() - .map(version => { - return generate_version_filter_list_item_html( - version, - "overviewLatest", - "", //not checked - runAmountByVersion[version], - "run" - ) - }).join(''); - overviewLatestVersionSelectorList.innerHTML = versionFilterListItemAllHtml + specificVersionFilterListItemHtml; - const allCheckBox = document.getElementById("overviewLatestVersionFilterListItemAllInput"); - setup_filter_checkbox_handler_listeners( - overviewLatestVersionSelectorList, - allCheckBox, - "overviewLatestVersionSelectedIndicator", - () => { handle_overview_latest_version_selection(overviewLatestVersionSelectorList, filteredLatestRunByProject) } - ); - } - } -} - // for runTag/version filter popup and overview function setup_filter_checkbox_handler_listeners( checkBoxContainerElement, @@ -1002,39 +939,36 @@ function setup_filter_checkbox_handler_listeners( } } -function setup_filter_checkbox_subfilter(parentElementId) { +// autoSelectMatches: while the search box has text, check every matching checkbox and +// uncheck the rest (e.g. typing "1." selects every 1.x version in one go) instead of only +// narrowing which rows are visible. Clearing the search box leaves the selection as-is. +// An optional ".filter-search-clear" button next to the search box empties it in one step +// (unlike deleting characters one by one, which re-runs the auto-select on every keystroke). +function setup_filter_checkbox_subfilter(parentElementId, autoSelectMatches = false) { const container = document.getElementById(parentElementId); const searchBar = container.querySelector("input.form-control"); + const clearButton = container.querySelector(".filter-search-clear"); const checkBoxRows = container.querySelectorAll("li.list-group-item-action"); - searchBar.addEventListener("input", () => { + const apply_search = () => { const filterText = searchBar.value.toLowerCase(); + if (clearButton) clearButton.hidden = !filterText; checkBoxRows.forEach(row => { const checkbox = row.querySelector("input.form-check-input"); const rowValue = checkbox.value.toLowerCase(); - const shouldHide = !rowValue.includes(filterText); - row.classList.toggle("d-none", shouldHide); + const matches = rowValue.includes(filterText); + row.classList.toggle("d-none", !matches); + if (autoSelectMatches && filterText && checkbox.value !== "All" && checkbox.checked !== matches) { + checkbox.checked = matches; + checkbox.dispatchEvent(new Event('change', { bubbles: true })); + } }); - }); -} - -function generate_version_filter_list_item_html(version, projectName, checked, amount, amountType) { - function versionFilterListItemInput(version, id, checked) { - return `` }; - function versionFilterListItemLabel(forId, version, amount, amountType, pluralPostfix) { - return `` - }; - const listItemId = `${projectName}VersionFilterListItem`; - const listItemInputId = `${listItemId}${version}Input`; - const pluralPostfix = amount === 1 ? '' : 's'; - return ` -
  • -
    - ${versionFilterListItemInput(version, listItemInputId, checked)} - ${versionFilterListItemLabel(listItemInputId, version, amount, amountType, pluralPostfix)} -
    -
  • - ` + searchBar.addEventListener("input", apply_search); + clearButton?.addEventListener("click", () => { + searchBar.value = ""; + apply_search(); + searchBar.focus(); + }); } function clear_all_filters() { @@ -1045,6 +979,10 @@ function clear_all_filters() { document.getElementById("amount").value = filteredAmountDefault; document.getElementById("metadata").value = "All"; setup_lowest_highest_dates(); + const runsIndicator = document.getElementById("filterRunSelectedIndicator"); + if (runsIndicator) runsIndicator.style.display = "none"; + const filtersActiveIndicator = document.getElementById("filtersActiveIndicator"); + if (filtersActiveIndicator) filtersActiveIndicator.style.display = "none"; } function clear_suite_path_filter() { @@ -1070,6 +1008,7 @@ function clear_custom_filters() { function clear_version_filter() { document.getElementById("projectVersionCheckBoxesFilter").value = ""; + document.getElementById("projectVersionCheckBoxesFilterClear").hidden = true; const versionElements = document.getElementById("projectVersionList").getElementsByTagName("input"); for (const input of versionElements) { input.checked = false; @@ -1384,6 +1323,11 @@ function apply_filter_profile(profile, name) { if (modeEl) modeEl.value = mode; } } + const runsIndicator = document.getElementById("filterRunSelectedIndicator"); + const runsVal = document.getElementById("runs")?.value; + if (runsIndicator) + runsIndicator.style.display = + runsVal && runsVal !== "All" ? "inline-block" : "none"; } // Load filter profiles from settings (cumulative — never deletes existing ones) @@ -1609,10 +1553,8 @@ export { setup_suite_path_navigator, setup_custom_filters_in_select_filter_buttons, setup_filter_checkbox_handler_listeners, - update_overview_version_select_list, clear_all_filters, set_filter_show_current_version, - generate_version_filter_list_item_html, build_profile_from_checks, apply_filter_profile, save_filter_profile_to_storage, diff --git a/robotframework_dashboard/js/graph_creation/all.js b/robotframework_dashboard/js/graph_creation/all.js index 88297dbd..e0ab7c57 100644 --- a/robotframework_dashboard/js/graph_creation/all.js +++ b/robotframework_dashboard/js/graph_creation/all.js @@ -1,9 +1,10 @@ import { settings } from "../variables/settings.js"; -import { +import { create_overview_latest_graphs, create_overview_total_graphs, - update_donut_charts + update_donut_charts, + update_grouped_data_for_filter } from "./overview.js"; import { create_run_statistics_graph, @@ -102,6 +103,7 @@ import { update_custom_stat_widgets } from "../statwidgets.js"; // function that creates all graphs from scratch - used on first load of each tab function create_dashboard_graphs() { if (settings.menu.overview) { + update_grouped_data_for_filter(); create_overview_latest_graphs(); create_overview_total_graphs(); update_donut_charts(); @@ -156,6 +158,7 @@ function create_dashboard_graphs() { // each update function falls back to create if the chart doesn't exist yet function update_dashboard_graphs() { if (settings.menu.overview) { + update_grouped_data_for_filter(); create_overview_latest_graphs(); create_overview_total_graphs(); update_donut_charts(); diff --git a/robotframework_dashboard/js/graph_creation/overview.js b/robotframework_dashboard/js/graph_creation/overview.js index 7f5cea24..73bf1370 100644 --- a/robotframework_dashboard/js/graph_creation/overview.js +++ b/robotframework_dashboard/js/graph_creation/overview.js @@ -25,7 +25,6 @@ import { } from '../variables/chartconfig.js'; import { settings } from '../variables/settings.js'; import { - DEFAULT_DURATION_PERCENTAGE, projects_by_tag, projects_by_name, selectedRunSetting, @@ -33,19 +32,18 @@ import { versionsByProject, latestRunByProjectName, latestRunByProjectTag, - areGroupedProjectsPrepared + areGroupedProjectsPrepared, + filteredRuns, } from '../variables/globals.js'; import { runs, use_logs } from '../variables/data.js'; import { clear_all_filters, update_filter_active_indicator, - setup_filter_checkbox_handler_listeners, - generate_version_filter_list_item_html } from '../filter.js'; // Data prep/aggregation function prepare_projects_grouped_data() { - for (const run of runs) { + for (const run of filteredRuns) { const tags = run.tags.split(","); const project_tags = tags.filter(tag => tag.toLowerCase().startsWith("project_")); for (const project of project_tags) { @@ -62,11 +60,11 @@ function prepare_projects_grouped_data() { areGroupedProjectsPrepared = true; } -// versionByProject = {projectName:{version:amount}} +// versionsByProject = {projectName: {version: amount}} function prepare_projects_version_counts_map(projects) { - for (const [project, runs] of Object.entries(projects)) { + for (const [project, projectRuns] of Object.entries(projects)) { const versionCounts = {}; - for (const run of runs) { + for (const run of projectRuns) { const projectVersion = run.project_version ?? "None"; versionCounts[projectVersion] = (versionCounts[projectVersion] || 0) + 1; } @@ -157,12 +155,15 @@ function generate_overview_card_html( compares = ''; } // for project bars - const versionsForProject = Object.keys(versionsByProject[projectName]); - const projectHasVersions = !(versionsForProject.length === 1 && versionsForProject[0] === "None"); + const runsForProject = projects_by_name[projectName] ?? projects_by_tag[projectName] ?? []; + const projectHasVersions = runsForProject.some(r => r.project_version != null && r.project_version !== "None"); // for overview statistics // Preserve the original project name (used for logic like tag-detection), // but compute a display name that omits the 'project_' prefix when prefixes are hidden. const originalProjectName = projectName; + const projectEverHasVersions = runs + .filter(r => r.name === originalProjectName) + .some(r => r.project_version != null && r.project_version !== "None"); const displayProjectName = settings.show.prefixes ? projectName : projectName.replace(/^project_/, ''); projectName = displayProjectName; let cardTitle = ` @@ -175,7 +176,7 @@ function generate_overview_card_html( cardTitle = `
    ${stats[5]}, Version: ${normalizedProjectVersion}
    `; - } else if (projectHasVersions) { + } else if (projectHasVersions || projectEverHasVersions) { // Non-tagged projects with versions: interactive version title cardTitle = `
    `; } -function apply_overview_latest_version_text_filter() { - const versionFilterInput = document.getElementById("overviewLatestVersionFilterSearch"); - const cardsContainer = document.getElementById("overviewLatestRunCardsContainer"); - if (!versionFilterInput || !cardsContainer) return; - const filterValue = versionFilterInput.value.toLowerCase(); - const runCards = Array.from(cardsContainer.querySelectorAll("div.overview-card")); - runCards.forEach(card => { - const version = (card.dataset.projectVersion ?? "").toLowerCase(); - card.style.display = version.includes(filterValue) ? "" : "none"; - }); -} - function clear_project_filter() { document.getElementById("runs").value = "All"; document.getElementById("runTagCheckBoxesFilter").value = ""; @@ -294,45 +283,8 @@ function _update_overview_heading(containerId, titleId, titleText) { if (subTitleEl) subTitleEl.innerHTML = `showing ${amountOfProjectsShown} project${pluralPostFix}`; } -// create overview latest runs section dynamically function create_overview_latest_runs_section() { - const percentageSelectHtml = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map(val => - `` - ).join(''); - const filtersHtml = ` -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    @@ -356,30 +308,6 @@ function create_overview_latest_runs_section() { create_overview_latest_graphs(); update_overview_latest_heading(); - - // Setup event listeners for filters - const percentageSelector = document.getElementById("overviewLatestDurationPercentage"); - if (percentageSelector) { - percentageSelector.addEventListener('change', () => { - show_loading_overlay(); - requestAnimationFrame(() => { - requestAnimationFrame(() => { - create_overview_latest_graphs(); - hide_loading_overlay(); - }); - }); - }); - } - - const versionFilterSearch = document.getElementById("overviewLatestVersionFilterSearch"); - if (versionFilterSearch) { - const handle_version_filter_input = () => { - apply_overview_latest_version_text_filter(); - }; - const maxDelay = 50; - const delayScaledByRunAmount = Math.min(runs.length / 100, maxDelay); - versionFilterSearch.addEventListener('input', debounce(handle_version_filter_input, delayScaledByRunAmount)); - } } // create overview total stats section dynamically @@ -396,23 +324,6 @@ function create_overview_total_stats_section() { // create project bar (the collapsables below overview statistic) in overview function create_project_bar(projectName, projectRuns, totalRunsAmount, passRate) { - const projectVersions = new Set( - Object.keys(versionsByProject[projectName]) - .sort() - .reverse() - ); - const versionAmount = projectVersions.size; - const versionFilterListItemAllHtml = generate_version_filter_list_item_html("All", projectName, "checked", versionAmount, "version"); - const versionFilterListItemsHtml = versionFilterListItemAllHtml + - [...projectVersions] - .map(version => { - const runAmount = versionsByProject[projectName][version]; - return generate_version_filter_list_item_html(version, projectName, "", runAmount, "run"); - }) - .join(''); - const percentageSelectHtml = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map(val => - `` - ).join(''); // added here instead of adding to informationMap, since the dynamic IDs don't work with the map const displayProjectName = (!settings.show.prefixes && projectName.startsWith('project_')) ? projectName.replace(/^project_/, '') @@ -439,37 +350,6 @@ See Settings > Overview for more options.`;
    Total Runs: ${totalRunsAmount} | Passed Runs: ${passRate}%
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    @@ -498,40 +378,6 @@ See Settings > Overview for more options.`; `; const overview = document.getElementById("overview") overview.appendChild(document.createRange().createContextualFragment(projectCard)); - - // percentage selector - const projectPercentageSelector = document.getElementById(`${projectName}DurationPercentage`); - projectPercentageSelector.addEventListener('change', () => { - const newPercent = parseInt(projectPercentageSelector.value, 10); - update_duration_comparison_for_project(projectName, projectRuns, newPercent); - }); - - // version filters - const versionFilterDropDownId = `${projectName}VersionFilterDropDown`; - const versionFilterSearchId = `${projectName}VersionFilterSearch`; - const versionFilterArgs = { - cardsContainerId: `${projectName}RunCardsContainer`, - versionDropDownFilterId: versionFilterDropDownId, - versionStringFilterId: versionFilterSearchId, - }; - const projectVersionFilterDropDown = document.getElementById(versionFilterDropDownId); - const allVersionsCheckBox = document.getElementById(`${projectName}VersionFilterListItemAllInput`); - const specificVersionSelectedIndicatorId = `${projectName}VersionSelectedIndicator`; - setup_filter_checkbox_handler_listeners( - projectVersionFilterDropDown, - allVersionsCheckBox, - specificVersionSelectedIndicatorId, - () => { update_project_version_filter_run_card_visibility(versionFilterArgs) } - ); - - // version filter input - const projectVersionFilterSearch = document.getElementById(versionFilterSearchId); - const handle_version_filter_input = () => { - update_project_version_filter_run_card_visibility(versionFilterArgs); - } - const maxDelay = 50; - const delayScaledByRunAmount = Math.min(totalRunsAmount / 100, maxDelay); // ~0.01ms per run or 50ms max - projectVersionFilterSearch.addEventListener('input', debounce(handle_version_filter_input, delayScaledByRunAmount)); } function create_project_overview() { @@ -556,10 +402,8 @@ function create_project_cards_container(projectName, projectRuns, percent = null // create section for project in overview if not present if (!cardsContainer) create_project_bar(projectName, projectRuns, totalRunsAmount, passRate); - // Read percentage from selector if not provided if (percent === null) { - const percentageSelector = document.getElementById(`${projectName}DurationPercentage`); - percent = percentageSelector ? parseInt(percentageSelector.value, 10) : DEFAULT_DURATION_PERCENTAGE; + percent = settings.show.overviewDurationPercentage; } const container = document.getElementById(`${projectName}RunCardsContainer`); @@ -616,7 +460,7 @@ function create_overview_latest_graphs(preFilteredRuns = null) { Object.entries(latestRunByProject).sort(([, runA], [, runB]) => runB.passed - runA.passed) ); } - const percent = document.getElementById("overviewLatestDurationPercentage").value; + const percent = settings.show.overviewDurationPercentage; for (const [projectName, latestRun] of Object.entries(latestRunByProject)) { const projectRuns = allProjects[projectName]; const totalRunsAmount = projectRuns.length; @@ -637,7 +481,7 @@ function create_overview_latest_graphs(preFilteredRuns = null) { 'overviewLatest' ); } - apply_overview_latest_version_text_filter(); + update_overview_latest_heading(); } // function to create overview total statistics @@ -803,39 +647,6 @@ function create_overview_run_donut(run, chartElementPostfix, projectName) { el.chartInstance = new Chart(el, config); } - -// apply version select checkbox and version textinput filter -function update_project_version_filter_run_card_visibility({ cardsContainerId, versionDropDownFilterId, versionStringFilterId }) { - const cardsContainerElement = document.getElementById(cardsContainerId); - const scrollOffsetBefore = cardsContainerElement.getBoundingClientRect().top; - const versionDropDownFilter = document.getElementById(versionDropDownFilterId); - const dropDownCheckBoxes = versionDropDownFilter.querySelectorAll(".version-checkbox"); - const selectedVersions = Array.from(dropDownCheckBoxes) - .filter(checkBox => checkBox.checked) - .map(checkBox => checkBox.value); - const runCardNodeList = cardsContainerElement.querySelectorAll("div.overview-card"); - const runCardsArray = Array.from(runCardNodeList); - let dropDownFilteredRunCards = runCardsArray; - if (!selectedVersions.includes("All")) { - dropDownFilteredRunCards = runCardsArray.filter(runCard => - selectedVersions.includes(runCard.dataset.projectVersion) - ); - } - const versionStringFilter = document.getElementById(versionStringFilterId); - const lowerCaseVersionStringFilterValue = versionStringFilter.value.toLowerCase(); - const fullyFilteredRunCards = dropDownFilteredRunCards.filter(runCard => - runCard.dataset.projectVersion.toLowerCase() - .includes(lowerCaseVersionStringFilterValue) - ); - runCardsArray.forEach(runCard => { - runCard.style.display = "none"; - }); - fullyFilteredRunCards.forEach(runCard => { - runCard.style.display = ""; - }); - const scrollOffsetAfter = cardsContainerElement.getBoundingClientRect().top; - window.scrollBy(0, scrollOffsetAfter - scrollOffsetBefore); -} function update_overview_latest_heading() { _update_overview_heading("overviewLatestRunCardsContainer", "overviewLatestTitle", "Latest Runs"); } @@ -852,17 +663,7 @@ function update_overview_sections_visibility() { } function update_overview_filter_visibility() { - // Update filter visibility for all bars using classes - const percentageFilters = document.querySelectorAll(".percentage-filter"); - const versionFilters = document.querySelectorAll(".version-filter"); const sortFilters = document.querySelectorAll(".sort-filter"); - - percentageFilters.forEach(container => { - container.hidden = !settings.switch.percentageFilters; - }); - versionFilters.forEach(container => { - container.hidden = !settings.switch.versionFilters; - }); sortFilters.forEach(container => { container.hidden = !settings.switch.sortFilters; }); @@ -875,6 +676,45 @@ function update_donut_charts() { }); } +// Updates project bars in-place after a filter change — avoids recreating Chart.js canvases. +function update_grouped_data_for_filter() { + Object.keys(projects_by_tag).forEach(k => delete projects_by_tag[k]); + Object.keys(projects_by_name).forEach(k => delete projects_by_name[k]); + Object.keys(latestRunByProjectTag).forEach(k => delete latestRunByProjectTag[k]); + Object.keys(latestRunByProjectName).forEach(k => delete latestRunByProjectName[k]); + Object.keys(versionsByProject).forEach(k => delete versionsByProject[k]); + prepare_projects_grouped_data(); + prepare_latest_run_by_project(); + prepare_projects_version_counts_map({ ...projects_by_name, ...projects_by_tag }); + const projectData = { ...projects_by_name, ...projects_by_tag }; + document.querySelectorAll(".overview-project-card").forEach(bar => { + const projectName = bar.id.replace(/Section$/, ""); + const projectRuns = projectData[projectName]; + const isTagged = projectName.startsWith("project_"); + const settingsVisible = isTagged ? settings.switch.runTags : settings.switch.runName; + bar.hidden = !settingsVisible || !projectRuns; + if (projectRuns && settingsVisible) { + const subtitleEl = bar.querySelector(".card-header h6"); + if (subtitleEl) { + const totalRunsAmount = projectRuns.length; + const passedRunsAmount = projectRuns.filter(run => run.failed === 0).length; + const passRate = ((passedRunsAmount / totalRunsAmount) * 100).toFixed(2); + subtitleEl.textContent = `Total Runs: ${totalRunsAmount} | Passed Runs: ${passRate}%`; + } + create_project_cards_container(projectName, projectRuns); + } + }); +} + +function update_duration_comparison_for_all_projects() { + const percent = settings.show.overviewDurationPercentage; + document.querySelectorAll(".overview-project-card").forEach(bar => { + const projectName = bar.id.replace(/Section$/, ""); + const projectRuns = projects_by_name[projectName] ?? projects_by_tag[projectName]; + if (projectRuns) update_duration_comparison_for_project(projectName, projectRuns, percent); + }); +} + // hide project bars based on switch config function update_projectbar_visibility() { const bars = document.querySelectorAll('.overview-project-card'); @@ -970,5 +810,7 @@ export { update_overview_latest_heading, update_overview_total_heading, update_overview_sections_visibility, - update_overview_filter_visibility + update_overview_filter_visibility, + update_grouped_data_for_filter, + update_duration_comparison_for_all_projects, }; diff --git a/robotframework_dashboard/js/menu.js b/robotframework_dashboard/js/menu.js index 1a8d77b9..b02a391e 100644 --- a/robotframework_dashboard/js/menu.js +++ b/robotframework_dashboard/js/menu.js @@ -1,4 +1,4 @@ -import { setup_filtered_data_and_filters, update_overview_version_select_list } from "./filter.js"; +import { setup_filtered_data_and_filters } from "./filter.js"; import { areGroupedProjectsPrepared, overviewNavStore } from "./variables/globals.js"; import { space_to_camelcase } from "./common.js"; import { set_local_storage_item, setup_overview_localstorage } from "./localstorage.js"; @@ -80,7 +80,6 @@ function update_menu(item) { set_local_storage_item(`menu.${menuItem}`, (item === id)); document.getElementById(id).classList.toggle("active", id === item); }); - document.getElementById("filters").hidden = (item === "menuOverview"); setup_data_and_graphs(true, item === "menuOverview" && !areGroupedProjectsPrepared); } @@ -138,14 +137,13 @@ function setup_data_and_graphs(menuUpdate = false, prepareOverviewProjectData = setup_spinner(false); // show spinner immediately requestAnimationFrame(() => { requestAnimationFrame(() => { + setup_filtered_data_and_filters(); if (prepareOverviewProjectData) { prepare_overview(); setup_overview_localstorage(); setup_overview_section_layout_buttons(); setup_overview_order_filters(); - update_overview_version_select_list(); } - setup_filtered_data_and_filters(); setup_section_order(); setup_graph_order(); setup_information_popups(); diff --git a/robotframework_dashboard/js/variables/globals.js b/robotframework_dashboard/js/variables/globals.js index 004ad7eb..30951d23 100644 --- a/robotframework_dashboard/js/variables/globals.js +++ b/robotframework_dashboard/js/variables/globals.js @@ -1,6 +1,5 @@ // UI defaults const CARDS_PER_ROW = 3; -const DEFAULT_DURATION_PERCENTAGE = 20; // populated by prepare_overview() const projects_by_tag = {}; @@ -147,7 +146,6 @@ var defaultFaviconHref = (() => { export { CARDS_PER_ROW, - DEFAULT_DURATION_PERCENTAGE, projects_by_tag, projects_by_name, latestRunByProjectTag, diff --git a/robotframework_dashboard/js/variables/information.js b/robotframework_dashboard/js/variables/information.js index 800c1c09..70abe162 100644 --- a/robotframework_dashboard/js/variables/information.js +++ b/robotframework_dashboard/js/variables/information.js @@ -21,8 +21,6 @@ const informationMap = { - Average Pass Rate: mean pass rate across all runs See Settings > Overview for display options.`, "overviewLatestInformation": "Shows the latest run per project. Click a card to apply a project filter and open the dashboard.", - "overviewLatestPercentageInfo": "Duration color threshold: green if the run is at least X% faster than average, red if X% slower.", - "overviewLatestVersionsInfo": "Filter overview cards by project version. 'All' shows all versions.", "overviewLatestSortInfo": "Sort project cards by: Most Recent, Oldest, Most Failed, Most Skipped, or Most Passed.", "unifiedStatisticsInformation": `Unified view combining data from all projects into one dashboard. - Use the top filters to focus on specific projects, versions, or timeframes. @@ -149,6 +147,8 @@ It helps identify tests with inconsistent execution times, which might be flaky - A dot indicates the filter is active.`, "filterVersionsInformation": `Filter by project version. 'All' disables the filter. - 'None' covers runs without a version label. +- Typing in the search box selects every matching version (e.g. "1." selects all 1.x). +- The X in the search box clears it while keeping the current selection. - A dot indicates the filter is active.`, "filterFromDateInformation": "Show only runs that started on or after this date.", "filterFromTimeInformation": "Show only runs that started at or after this time (combined with From Date).", @@ -186,8 +186,7 @@ Tip: avoid using Status and Only Changes together — the result will be empty.` "settingProjectsByName": "Group and display projects on the Overview by their Robot Framework run name.", "settingProjectsByTag": "Group and display projects on the Overview by custom project_ tags. See the docs for project tagging.", "settingPrefixes": "Show or hide the 'project_' prefix on tag-based project names on the Overview.", - "settingPercentageFilters": "Show the duration percentage threshold filter used to color-code run durations on the Overview.", - "settingVersionFilters": "Show the version filter for per-project version selection on the Overview.", + "settingOverviewDurationPercentage": "Threshold for duration color comparison on all project bars. A run is green if at least X% faster than average, red if X% slower.", "settingSortFilters": "Show the sort controls for ordering Overview project bars.", "settingBackgroundColor": "Main page background color for the current theme.", "settingCardColor": "Background color for graph cards and content panels.", diff --git a/robotframework_dashboard/js/variables/settings.js b/robotframework_dashboard/js/variables/settings.js index f8a28448..d865ed3f 100644 --- a/robotframework_dashboard/js/variables/settings.js +++ b/robotframework_dashboard/js/variables/settings.js @@ -18,8 +18,6 @@ var settings = { runName: true, totalStats: true, latestRuns: true, - percentageFilters: true, - versionFilters: true, sortFilters: true, suitePathsSuiteSection: false, suitePathsTestSection: false, @@ -43,6 +41,7 @@ var settings = { compareStatusFilter: "All", }, show: { + overviewDurationPercentage: 20, unified: false, dateLabels: true, legends: true, diff --git a/robotframework_dashboard/templates/dashboard.html b/robotframework_dashboard/templates/dashboard.html index f360f58c..1210a8d9 100644 --- a/robotframework_dashboard/templates/dashboard.html +++ b/robotframework_dashboard/templates/dashboard.html @@ -56,8 +56,10 @@
    - Display Percentage Filters -
    - -
    -
    -
    - Display Version Filters -
    - -
    + Duration comparison percentage +
    Display Sort Filters @@ -1253,4 +1264,4 @@ - \ No newline at end of file + diff --git a/tests/robot/resources/dashboard_output/overview/latestRuns.png b/tests/robot/resources/dashboard_output/overview/latestRuns.png index 020619cf..b6b74711 100644 Binary files a/tests/robot/resources/dashboard_output/overview/latestRuns.png and b/tests/robot/resources/dashboard_output/overview/latestRuns.png differ diff --git a/tests/robot/resources/dashboard_output/overview/latestRunsRunTags.png b/tests/robot/resources/dashboard_output/overview/latestRunsRunTags.png index 27579771..64f1e31c 100644 Binary files a/tests/robot/resources/dashboard_output/overview/latestRunsRunTags.png and b/tests/robot/resources/dashboard_output/overview/latestRunsRunTags.png differ diff --git a/tests/robot/resources/dashboard_output/overview/prjOther.png b/tests/robot/resources/dashboard_output/overview/prjOther.png index fe3c1364..2d1b195b 100644 Binary files a/tests/robot/resources/dashboard_output/overview/prjOther.png and b/tests/robot/resources/dashboard_output/overview/prjOther.png differ diff --git a/tests/robot/resources/dashboard_output/overview/prjOtherV1_1.png b/tests/robot/resources/dashboard_output/overview/prjOtherV1_1.png deleted file mode 100644 index a0881cab..00000000 Binary files a/tests/robot/resources/dashboard_output/overview/prjOtherV1_1.png and /dev/null differ diff --git a/tests/robot/resources/dashboard_output/overview/prjTests.png b/tests/robot/resources/dashboard_output/overview/prjTests.png index 7b0e2926..dedfaae3 100644 Binary files a/tests/robot/resources/dashboard_output/overview/prjTests.png and b/tests/robot/resources/dashboard_output/overview/prjTests.png differ diff --git a/tests/robot/resources/dashboard_output/overview/prjTestsV1_1.png b/tests/robot/resources/dashboard_output/overview/prjTestsV1_1.png deleted file mode 100644 index 3ca5aa73..00000000 Binary files a/tests/robot/resources/dashboard_output/overview/prjTestsV1_1.png and /dev/null differ diff --git a/tests/robot/resources/dashboard_output/overview/prjTestsuites.png b/tests/robot/resources/dashboard_output/overview/prjTestsuites.png index fe3c1364..2d1b195b 100644 Binary files a/tests/robot/resources/dashboard_output/overview/prjTestsuites.png and b/tests/robot/resources/dashboard_output/overview/prjTestsuites.png differ diff --git a/tests/robot/resources/dashboard_output/overview/prjTestsuitesV1_1.png b/tests/robot/resources/dashboard_output/overview/prjTestsuitesV1_1.png deleted file mode 100644 index 71087786..00000000 Binary files a/tests/robot/resources/dashboard_output/overview/prjTestsuitesV1_1.png and /dev/null differ diff --git a/tests/robot/testsuites/02_overview.robot b/tests/robot/testsuites/02_overview.robot index cc61f678..10b4a7c4 100644 --- a/tests/robot/testsuites/02_overview.robot +++ b/tests/robot/testsuites/02_overview.robot @@ -43,30 +43,12 @@ Validate Project Tests Click selector=id=collapseTestsBody Validate Component id=TestsSection name=prjTests folder=overview -Validate Project Tests With Version Filter - Open Overview Page - Click selector=id=collapseTestsBody - Fill Text selector=id=TestsVersionFilterSearch txt=1.1 - Validate Component id=TestsSection name=prjTestsV1_1 folder=overview - Validate Project Testsuites Open Overview Page Click selector=id=collapseTestsuitesBody Validate Component id=TestsuitesSection name=prjTestsuites folder=overview -Validate Project Testsuites With Version Filter - Open Overview Page - Click selector=id=collapseTestsuitesBody - Fill Text selector=id=TestsuitesVersionFilterSearch txt=1.1 - Validate Component id=TestsuitesSection name=prjTestsuitesV1_1 folder=overview - Validate Project Other Open Overview Page Click selector=id=collapseTestsuitesBody Validate Component id=TestsuitesSection name=prjOther folder=overview - -Validate Project Other With Version Filter - Open Overview Page - Click selector=id=collapseTestsuitesBody - Fill Text selector=id=TestsuitesVersionFilterSearch txt=1.1 - Validate Component id=TestsuitesSection name=prjOtherV1_1 folder=overview diff --git a/tests/robot/testsuites/06_filters.robot b/tests/robot/testsuites/06_filters.robot index f99a1c98..1c1c0ab2 100644 --- a/tests/robot/testsuites/06_filters.robot +++ b/tests/robot/testsuites/06_filters.robot @@ -66,6 +66,45 @@ Add Filter Profile With Versions Filter Add Filter Profile PrfVersions For Versions Filter Profile PrfVersions Should Be {'projectVersions': [{'value': 'All', 'checked': False}, {'value': 'None', 'checked': True}, {'value': '1.2', 'checked': False}, {'value': '1.1', 'checked': False}, {'value': '1.0', 'checked': False}]} +Versions Filter Search Selects Matching Versions + Open Filter Dialog + Click selector=id=selectProjectVersion + Fill Text selector=id=projectVersionCheckBoxesFilter txt=1. + ${state10} Get Checkbox State selector=id=projectVersionInputItem1.0 + ${state11} Get Checkbox State selector=id=projectVersionInputItem1.1 + ${state12} Get Checkbox State selector=id=projectVersionInputItem1.2 + ${stateAll} Get Checkbox State selector=id=projectVersionInputItemAll + ${stateNone} Get Checkbox State selector=id=projectVersionInputItemNone + Should Be True ${state10} + Should Be True ${state11} + Should Be True ${state12} + Should Not Be True ${stateAll} + Should Not Be True ${stateNone} + Close Filter Dialog + +Versions Filter Search Clear Button Keeps Selection + Open Filter Dialog + Click selector=id=selectProjectVersion + Wait For Elements State selector=id=projectVersionCheckBoxesFilterClear state=hidden + Fill Text selector=id=projectVersionCheckBoxesFilter txt=1. + Wait For Elements State selector=id=projectVersionCheckBoxesFilterClear state=visible + Wait For Elements State selector=id=projectVersionInputItemNone state=hidden + Click selector=id=projectVersionCheckBoxesFilterClear + Wait For Elements State selector=id=projectVersionCheckBoxesFilterClear state=hidden + Wait For Elements State selector=id=projectVersionInputItemNone state=visible + Get Property selector=id=projectVersionCheckBoxesFilter property=value assertion_operator=== assertion_expected=${EMPTY} + ${state10} Get Checkbox State selector=id=projectVersionInputItem1.0 + ${state11} Get Checkbox State selector=id=projectVersionInputItem1.1 + ${state12} Get Checkbox State selector=id=projectVersionInputItem1.2 + ${stateAll} Get Checkbox State selector=id=projectVersionInputItemAll + ${stateNone} Get Checkbox State selector=id=projectVersionInputItemNone + Should Be True ${state10} + Should Be True ${state11} + Should Be True ${state12} + Should Not Be True ${stateAll} + Should Not Be True ${stateNone} + Close Filter Dialog + Add Filter Profile With Date Filters Set Date Filter fromDate=03132025 fromTime=1225am toDate=04012025 toTime=1159pm