From 34b48cda2e0916a0a6c2db012c5fc0f9ba7b91c3 Mon Sep 17 00:00:00 2001 From: William Allen Date: Sun, 22 Mar 2026 16:14:04 -0400 Subject: [PATCH] Revamp project settings page This PR completely replaces the project settings page, fixing numerous bugs, cleaning up the UI, and allowing us to deprecate two legacy API routes. Along the way, I fixed an issue where the fileUploadLimit returned a value in bytes rather than the documented GiB. --- .../Controllers/EditProjectController.php | 26 - app/Http/Controllers/ProjectController.php | 2 + .../Controllers/ProjectSettingsController.php | 24 + app/Models/Project.php | 15 + app/cdash/public/subscribeProject.xsl | 4 +- app/cdash/tests/CMakeLists.txt | 2 + graphql/schema.graphql | 4 +- phpstan-baseline.neon | 38 +- resources/js/vue/app.js | 2 +- .../js/vue/components/CreateProjectPage.vue | 2 +- resources/js/vue/components/EditProject.vue | 2465 ----------------- .../ProjectSettings/CheckboxField.vue | 73 + .../ProjectSettings/FormSection.vue | 21 + .../components/ProjectSettings/GeneralTab.vue | 684 +++++ .../components/ProjectSettings/InputField.vue | 92 + .../ProjectSettings/IntegrationsTab.vue | 290 ++ .../ProjectSettings/SelectField.vue | 83 + .../components/ProjectSettings/TabContent.vue | 22 + .../ProjectSettings/TextAreaField.vue | 64 + .../js/vue/components/ProjectSettingsPage.vue | 65 + resources/js/vue/components/UserHomepage.vue | 2 +- resources/views/components/header.blade.php | 2 +- routes/web.php | 10 +- .../Browser/Pages/ProjectSettingsPageTest.php | 299 ++ .../GraphQL/Mutations/UpdateProjectTest.php | 2 +- tests/Feature/GraphQL/ProjectTypeTest.php | 2 +- tests/Spec/CMakeLists.txt | 1 - tests/Spec/edit-project.spec.js | 111 - 28 files changed, 1789 insertions(+), 2618 deletions(-) delete mode 100644 app/Http/Controllers/EditProjectController.php create mode 100644 app/Http/Controllers/ProjectSettingsController.php delete mode 100644 resources/js/vue/components/EditProject.vue create mode 100644 resources/js/vue/components/ProjectSettings/CheckboxField.vue create mode 100644 resources/js/vue/components/ProjectSettings/FormSection.vue create mode 100644 resources/js/vue/components/ProjectSettings/GeneralTab.vue create mode 100644 resources/js/vue/components/ProjectSettings/InputField.vue create mode 100644 resources/js/vue/components/ProjectSettings/IntegrationsTab.vue create mode 100644 resources/js/vue/components/ProjectSettings/SelectField.vue create mode 100644 resources/js/vue/components/ProjectSettings/TabContent.vue create mode 100644 resources/js/vue/components/ProjectSettings/TextAreaField.vue create mode 100644 resources/js/vue/components/ProjectSettingsPage.vue create mode 100644 tests/Browser/Pages/ProjectSettingsPageTest.php delete mode 100644 tests/Spec/edit-project.spec.js diff --git a/app/Http/Controllers/EditProjectController.php b/app/Http/Controllers/EditProjectController.php deleted file mode 100644 index 5301e00665..0000000000 --- a/app/Http/Controllers/EditProjectController.php +++ /dev/null @@ -1,26 +0,0 @@ -vue('edit-project', 'Create Project', ['projectid' => 0], false); - } - - // Render the edit project form. - public function edit(int $project_id): View - { - $this->setProjectById($project_id); - Gate::authorize('edit-project', $this->project); - - return $this->vue('edit-project', 'Edit Project', ['projectid' => $this->project->Id], false); - } -} diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 2da9f7ed7a..e35865d609 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -159,6 +159,8 @@ public function apiCreateProject(): JsonResponse $response['ldap_enabled'] = config('cdash.ldap_enabled'); + $response['deprecated'] = 'This endpoint will be removed in the next major version of CDash.'; + $pageTimer->end($response); return response()->json(cast_data_for_JSON($response)); } diff --git a/app/Http/Controllers/ProjectSettingsController.php b/app/Http/Controllers/ProjectSettingsController.php new file mode 100644 index 0000000000..92423dd072 --- /dev/null +++ b/app/Http/Controllers/ProjectSettingsController.php @@ -0,0 +1,24 @@ +setProjectById($project_id); + + $project = Project::find($project_id); + Gate::authorize('update', $project); + + return $this->vue('project-settings-page', 'Project Settings', [ + 'project-id' => $project_id, + 'ldap-enabled' => (bool) config('cdash.ldap_enabled'), + ]); + } +} diff --git a/app/Models/Project.php b/app/Models/Project.php index da1caf4172..99345be961 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -41,6 +41,7 @@ * @property int $autoremovetimeframe * @property int $autoremovemaxbuilds * @property int $uploadquota Maximum sum of uploaded file sizes (in bytes) + * @property int $uploadquotagb Maximum sum of uploaded file sizes (in GiB) * @property bool $showcoveragecode * @property bool $sharelabelfilters * @property bool $authenticatesubmissions @@ -89,6 +90,7 @@ class Project extends Model 'autoremovetimeframe', 'autoremovemaxbuilds', 'uploadquota', + 'uploadquotagb', 'showcoveragecode', 'sharelabelfilters', 'authenticatesubmissions', @@ -137,6 +139,19 @@ protected function logoUrl(): Attribute ); } + /** + * @return Attribute + */ + protected function uploadquotagb(): Attribute + { + return Attribute::make( + get: fn (mixed $value, array $attributes): int => (int) ((int) $attributes['uploadquota'] / (2 ** 30)), + set: fn (int $value): array => [ + 'uploadquota' => $value * (2 ** 30), + ] + ); + } + /** * Get the users who have been added to this project. Note that this selects users with all roles. * diff --git a/app/cdash/public/subscribeProject.xsl b/app/cdash/public/subscribeProject.xsl index 811aff65fc..bf5190c33f 100644 --- a/app/cdash/public/subscribeProject.xsl +++ b/app/cdash/public/subscribeProject.xsl @@ -61,7 +61,7 @@ *This project has not been configured to send emails. - project//edit#EmailChange the project settings. + project//settings#EmailChange the project settings. Contact the project administrator. @@ -135,7 +135,7 @@ *This project has not been configured to send emails. - project//edit#EmailChange the project settings. + project//settings#EmailChange the project settings. Contact the project administrator. diff --git a/app/cdash/tests/CMakeLists.txt b/app/cdash/tests/CMakeLists.txt index 66e9f47dd4..0ec80ad976 100644 --- a/app/cdash/tests/CMakeLists.txt +++ b/app/cdash/tests/CMakeLists.txt @@ -417,6 +417,8 @@ add_browser_test(/Browser/Pages/BuildSummaryPageTest) add_browser_test(/Browser/Pages/BuildSidebarComponentTest) +add_browser_test(/Browser/Pages/ProjectSettingsPageTest) + add_php_test(image) set_tests_properties(image PROPERTIES DEPENDS /CDash/XmlHandler/UpdateHandler) diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 3f324421ad..47a5a59a91 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -307,7 +307,7 @@ type Project { """ Uploaded files exceeding this limit (in GB) will be deleted from oldest to newest. """ - fileUploadLimit: Int! @rename(attribute: "uploadquota") + fileUploadLimit: Int! @rename(attribute: "uploadquotagb") """ Enable/Disable the display of source code for the project. Only administrators can see the source @@ -478,7 +478,7 @@ input UpdateProjectInput @validator { autoRemoveMaxBuilds: Int @rename(attribute: "autoremovemaxbuilds") - fileUploadLimit: Int @rename(attribute: "uploadquota") + fileUploadLimit: Int @rename(attribute: "uploadquotagb") showCoverageCode: Boolean @rename(attribute: "showcoveragecode") diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 63cefd0747..72a7f7db0e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -6138,7 +6138,7 @@ parameters: - rawMessage: Cannot cast mixed to int. identifier: cast.int - count: 1 + count: 2 path: app/Models/Project.php - @@ -26478,6 +26478,42 @@ parameters: count: 1 path: tests/Browser/Pages/ProjectMembersPageTest.php + - + rawMessage: 'Only booleans are allowed in a ternary operator condition, mixed given.' + identifier: ternary.condNotBoolean + count: 2 + path: tests/Browser/Pages/ProjectSettingsPageTest.php + + - + rawMessage: 'Parameter #2 $value of method Laravel\Dusk\Browser::assertRadioSelected() expects string, mixed given.' + identifier: argument.type + count: 1 + path: tests/Browser/Pages/ProjectSettingsPageTest.php + + - + rawMessage: 'Parameter #2 $value of method Laravel\Dusk\Browser::assertValue() expects string, mixed given.' + identifier: argument.type + count: 1 + path: tests/Browser/Pages/ProjectSettingsPageTest.php + + - + rawMessage: 'Parameter #2 $value of method Laravel\Dusk\Browser::radio() expects string, mixed given.' + identifier: argument.type + count: 1 + path: tests/Browser/Pages/ProjectSettingsPageTest.php + + - + rawMessage: 'Parameter #2 $value of method Laravel\Dusk\Browser::select() expects array|string|null, mixed given.' + identifier: argument.type + count: 1 + path: tests/Browser/Pages/ProjectSettingsPageTest.php + + - + rawMessage: 'Parameter #2 $value of method Laravel\Dusk\Browser::type() expects string, mixed given.' + identifier: argument.type + count: 1 + path: tests/Browser/Pages/ProjectSettingsPageTest.php + - rawMessage: 'You should use assertSame() instead of assertEquals(), because both values are scalars of the same type' identifier: phpunit.assertEquals diff --git a/resources/js/vue/app.js b/resources/js/vue/app.js index 0fb58ab443..3304ff6ed3 100755 --- a/resources/js/vue/app.js +++ b/resources/js/vue/app.js @@ -13,7 +13,6 @@ const app = Vue.createApp({ BuildNotesPage: Vue.defineAsyncComponent(() => import('./components/BuildNotesPage.vue')), BuildSummary: Vue.defineAsyncComponent(() => import('./components/BuildSummary')), BuildUpdate: Vue.defineAsyncComponent(() => import('./components/BuildUpdate')), - EditProject: Vue.defineAsyncComponent(() => import('./components/EditProject')), UserHomepage: Vue.defineAsyncComponent(() => import('./components/UserHomepage')), ManageAuthTokens: Vue.defineAsyncComponent(() => import('./components/ManageAuthTokens.vue')), ManageMeasurements: Vue.defineAsyncComponent(() => import('./components/ManageMeasurements')), @@ -37,6 +36,7 @@ const app = Vue.createApp({ BuildCoveragePage: Vue.defineAsyncComponent(() => import('./components/BuildCoveragePage.vue')), CreateProjectPage: Vue.defineAsyncComponent(() => import('./components/CreateProjectPage.vue')), AdministrationPage: Vue.defineAsyncComponent(() => import('./components/AdministrationPage.vue')), + ProjectSettingsPage: Vue.defineAsyncComponent(() => import('./components/ProjectSettingsPage.vue')), }, }); diff --git a/resources/js/vue/components/CreateProjectPage.vue b/resources/js/vue/components/CreateProjectPage.vue index d8a9d57184..6da7213d65 100644 --- a/resources/js/vue/components/CreateProjectPage.vue +++ b/resources/js/vue/components/CreateProjectPage.vue @@ -212,7 +212,7 @@ export default { }); if (response.data.createProject) { - window.location.href = `${this.$baseURL}/projects/${response.data.createProject.id}/edit`; + window.location.href = `${this.$baseURL}/projects/${response.data.createProject.id}/settings`; } } catch (error) { diff --git a/resources/js/vue/components/EditProject.vue b/resources/js/vue/components/EditProject.vue deleted file mode 100644 index 99ae8e5bab..0000000000 --- a/resources/js/vue/components/EditProject.vue +++ /dev/null @@ -1,2465 +0,0 @@ -