Skip to content

Commit 24f6a30

Browse files
Add GraphQL mutations for test measurement pinning (#3437)
This PR adds mutations for creating, deleting, and updating the order of pinned test measurements. This will eventually replace `/api/v1/manageMeasurements.php`.
1 parent 41c8954 commit 24f6a30

21 files changed

+1030
-34
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\GraphQL\Mutations;
6+
7+
use App\Models\PinnedTestMeasurement;
8+
use App\Models\Project;
9+
use Illuminate\Support\Facades\Gate;
10+
11+
final class CreatePinnedTestMeasurement extends AbstractMutation
12+
{
13+
public ?PinnedTestMeasurement $pinnedTestMeasurement = null;
14+
15+
/**
16+
* @param array{
17+
* projectId: int,
18+
* name: string,
19+
* } $args
20+
*/
21+
protected function mutate(array $args): void
22+
{
23+
$project = Project::find((int) $args['projectId']);
24+
Gate::authorize('createPinnedTestMeasurement', $project);
25+
26+
$nextAvailablePosition = $project?->pinnedTestMeasurements()->max('position');
27+
if ($nextAvailablePosition === null) {
28+
$nextAvailablePosition = 1;
29+
} else {
30+
$nextAvailablePosition++;
31+
}
32+
33+
$this->pinnedTestMeasurement = $project?->pinnedTestMeasurements()->create([
34+
'name' => $args['name'],
35+
'position' => $nextAvailablePosition,
36+
]);
37+
}
38+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\GraphQL\Mutations;
6+
7+
use App\Models\PinnedTestMeasurement;
8+
use Illuminate\Support\Facades\Gate;
9+
10+
final class DeletePinnedTestMeasurement extends AbstractMutation
11+
{
12+
/**
13+
* @param array{
14+
* id: int,
15+
* } $args
16+
*/
17+
protected function mutate(array $args): void
18+
{
19+
$measurement = PinnedTestMeasurement::find((int) $args['id']);
20+
21+
Gate::authorize('deletePinnedTestMeasurement', $measurement?->project);
22+
23+
$measurement?->delete();
24+
}
25+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\GraphQL\Mutations;
6+
7+
use App\Models\PinnedTestMeasurement;
8+
use App\Models\Project;
9+
use Exception;
10+
use Illuminate\Support\Collection;
11+
use Illuminate\Support\Facades\Gate;
12+
13+
final class UpdatePinnedTestMeasurementOrder extends AbstractMutation
14+
{
15+
/** @var ?Collection<PinnedTestMeasurement> */
16+
public ?Collection $pinnedTestMeasurements = null;
17+
18+
/**
19+
* @param array{
20+
* projectId: int,
21+
* pinnedTestMeasurementIds: array<int>,
22+
* } $args
23+
*/
24+
protected function mutate(array $args): void
25+
{
26+
$project = Project::find((int) $args['projectId']);
27+
Gate::authorize('updatePinnedTestMeasurementOrder', $project);
28+
29+
$projectMeasurementIds = $project?->pinnedTestMeasurements()->pluck('id');
30+
$newOrder = collect($args['pinnedTestMeasurementIds']);
31+
32+
if ($projectMeasurementIds->diff($newOrder)->isNotEmpty()) {
33+
throw new Exception('IDs for all PinnedTestMeasurements must be provided.');
34+
}
35+
36+
if ($newOrder->count() !== $projectMeasurementIds->count()) {
37+
throw new Exception('Provided set cannot contain duplicate IDs.');
38+
}
39+
40+
if ($newOrder->isEmpty()) {
41+
throw new Exception("Can't order an empty set.");
42+
}
43+
44+
// We start at the previous maximum ID + 1 to guarantee that there are never any conflicts.
45+
// Only the relative order matters, so we don't care if the minimum position is now 1.
46+
$position = (int) $project?->pinnedTestMeasurements()->max('position') + 1;
47+
foreach ($newOrder as $id) {
48+
/** @var PinnedTestMeasurement $measurement */
49+
$measurement = $project?->pinnedTestMeasurements()->findOrFail((int) $id);
50+
51+
$measurement->position = $position;
52+
$measurement->save();
53+
$position++;
54+
}
55+
56+
$this->pinnedTestMeasurements = $project?->pinnedTestMeasurements()->orderBy('position')->get();
57+
}
58+
}

app/Http/Controllers/BuildController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public function tests(int $build_id): View
107107
'project-name' => $eloquent_project->name,
108108
'build-time' => Carbon::parse($this->build->StartTime)->toIso8601String(),
109109
'initial-filters' => $filters,
110-
'pinned-measurements' => $eloquent_project->measurements()->orderBy('position')->pluck('name')->toArray(),
110+
'pinned-measurements' => $eloquent_project->pinnedTestMeasurements()->orderBy('position')->pluck('name')->toArray(),
111111
]);
112112
}
113113

app/Http/Controllers/ManageMeasurementsController.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace App\Http\Controllers;
44

