From b0d5139d64b9f1caa5ecc7a314dd11bfd57e4b9c Mon Sep 17 00:00:00 2001 From: Saif Date: Sat, 13 Jul 2024 19:16:02 +0100 Subject: [PATCH 1/3] Add branch filter system to project pages - shows a list of branches on the left and right side - allows to search for branches - allows to sort branches by most used, most recent, or alphabetically Signed-off-by: Stefan Marr --- resources/style.css | 131 +++++++++++++++++++++++++++ src/backend/project/project.html | 26 +++++- src/frontend/render.ts | 149 ++++++++++++++++++++++++++++++- 3 files changed, 304 insertions(+), 2 deletions(-) diff --git a/resources/style.css b/resources/style.css index 9c0f0340..51e9056e 100644 --- a/resources/style.css +++ b/resources/style.css @@ -3,6 +3,11 @@ body { margin: 0 auto; } +body.project-body { + max-width: 1200px; + margin: 0 auto; +} + body.compare, body.timeline { max-width: initial; } @@ -25,6 +30,114 @@ body.compare, body.timeline { margin-left: -10px; } +@media (min-width: 576px) { + .container { + max-width: 100%; + } +} + +@media (min-width: 800px) { + #project { + display: grid; + grid-template-columns: auto 800px auto; + } +} + +@media (max-width: 800px) { + .branch-filter-sidebar { + /* hidden */ + display: none; + } +} + +@media (max-width: 576px) { + #project { + display: block; + } +} + + +#branch-search { + width: 100%; + max-width: 100%; + padding: 8px; + border-radius: 4px; + border: 1px solid #ccc; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + transition: border-color 0.3s ease; +} + +#branch-search:focus { + border-color: #007bff; + outline: none; +} + +.branch-filter-sidebar.left { + text-align: right; +} + +.branch-filter-sidebar { + border: 1px solid #ddd; + border-radius: 8px; + background-color: #fff; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 200px; + max-width: 200px; + margin-top: 1em; + font-size: 75%; +} + +.filter-header { + margin-bottom: 0.5em; + padding: 0.5em; +} + +.filter-options { + background-color: #f9f9f9; + border-radius: 4px; + margin-bottom: 1em; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + padding: 0.5em; +} + +.filter-options a { + display: block; + color: #007bff; + text-decoration: none; + font-size: 0.9em; + padding: 4px 7px; + transition: background-color 0.3s, color 0.3s; +} + +.filter-options a:hover, +.filter-options a.selected-text { + background-color: #007bff; + color: white; +} + +.branch-cards-container { + max-height: 800px; + overflow-y: auto; + overflow-x: hidden; +} + +.branch-cards-container .list-group-item { + cursor: pointer; + padding: 2px 7px; + border: none; + color: #444444; +} + +.branch-cards-container .list-group-item:hover { + background-color: #f1f1f1; +} + +.branch-cards-container .list-group-item.active { + background-color: #007bff; + color: white; +} + nav.compare { position: -webkit-sticky; position: sticky; @@ -559,3 +672,21 @@ td.warmup-plot { table#benchmark-set-change tbody.hide-most-rows tr.benchmark-row:nth-child(n+4) { display: none; } + +.link-container { + width: 100%; + text-align: center; +} +.link-container a { + display: block; + margin: 10px 0; +} +.main-content { + width: 100%; +} +.left-side, .right-side { + width: 15%; +} +.content-area { + width: 70%; +} diff --git a/src/backend/project/project.html b/src/backend/project/project.html index 3f598004..53bb2b01 100644 --- a/src/backend/project/project.html +++ b/src/backend/project/project.html @@ -8,7 +8,7 @@ {%- include('header.html') %} - +

{%= it.name %}

@@ -18,6 +18,18 @@

{%= it.description%}

+
+
+ +
+ +
+
+
{%= it.name %}
@@ -43,6 +55,18 @@
Changes
Timeline
+
+
+ +
+ +
+
+
diff --git a/src/frontend/render.ts b/src/frontend/render.ts index 3a8303d3..bd83600b 100644 --- a/src/frontend/render.ts +++ b/src/frontend/render.ts @@ -87,13 +87,20 @@ export function renderProjectDataOverview( } export function renderChanges(projectId: string): void { - const changesP = fetch(`/rebenchdb/dash/${projectId}/changes`); + const changesP = getChangesDetails(projectId); changesP.then( async (changesDetailsResponse) => await renderChangeDetails(changesDetailsResponse, projectId) ); } +async function getChangesDetails(projectId: string) { + const changesDetailsResponse = await fetch( + `/rebenchdb/dash/${projectId}/changes` + ); + return changesDetailsResponse; +} + async function renderChangeDetails(changesDetailsResponse, projectId: string) { const details = await changesDetailsResponse.json(); @@ -124,6 +131,146 @@ async function renderChangeDetails(changesDetailsResponse, projectId: string) { $(`#p${projectId}-change a`).on('click', (event) => setHref(event, projectId, false) ); + + renderFilterMenu(details, projectId); +} + +function renderFilterMenu(details, projectId) { + // details.branchortag : string + // details.commitid : string + // details.commitmessage : string + // details.experimenttime : string + // details.projectid : number + // details.repourl : string + + $('.branch-filter-sidebar').each(function (index) { + const $card = $(this); + const branchortag: string[] = details.changes.map( + (change) => change.branchortag + ); + const uniqueBranchOrTag = [...new Set(branchortag)]; + + function renderList(filter = '') { + $card.find('.branch-cards-container').remove(); + + const container = $('
'); + + uniqueBranchOrTag + .filter((bot) => bot.toLowerCase().includes(filter.toLowerCase())) + .forEach((bot) => { + const $link = $(` + ${bot} + + `); + + $link.on('click', function (event) { + event.preventDefault(); + + $card.find('.list-group-item').removeClass('active'); + $(this).toggleClass('active'); + + if (index === 0) { + updateChangesList(bot, projectId, true); + } else { + updateChangesList(bot, projectId, false); + } + }); + + container.append($link); + }); + + $card.append(container); + } + + renderList(); + + $card.find('.filter-header input').on('input', (event) => { + const filter = $(event.target).val(); + renderList(filter); + }); + + $card.find('.filter-options .filter-option').on('click', function (event) { + event.preventDefault(); + + $card.find('.filter-option').removeClass('selected-text'); + + $(this).toggleClass('selected-text'); + + const filter = $(this).text(); + switch (filter) { + case 'Most Used': + uniqueBranchOrTag.sort((a, b) => { + return ( + branchortag.filter((x) => x === b).length - + branchortag.filter((x) => x === a).length + ); + }); + break; + case 'Alphabetical': + uniqueBranchOrTag.sort((a, b) => a.localeCompare(b)); + break; + case 'Most Recent': + uniqueBranchOrTag.sort((a, b) => { + const dateA = details.changes.find( + (x) => x.branchortag === a + ).experimenttime; + const dateB = details.changes.find( + (x) => x.branchortag === b + ).experimenttime; + return new Date(dateB).getTime() - new Date(dateA).getTime(); + }); + break; + default: + break; + } + + renderList(); + }); + }); + + $('.left .filter-options .filter-option:contains("Most Used")').trigger( + 'click' + ); + $('.right .filter-options .filter-option:contains("Most Recent")').trigger( + 'click' + ); +} + +function updateChangesList(branchOrTag, projectId, isBaseline) { + const selector = isBaseline + ? `#p${projectId}-baseline` + : `#p${projectId}-change`; + const target = $(selector); + target.empty(); + + const changesP = getChangesDetails(projectId); + changesP.then(async (changesDetailsResponse) => { + const details = await changesDetailsResponse.json(); + + for (const change of details.changes) { + if (change.branchortag === branchOrTag) { + const msg = filterCommitMessage(change.commitmessage); + const date = formatDateWithTime(change.experimenttime); + + const option = ` +
${date}
+ ${change.commitid.substring(0, 6)} ${change.branchortag}
+
${msg}
+
`; + + target.append(option); + } + } + + // set a event for each list group item which calls setHref + target + .find('a') + .on('click', (event) => setHref(event, projectId, isBaseline)); + }); } function setHref(event, projectId, isBaseline) { From 36e77bf2769dd6fcc1010b8d815e2247ca0996d8 Mon Sep 17 00:00:00 2001 From: Stefan Marr Date: Fri, 26 Jul 2024 00:19:51 +0100 Subject: [PATCH 2/3] Rework code - adapt naming to match the implementation - sort vs filter, cards-container vs list - added added type to get better tool feedback - refactor code to: - only request data once from the server - process branch details only once - eliminate redundant code to add changes to the changes lists (with and without filtering) - break things up into more functions - use .data() function to access the triggered element and avoid having to create new closures for each element Signed-off-by: Stefan Marr --- resources/style.css | 22 +-- src/backend/main/main.ts | 3 +- src/backend/project/project.html | 26 ++- src/frontend/render.ts | 323 +++++++++++++++---------------- src/shared/view-types.ts | 14 ++ 5 files changed, 197 insertions(+), 191 deletions(-) diff --git a/resources/style.css b/resources/style.css index 51e9056e..7c17ac6a 100644 --- a/resources/style.css +++ b/resources/style.css @@ -44,7 +44,7 @@ body.compare, body.timeline { } @media (max-width: 800px) { - .branch-filter-sidebar { + .branch-sidebar { /* hidden */ display: none; } @@ -72,11 +72,11 @@ body.compare, body.timeline { outline: none; } -.branch-filter-sidebar.left { +.branch-sidebar.left { text-align: right; } -.branch-filter-sidebar { +.branch-sidebar { border: 1px solid #ddd; border-radius: 8px; background-color: #fff; @@ -92,7 +92,7 @@ body.compare, body.timeline { padding: 0.5em; } -.filter-options { +.sort-options { background-color: #f9f9f9; border-radius: 4px; margin-bottom: 1em; @@ -101,7 +101,7 @@ body.compare, body.timeline { padding: 0.5em; } -.filter-options a { +.sort-options a { display: block; color: #007bff; text-decoration: none; @@ -110,30 +110,30 @@ body.compare, body.timeline { transition: background-color 0.3s, color 0.3s; } -.filter-options a:hover, -.filter-options a.selected-text { +.sort-options a:hover, +.sort-options a.selected-text { background-color: #007bff; color: white; } -.branch-cards-container { +.branch-list { max-height: 800px; overflow-y: auto; overflow-x: hidden; } -.branch-cards-container .list-group-item { +.branch-list .list-group-item { cursor: pointer; padding: 2px 7px; border: none; color: #444444; } -.branch-cards-container .list-group-item:hover { +.branch-list .list-group-item:hover { background-color: #f1f1f1; } -.branch-cards-container .list-group-item.active { +.branch-list .list-group-item.active { background-color: #007bff; color: white; } diff --git a/src/backend/main/main.ts b/src/backend/main/main.ts index 95317faa..0ecfaba9 100644 --- a/src/backend/main/main.ts +++ b/src/backend/main/main.ts @@ -13,6 +13,7 @@ import { startRequest } from '../perf-tracker.js'; import type { AllResults } from '../../shared/api.js'; +import type { ChangesResponse } from '../../shared/view-types.js'; import { Database } from '../db/db.js'; import { TimedCacheValidity } from '../db/timed-cache-validity.js'; import { getNumberOrError } from '../request-check.js'; @@ -187,7 +188,7 @@ export async function getChangesAsJson( export async function getChanges( projectId: number, db: Database -): Promise<{ changes: any[] }> { +): Promise { const result = await db.query({ name: 'fetchAllChangesByProjectId', text: ` SELECT commitId, branchOrTag, projectId, repoURL, commitMessage, diff --git a/src/backend/project/project.html b/src/backend/project/project.html index 53bb2b01..56edca46 100644 --- a/src/backend/project/project.html +++ b/src/backend/project/project.html @@ -18,17 +18,16 @@

{%= it.description%}

-
+ -
+ diff --git a/src/frontend/render.ts b/src/frontend/render.ts index bd83600b..5a94e010 100644 --- a/src/frontend/render.ts +++ b/src/frontend/render.ts @@ -1,4 +1,5 @@ import type { AllResults } from '../shared/api.js'; +import type { ChangesResponse, ChangesRow } from '../shared/view-types.js'; import { renderResultsPlots } from './plots.js'; export function filterCommitMessage(msg: string): string { @@ -87,27 +88,20 @@ export function renderProjectDataOverview( } export function renderChanges(projectId: string): void { - const changesP = getChangesDetails(projectId); + const changesP = fetch(`/rebenchdb/dash/${projectId}/changes`); changesP.then( - async (changesDetailsResponse) => + async (changesDetailsResponse: Response) => await renderChangeDetails(changesDetailsResponse, projectId) ); } -async function getChangesDetails(projectId: string) { - const changesDetailsResponse = await fetch( - `/rebenchdb/dash/${projectId}/changes` - ); - return changesDetailsResponse; -} - -async function renderChangeDetails(changesDetailsResponse, projectId: string) { - const details = await changesDetailsResponse.json(); - - const p1baseline = $(`#p${projectId}-baseline`); - const p1change = $(`#p${projectId}-change`); - - for (const change of details.changes) { +function addChangesToList( + $list: JQuery, + changes: ChangesRow[], + projectId: string, + isBaseline: boolean +) { + for (const change of changes) { // strip out some metadata to be nicer to view. const msg = filterCommitMessage(change.commitmessage); const date = formatDateWithTime(change.experimenttime); @@ -120,160 +114,14 @@ async function renderChangeDetails(changesDetailsResponse, projectId: string) {
${msg}
`; - p1baseline.append(option); - p1change.append(option); + $list.append(option); } // set a event for each list group item which calls setHref - $(`#p${projectId}-baseline a`).on('click', (event) => - setHref(event, projectId, true) - ); - $(`#p${projectId}-change a`).on('click', (event) => - setHref(event, projectId, false) - ); - - renderFilterMenu(details, projectId); -} - -function renderFilterMenu(details, projectId) { - // details.branchortag : string - // details.commitid : string - // details.commitmessage : string - // details.experimenttime : string - // details.projectid : number - // details.repourl : string - - $('.branch-filter-sidebar').each(function (index) { - const $card = $(this); - const branchortag: string[] = details.changes.map( - (change) => change.branchortag - ); - const uniqueBranchOrTag = [...new Set(branchortag)]; - - function renderList(filter = '') { - $card.find('.branch-cards-container').remove(); - - const container = $('
'); - - uniqueBranchOrTag - .filter((bot) => bot.toLowerCase().includes(filter.toLowerCase())) - .forEach((bot) => { - const $link = $(` - ${bot} - - `); - - $link.on('click', function (event) { - event.preventDefault(); - - $card.find('.list-group-item').removeClass('active'); - $(this).toggleClass('active'); - - if (index === 0) { - updateChangesList(bot, projectId, true); - } else { - updateChangesList(bot, projectId, false); - } - }); - - container.append($link); - }); - - $card.append(container); - } - - renderList(); - - $card.find('.filter-header input').on('input', (event) => { - const filter = $(event.target).val(); - renderList(filter); - }); - - $card.find('.filter-options .filter-option').on('click', function (event) { - event.preventDefault(); - - $card.find('.filter-option').removeClass('selected-text'); - - $(this).toggleClass('selected-text'); - - const filter = $(this).text(); - switch (filter) { - case 'Most Used': - uniqueBranchOrTag.sort((a, b) => { - return ( - branchortag.filter((x) => x === b).length - - branchortag.filter((x) => x === a).length - ); - }); - break; - case 'Alphabetical': - uniqueBranchOrTag.sort((a, b) => a.localeCompare(b)); - break; - case 'Most Recent': - uniqueBranchOrTag.sort((a, b) => { - const dateA = details.changes.find( - (x) => x.branchortag === a - ).experimenttime; - const dateB = details.changes.find( - (x) => x.branchortag === b - ).experimenttime; - return new Date(dateB).getTime() - new Date(dateA).getTime(); - }); - break; - default: - break; - } - - renderList(); - }); - }); - - $('.left .filter-options .filter-option:contains("Most Used")').trigger( - 'click' - ); - $('.right .filter-options .filter-option:contains("Most Recent")').trigger( - 'click' - ); + $list.find('a').on('click', (event) => setHref(event, projectId, isBaseline)); } -function updateChangesList(branchOrTag, projectId, isBaseline) { - const selector = isBaseline - ? `#p${projectId}-baseline` - : `#p${projectId}-change`; - const target = $(selector); - target.empty(); - - const changesP = getChangesDetails(projectId); - changesP.then(async (changesDetailsResponse) => { - const details = await changesDetailsResponse.json(); - - for (const change of details.changes) { - if (change.branchortag === branchOrTag) { - const msg = filterCommitMessage(change.commitmessage); - const date = formatDateWithTime(change.experimenttime); - - const option = ` -
${date}
- ${change.commitid.substring(0, 6)} ${change.branchortag}
-
${msg}
-
`; - - target.append(option); - } - } - - // set a event for each list group item which calls setHref - target - .find('a') - .on('click', (event) => setHref(event, projectId, isBaseline)); - }); -} - -function setHref(event, projectId, isBaseline) { +function setHref(event, projectId: string, isBaseline: boolean) { // every time a commit is clicked, check to see if both left and right // commit are defined. set link if that is true const baseJQ = $(`#p${projectId}-baseline`); @@ -301,6 +149,151 @@ function setHref(event, projectId, isBaseline) { ); } +async function renderChangeDetails( + changesDetailsResponse: Response, + projId: string +) { + const details: ChangesResponse = await changesDetailsResponse.json(); + const changes = details.changes; + + const p1baseline = $(`#p${projId}-baseline`); + const p1change = $(`#p${projId}-change`); + + addChangesToList(p1baseline, changes, projId, true); + addChangesToList(p1change, changes, projId, false); + + const branches = getBranchDetails(changes); + + renderSidebar($('.branch-sidebar.left'), true, branches, changes, projId); + renderSidebar($('.branch-sidebar.right'), false, branches, changes, projId); + + // trigger initial rendering and sorting + $('.left .sort-option:contains("Most Used")').trigger('click'); + $('.right .sort-option:contains("Most Recent")').trigger('click'); +} + +interface BranchDetails { + count: number; + mostRecent: Date; +} + +function getBranchDetails(changes: ChangesRow[]) { + const branches = new Map(); + + for (const change of changes) { + const branch = change.branchortag; + const date = new Date(change.experimenttime); + + const branchDetails = branches.get(branch); + if (branchDetails !== undefined) { + branchDetails.count += 1; + if (date.getTime() > branchDetails.mostRecent.getTime()) { + branchDetails.mostRecent = date; + } + } else { + branches.set(branch, { count: 1, mostRecent: date }); + } + } + return branches; +} + +function renderSidebar( + $container: JQuery, + isBaseline: boolean, + branchDetails: Map, + allChanges: ChangesRow[], + projectId: string +) { + const branches = Array.from(branchDetails.keys()); + + $container.find('.filter-header input').on('input', () => { + renderBranchList($container, branches, allChanges, projectId, isBaseline); + }); + + $container.find('.sort-option').on('click', function (event) { + event.preventDefault(); + + $container.find('.sort-option').removeClass('selected-text'); + $(this).toggleClass('selected-text'); + + const selectedOrder = $(this).text(); + switch (selectedOrder) { + case 'Most Used': { + branches.sort( + (a, b) => branchDetails.get(a)!.count - branchDetails.get(b)!.count + ); + break; + } + case 'Alphabetical': { + branches.sort((a, b) => a.localeCompare(b)); + break; + } + case 'Most Recent': { + branches.sort( + (a, b) => + branchDetails.get(b)!.mostRecent.getTime() - + branchDetails.get(a)!.mostRecent.getTime() + ); + break; + } + } + + renderBranchList($container, branches, allChanges, projectId, isBaseline); + }); +} + +function renderBranchList( + $container: JQuery, + allBranches: string[], + allChanges: ChangesRow[], + projectId: string, + isBaseline: boolean +) { + let branches: string[]; + + const filter = $container.find('.filter-header input').val(); + if (filter) { + branches = allBranches.filter((b) => + b.toLowerCase().includes(filter.toLowerCase()) + ); + } else { + branches = allBranches; + } + + const $branchList = $container.find('.branch-list'); + $branchList.empty(); + + for (const b of branches) { + const $link = $(`${b}`); + $branchList.append($link); + } + + $branchList.find('a').on('click', function (event) { + event.preventDefault(); + + $container.find('.list-group-item').removeClass('active'); + $(this).toggleClass('active'); + + const branch = $(this).data('branch'); + updateChangesList(branch, allChanges, projectId, isBaseline); + }); +} + +function updateChangesList( + branch: string, + allChanges: ChangesRow[], + projId: string, + isBaseline: boolean +) { + const selectedChanges = allChanges.filter((c) => c.branchortag === branch); + const $list = $(isBaseline ? `#p${projId}-baseline` : `#p${projId}-change`); + $list.empty(); + + addChangesToList($list, selectedChanges, projId, isBaseline); +} + export function renderAllResults(projectId: string): void { const resultsP = fetch(`/rebenchdb/dash/${projectId}/results`); resultsP.then(async (resultsResponse) => { diff --git a/src/shared/view-types.ts b/src/shared/view-types.ts index 2f35729b..f009134d 100644 --- a/src/shared/view-types.ts +++ b/src/shared/view-types.ts @@ -277,3 +277,17 @@ export interface ProfileRow { warmup: number; profile: string | ProfileElement[]; } + +/** Row returned by the /rebenchdb/dash/:projectId/changes end point. */ +export interface ChangesRow { + commitid: string; + branchortag: string; + projectid: number; + repourl: string; + commitmessage: string; + experimenttime: string; +} + +export interface ChangesResponse { + changes: ChangesRow[]; +} From 9e299f71e790f3372d5ecc0640127477340f1de1 Mon Sep 17 00:00:00 2001 From: Stefan Marr Date: Fri, 26 Jul 2024 21:50:27 +0100 Subject: [PATCH 3/3] Turn id into class to avoid multiple identical ids Signed-off-by: Stefan Marr --- resources/style.css | 4 ++-- src/backend/project/project.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/style.css b/resources/style.css index 7c17ac6a..8c807acf 100644 --- a/resources/style.css +++ b/resources/style.css @@ -57,7 +57,7 @@ body.compare, body.timeline { } -#branch-search { +.branch-search { width: 100%; max-width: 100%; padding: 8px; @@ -67,7 +67,7 @@ body.compare, body.timeline { transition: border-color 0.3s ease; } -#branch-search:focus { +.branch-search:focus { border-color: #007bff; outline: none; } diff --git a/src/backend/project/project.html b/src/backend/project/project.html index 56edca46..9014f5ef 100644 --- a/src/backend/project/project.html +++ b/src/backend/project/project.html @@ -20,7 +20,7 @@

{%= it.description%}

- +
Most Used @@ -56,7 +56,7 @@
Changes
- +