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..a40dddc1 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, ChangesRow } 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[]; +}