5-
use App\Models\Measurement;
5+
use App\Models\PinnedTestMeasurement;
66
use App\Models\Project as EloquentProject;
77
use App\Utils\PageTimer;
88
use Illuminate\Http\JsonResponse;
@@ -40,7 +40,7 @@ public function apiGet(): JsonResponse
4040
// Get any measurements associated with this project's tests.
4141
$measurements_response = [];
4242
$measurements = EloquentProject::findOrFail($this->project->Id)
43-
->measurements()
43+
->pinnedTestMeasurements()
4444
->orderBy('position', 'asc')
4545
->get();
4646

@@ -71,9 +71,9 @@ public function apiPost(): JsonResponse
7171
$id = (int) $measurement_data['id'];
7272
if ($id > 0) {
7373
// Update an existing measurement rather than creating a new one.
74-
$measurement = Measurement::find($id);
74+
$measurement = PinnedTestMeasurement::find($id);
7575
} else {
76-
$measurement = new Measurement();
76+
$measurement = new PinnedTestMeasurement();
7777
}
7878
$measurement->projectid = $this->project->Id;
7979
$measurement->name = $measurement_data['name'];
@@ -107,7 +107,7 @@ public function apiDelete(): JsonResponse
107107
}
108108

109109
$deleted = EloquentProject::findOrFail($this->project->Id)
110-
->measurements()
110+
->pinnedTestMeasurements()
111111
->where('id', (int) request()->input('id'))
112112
->delete();
113113

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
* @property string $name
1313
* @property int $position
1414
*
15-
* @mixin Builder<Measurement>
15+
* @mixin Builder<PinnedTestMeasurement>
1616
*/
17-
class Measurement extends Model
17+
class PinnedTestMeasurement extends Model
1818
{
1919
protected $table = 'measurement';
2020

@@ -37,6 +37,6 @@ class Measurement extends Model
3737
*/
3838
public function project(): BelongsTo
3939
{
40-
return $this->belongsTo(Project::class, 'id', 'projectid');
40+
return $this->belongsTo(Project::class, 'projectid');
4141
}
4242
}

app/Models/Project.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,11 @@ public function subprojects(?Carbon $date = null): HasMany
232232
}
233233

234234
/**
235-
* @return HasMany<Measurement, $this>
235+
* @return HasMany<PinnedTestMeasurement, $this>
236236
*/
237-
public function measurements(): HasMany
237+
public function pinnedTestMeasurements(): HasMany
238238
{
239-
return $this->hasMany(Measurement::class, 'projectid', 'id');
239+
return $this->hasMany(PinnedTestMeasurement::class, 'projectid', 'id');
240240
}
241241

242242
/**

app/Policies/ProjectPolicy.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@ public function leave(User $currentUser, Project $project): bool
141141
return !$this->isLdapControlledMembership($project) && $project->users()->where('id', $currentUser->id)->exists();
142142
}
143143

144+
public function createPinnedTestMeasurement(User $currentUser, Project $project): bool
145+
{
146+
return $this->update($currentUser, $project);
147+
}
148+
149+
public function deletePinnedTestMeasurement(User $currentUser, Project $project): bool
150+
{
151+
return $this->update($currentUser, $project);
152+
}
153+
154+
public function updatePinnedTestMeasurementOrder(User $currentUser, Project $project): bool
155+
{
156+
return $this->update($currentUser, $project);
157+
}
158+
144159
private function isLdapControlledMembership(Project $project): bool
145160
{
146161
// If a LDAP filter has been specified and LDAP is enabled, CDash controls the entire members list.

app/cdash/app/Controller/Api/QueryTests.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
namespace CDash\Controller\Api;
1919

20-
use App\Models\Measurement;
20+
use App\Models\PinnedTestMeasurement;
2121
use App\Models\Project as EloquentProject;
2222
use App\Models\TestMeasurement;
2323
use CDash\Database;
@@ -276,10 +276,10 @@ public function getResponse(): array
276276
// Get the list of extra test measurements that should be displayed on this page.
277277
$this->extraMeasurements = [];
278278
$measurements = EloquentProject::findOrFail($this->project->Id)
279-
->measurements()
279+
->pinnedTestMeasurements()
280280
->orderBy('position')
281281
->get();
282-
/** @var Measurement $measurement */
282+
/** @var PinnedTestMeasurement $measurement */
283283
foreach ($measurements as $measurement) {
284284
// If we have the Processors measurement, then we should also
285285
// compute and display 'Proc Time'.

app/cdash/app/Controller/Api/TestDetails.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ function () use ($query): void {
292292

293293
// Get the list of extra test measurements that have been explicitly added to this project.
294294
$extra_measurements = EloquentProject::findOrFail($this->project->Id)
295-
->measurements()
295+
->pinnedTestMeasurements()
296296
->orderBy('position')
297297
->pluck('name')
298298
->toArray();

0 commit comments

Comments
 (0)