Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic build measurements UI #2501

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/Http/Controllers/BuildController.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ public function tests(int $build_id): View
->with('filters', $filters);
}

public function measurements(int $build_id): View
{
$this->setBuildById($build_id);

$filters = json_decode(request()->get('filters')) ?? ['all' => []];

return $this->view('build.measurements', 'Build Measurements')
->with('filters', $filters);
}

protected function renderBuildPage(int $build_id, string $page_name, string $page_title = '')
{
$this->setBuildById($build_id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('buildmeasurements', function (Blueprint $table) {
$table->unique(['buildid', 'source', 'type', 'name']);
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('buildmeasurements', function (Blueprint $table) {
$table->dropUnique(['buildid', 'source', 'type', 'name']);
});
}
};
2 changes: 2 additions & 0 deletions resources/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import ViewDynamicAnalysis from './components/ViewDynamicAnalysis.vue';
import AllProjects from './components/AllProjects.vue';
import SubProjectDependencies from './components/SubProjectDependencies.vue';
import BuildTestsPage from './components/BuildTestsPage.vue';
import BuildMeasurementsPage from './components/BuildMeasurementsPage.vue';

import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import * as FA from '@fortawesome/fontawesome-svg-core';
Expand Down Expand Up @@ -60,6 +61,7 @@ const cdash_components = {
AllProjects,
SubProjectDependencies,
BuildTestsPage,
BuildMeasurementsPage,
};

/**
Expand Down
152 changes: 152 additions & 0 deletions resources/js/components/BuildMeasurementsPage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<template>
<div class="tw-flex tw-flex-col tw-w-full tw-gap-4">
<filter-builder
filter-type="BuildMeasurementsFiltersMultiFilterInput"
primary-record-name="build measurements"
:initial-filters="initialFilters"
:execute-query-link="executeQueryLink"
@changeFilters="filters => changedFilters = filters"
/>
<loading-indicator :is-loading="!build">
<data-table
:columns="columns"
:rows="formattedMeasurementRows"
:full-width="true"
initial-sort-column="source"
/>
</loading-indicator>
</div>
</template>

<script>

import DataTable from './shared/DataTable.vue';
import gql from 'graphql-tag';
import FilterBuilder from './shared/FilterBuilder.vue';
import LoadingIndicator from './shared/LoadingIndicator.vue';

export default {
components: {
LoadingIndicator,
FilterBuilder,
DataTable,
},

props: {
buildId: {
type: Number,
required: true,
},

initialFilters: {
type: Object,
required: true,
},
},

apollo: {
build: {
query: gql`
query($buildid: ID, $filters: BuildMeasurementsFiltersMultiFilterInput, $after: String) {
build(id: $buildid) {
measurements(filters: $filters, after: $after, first: 100) {
edges {
node {
id
name
source
type
value
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
`,
variables() {
return {
buildid: this.buildId,
filters: this.initialFilters,
after: '',
};
},
result({data}) {
if (data && data.build.measurements.pageInfo.hasNextPage) {
this.$apollo.queries.build.fetchMore({
variables: {
after: data.build.measurements.pageInfo.endCursor,
},
});
}
},
},
},

data() {
return {
changedFilters: JSON.parse(JSON.stringify(this.initialFilters)),
};
},

computed: {
executeQueryLink() {
return `${window.location.origin}${window.location.pathname}?filters=${encodeURIComponent(JSON.stringify(this.changedFilters))}`;
},

columns() {
const uniqueMeasurements = [...new Set(this.build.measurements.edges.map(edge => {
return edge.node.name;
}))];

const columns = [
{
name: 'source',
displayName: 'Source',
expand: true,
},
{
name: 'type',
displayName: 'Type',
},
];

uniqueMeasurements.forEach(element => {
columns.push({
name: `measurement_${element}`,
displayName: element,
});
});

return columns;
},

formattedMeasurementRows() {
// A mapping of the form: source_type => {row object}
const source_type_pairs = {};

this.build.measurements.edges.forEach(edge => {
const key = `${edge.node.source}_${edge.node.type}`;
if (!source_type_pairs.hasOwnProperty(key)) {
source_type_pairs[key] = {
source: edge.node.source,
type: edge.node.type,
};
}

source_type_pairs[key][`measurement_${edge.node.name}`] = {
value: isNaN(edge.node.value) ? edge.node.value : Number(edge.node.value),
text: edge.node.value,
};
});

return Object.values(source_type_pairs);
},
},
};
</script>
8 changes: 8 additions & 0 deletions resources/views/build/measurements.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@extends('cdash', [
'vue' => true,
'daisyui' => true,
])

@section('main_content')
<build-measurements-page :build-id="{{ $build->Id }}" :initial-filters="@js($filters)"></build-measurements-page>
@endsection
2 changes: 2 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@

Route::get('/builds/{build_id}/tests', 'BuildController@tests');

Route::get('/builds/{build_id}/measurements', 'BuildController@measurements');

Route::get('/builds/{id}/update', 'BuildController@update');
Route::permanentRedirect('/build/{id}/update', url('/builds/{id}/update'));
Route::get('/viewUpdate.php', function (Request $request) {
Expand Down
3 changes: 3 additions & 0 deletions tests/cypress/e2e/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,6 @@ set_tests_properties(cypress/e2e/build-configure PROPERTIES DEPENDS cypress/e2e/

add_cypress_e2e_test(all-projects)
set_tests_properties(cypress/e2e/all-projects PROPERTIES DEPENDS cypress/e2e/build-configure)

add_cypress_e2e_test(build-measurements)
set_tests_properties(cypress/e2e/build-measurements PROPERTIES DEPENDS cypress/e2e/all-projects)
16 changes: 16 additions & 0 deletions tests/cypress/e2e/build-measurements.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* TODO: Fill out this test once seed data for the build measurements functionality becomes available.
* For now, we just verify that the page loads with no errors.
*/
describe('Build measurements page', () => {
it('Loads page successfully', () => {
cy.visit('/builds/372/measurements');
cy.get('#headername2').should('not.contain', '404 Not Found');
});

it('Shows 404 if build does not exist', () => {
cy.request({url: '/builds/123456789/measurements', failOnStatusCode: false}).should('have.property', 'status', 404);
cy.visit({url: '/builds/123456789/measurements', failOnStatusCode: false});
cy.get('#headername2').should('contain', '404 Not Found');
});
});