From 95bdb34e7158e1842eefe8e042af37f20f3cd7aa Mon Sep 17 00:00:00 2001 From: William Allen <16820599+williamjallen@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:15:03 -0400 Subject: [PATCH] Add client-side GraphQL handling (#2219) Apollo is currently the most popular GraphQL client and caching library. This PR eliminates my prior attempt at creating a custom GraphQL client, and sets up Apollo for future use. As a proof-of-concept, I switched `/projects` to a GraphQL backend to demonstrate Apollo's pagination features. While doing so, I added Luxon for easy client-side date handling. Since the API endpoint for `/projects` is no longer used, I have marked it as deprecated, with final removal scheduled for CDash 4.0. Users of this endpoint are advised to use the GraphQL API instead. Reviewers can see the pagination functionality in action by changing the `default_count` value in `/config/lighthouse.php` to a small value. --------- Co-authored-by: Shelly Belsky <71195502+sbelsk@users.noreply.github.com> Co-authored-by: Zack Galbreath --- .../Controllers/ViewProjectsController.php | 13 +- app/Models/Project.php | 16 + app/cdash/app/Controller/Api/ViewProjects.php | 5 +- graphql/schema.graphql | 6 + package-lock.json | 339 ++++++++++++++++++ package.json | 7 + phpstan-baseline.neon | 8 + resources/js/app.js | 28 ++ resources/js/components/AllProjects.vue | 275 ++++++++------ resources/js/components/shared/DataTable.vue | 5 +- .../js/components/shared/GraphqlLoader.vue | 75 ---- .../views/project/view-all-projects.blade.php | 2 +- routes/api.php | 1 + routes/web.php | 3 +- tests/Feature/GraphQL/ProjectTypeTest.php | 56 +++ tests/cypress/component/graphql-loader.cy.js | 125 ------- tests/cypress/e2e/CMakeLists.txt | 3 + tests/cypress/e2e/all-projects.cy.js | 22 ++ 18 files changed, 668 insertions(+), 321 deletions(-) delete mode 100644 resources/js/components/shared/GraphqlLoader.vue delete mode 100644 tests/cypress/component/graphql-loader.cy.js create mode 100644 tests/cypress/e2e/all-projects.cy.js diff --git a/app/Http/Controllers/ViewProjectsController.php b/app/Http/Controllers/ViewProjectsController.php index 63621bfa77..b9d8541368 100644 --- a/app/Http/Controllers/ViewProjectsController.php +++ b/app/Http/Controllers/ViewProjectsController.php @@ -12,6 +12,16 @@ final class ViewProjectsController extends AbstractController { public function viewAllProjects(): View|RedirectResponse + { + return $this->viewProjects(true); + } + + public function viewActiveProjects(): View|RedirectResponse + { + return $this->viewProjects(); + } + + private function viewProjects(bool $all = false): View|RedirectResponse { $num_public_projects = (int) DB::select(' SELECT COUNT(*) AS c FROM project WHERE public=? @@ -22,7 +32,8 @@ public function viewAllProjects(): View|RedirectResponse return $this->redirectToLogin(); } - return $this->view("project.view-all-projects"); + return $this->view("project.view-all-projects") + ->with('show_all', $all); } public function fetchPageContent(): JsonResponse diff --git a/app/Models/Project.php b/app/Models/Project.php index b38f0e7d6f..6e9b9e155d 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Support\Carbon; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Support\Facades\Auth; @@ -216,6 +217,21 @@ public function builds(): HasMany }); } + /** + * TODO: Share code with builds(). As of Laravel 10, aggregates added to hasMany relations + * with conditional clauses are unsupported/broken. A reusable scope may be a better approach. + * + * @return HasOne + */ + public function mostRecentBuild(): HasOne + { + return $this->hasOne(Build::class, 'projectid', 'id') + ->ofMany(['submittime' => 'max'], function (Builder $query) { + $query->where('parentid', 0) + ->orWhere('parentid', -1); + }); + } + /** * @return HasMany */ diff --git a/app/cdash/app/Controller/Api/ViewProjects.php b/app/cdash/app/Controller/Api/ViewProjects.php index bf564d7243..388cbcaabc 100644 --- a/app/cdash/app/Controller/Api/ViewProjects.php +++ b/app/cdash/app/Controller/Api/ViewProjects.php @@ -23,9 +23,9 @@ use Illuminate\Support\Facades\DB; /** - * TODO: (williamjallen) move all of this logic over to app/Http/Controllers/ViewProjectsController.php - * * API controller for viewProjects.php. + * + * @deprecated 05/23/2024 This endpoint is deprecated in favor of the GraphQL API and will eventually be removed. **/ class ViewProjects extends \CDash\Controller\Api { @@ -45,6 +45,7 @@ public function __construct(Database $db) public function getResponse() { $response = begin_JSON_response(); + $response['deprecated'] = 'This endpoint is deprecated and will eventually be removed.'; $global_banner = Banner::find(0); if ($global_banner !== null && strlen($global_banner->text) > 0) { diff --git a/graphql/schema.graphql b/graphql/schema.graphql index b879610f4f..2d2459c829 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -72,6 +72,12 @@ type Project { filters: _ @filter(inputType: "BuildFilterInput") ): [Build!]! @hasMany(type: CONNECTION) @orderBy(column: "id") + """ + The most recent build submitted to this server. Note: this is determined by + submission time, not build start time. + """ + mostRecentBuild: Build @hasOne + "The sites which have submitted a build to this project." sites( filters: _ @filter(inputType: "SiteFilterInput") diff --git a/package-lock.json b/package-lock.json index 55f81d84d9..9b730bc7b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,15 @@ "version": "3.5.0", "license": "BSD-3-Clause", "dependencies": { + "@apollo/client": "^3.10.3", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", + "@vue/apollo-components": "^4.0.0", + "@vue/apollo-composable": "^4.0.2", + "@vue/apollo-option": "^4.0.0", "angular": "^1.8.0", "angular-animate": "^1.4.7", "angular-ui-bootstrap": "^2.0.0", @@ -30,10 +34,13 @@ "eslint-plugin-vue": "^9.24.0", "flot": "^4.2.6", "git-revision-webpack-plugin": "^5.0.0", + "graphql": "^16.8.1", + "graphql-tag": "^2.12.6", "jquery": "^3.7", "jquery-ui-dist": "^1.13.1", "jquery.cookie": "^1.4.1", "laravel-mix": "^6.0.49", + "luxon": "^3.4.4", "ng-file-upload": "^12.0.4", "nvd3": "^1.8.5", "replace-in-file-webpack-plugin": "^1.0.6", @@ -90,6 +97,48 @@ "node": ">=6.0.0" } }, + "node_modules/@apollo/client": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.10.3.tgz", + "integrity": "sha512-4EIgZnFmRO1laWv3NCxlVIxcvimG63djuAXvyXhpQH3wkNMv9SykrasKRN08+z+cn/fVisBOLmkSRdyNyP9f4A==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.5.0", + "graphql-tag": "^2.12.6", + "hoist-non-react-statics": "^3.3.2", + "optimism": "^0.18.0", + "prop-types": "^15.7.2", + "rehackt": "^0.1.0", + "response-iterator": "^0.2.6", + "symbol-observable": "^4.0.0", + "ts-invariant": "^0.10.3", + "tslib": "^2.3.0", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "graphql": "^15.0.0 || ^16.0.0", + "graphql-ws": "^5.5.5", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -2033,6 +2082,14 @@ "vue": ">= 3.0.0 < 4" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -3404,6 +3461,75 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@vue/apollo-components": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@vue/apollo-components/-/apollo-components-4.0.0.tgz", + "integrity": "sha512-py5gYgYQWgg56eJj5O1n8YugCYcMcu9lTtMCjwlh+QokYGXpdf5ejgSzY0j4gGcj51MgkucxF3fYQT3Xaffaqw==", + "dependencies": { + "@vue/apollo-option": "^4.0.0" + }, + "peerDependencies": { + "vue": "^3.1.0" + } + }, + "node_modules/@vue/apollo-composable": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@vue/apollo-composable/-/apollo-composable-4.0.2.tgz", + "integrity": "sha512-/yfEktliXw+oMy/6n9C0J05woYoKlM+kcffVXMh0qljbXdrioyj0WkR/LgPdpnRf6TglFnjIgCKKicd5Gyzpnw==", + "dependencies": { + "throttle-debounce": "^5.0.0", + "ts-essentials": "^9.4.0", + "vue-demi": "^0.14.6" + }, + "peerDependencies": { + "@apollo/client": "^3.4.13", + "@vue/composition-api": "^1.0.0", + "graphql": ">=15", + "vue": "^2.6.0 || ^3.1.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vue/apollo-composable/node_modules/vue-demi": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", + "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vue/apollo-option": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@vue/apollo-option/-/apollo-option-4.0.0.tgz", + "integrity": "sha512-rZf4bLNldV9egADp3UhC2O4qmmn6L3vF/RwRJse8lgtpGFrov9qtZxl/rRg0+9UtZeOTFwTovnQo7J7C7FP3Ow==", + "dependencies": { + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "@apollo/client": "^3.2.1", + "vue": "^3.1.0" + } + }, "node_modules/@vue/compiler-core": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz", @@ -3715,6 +3841,50 @@ } } }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", + "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -8019,6 +8189,28 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -8139,6 +8331,19 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -11414,6 +11619,17 @@ "node": ">=8" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -11430,6 +11646,14 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -12080,6 +12304,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optimism": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.0.tgz", + "integrity": "sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==", + "dependencies": { + "@wry/caches": "^1.0.0", + "@wry/context": "^0.7.0", + "@wry/trie": "^0.4.3", + "tslib": "^2.3.0" + } + }, + "node_modules/optimism/node_modules/@wry/trie": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.4.3.tgz", + "integrity": "sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -13125,6 +13371,21 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -13437,6 +13698,23 @@ "jsesc": "bin/jsesc" } }, + "node_modules/rehackt": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", + "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", + "peerDependencies": { + "@types/react": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -13560,6 +13838,14 @@ "node": ">=10" } }, + "node_modules/response-iterator": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", + "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -14598,6 +14884,14 @@ "node": ">=0.10.0" } }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -14866,6 +15160,14 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/throttleit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", @@ -14961,11 +15263,35 @@ "node": ">=6" } }, + "node_modules/ts-essentials": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.2.tgz", + "integrity": "sha512-mB/cDhOvD7pg3YCLk2rOtejHjjdSi9in/IBYE13S+8WA5FBSraYf4V/ws55uvs0IvQ/l0wBOlXy5yBNZ9Bl8ZQ==", + "peerDependencies": { + "typescript": ">=4.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" }, + "node_modules/ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tsconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", @@ -16192,6 +16518,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "node_modules/zen-observable-ts": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "dependencies": { + "zen-observable": "0.8.15" + } } } } diff --git a/package.json b/package.json index 8a75c8d7cf..67815abd39 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,15 @@ "eslint:fix": "eslint --fix \"**/*.js\" \"**/*.vue\"" }, "dependencies": { + "@apollo/client": "^3.10.3", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", + "@vue/apollo-components": "^4.0.0", + "@vue/apollo-composable": "^4.0.2", + "@vue/apollo-option": "^4.0.0", "angular": "^1.8.0", "angular-animate": "^1.4.7", "angular-ui-bootstrap": "^2.0.0", @@ -38,10 +42,13 @@ "eslint-plugin-vue": "^9.24.0", "flot": "^4.2.6", "git-revision-webpack-plugin": "^5.0.0", + "graphql": "^16.8.1", + "graphql-tag": "^2.12.6", "jquery": "^3.7", "jquery-ui-dist": "^1.13.1", "jquery.cookie": "^1.4.1", "laravel-mix": "^6.0.49", + "luxon": "^3.4.4", "ng-file-upload": "^12.0.4", "nvd3": "^1.8.5", "replace-in-file-webpack-plugin": "^1.0.6", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4855d08ee2..7724bc89d8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2492,6 +2492,14 @@ parameters: count: 1 path: app/Http/Controllers/UserStatisticsController.php + - + message: """ + #^Instantiation of deprecated class CDash\\\\Controller\\\\Api\\\\ViewProjects\\: + 05/23/2024 This endpoint is deprecated in favor of the GraphQL API and will eventually be removed\\.$# + """ + count: 1 + path: app/Http/Controllers/ViewProjectsController.php + - message: "#^Parameter \\#2 \\$build of class CDash\\\\Controller\\\\Api\\\\ViewTest constructor expects CDash\\\\Model\\\\Build, CDash\\\\Model\\\\Build\\|null given\\.$#" count: 1 diff --git a/resources/js/app.js b/resources/js/app.js index 3b6aba2fcb..19e1b69e1e 100755 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -5,8 +5,15 @@ */ import * as Vue from 'vue'; + import axios from 'axios'; +import { ApolloClient, InMemoryCache } from '@apollo/client/core'; +import { createApolloProvider } from '@vue/apollo-option'; +import VueApolloComponents from '@vue/apollo-components'; +import { relayStylePagination } from '@apollo/client/utilities'; +import { DefaultApolloClient } from '@vue/apollo-composable'; + import BuildConfigure from './components/BuildConfigure'; import BuildNotes from './components/BuildNotes'; import BuildSummary from './components/BuildSummary'; @@ -77,5 +84,26 @@ else { app.config.globalProperties.$axios = axios; +const apolloClient = new ApolloClient({ + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + projects: relayStylePagination(), + }, + }, + }, + }), + uri: `${app.config.globalProperties.$baseURL}/graphql`, +}); + +const apolloProvider = createApolloProvider({ + defaultClient: apolloClient, +}); + +app.use(apolloProvider); +app.use(VueApolloComponents); +app.provide(DefaultApolloClient, apolloClient); + window.Vue = Vue; app.mount('#app'); diff --git a/resources/js/components/AllProjects.vue b/resources/js/components/AllProjects.vue index e3e5664b0c..97368c03d6 100644 --- a/resources/js/components/AllProjects.vue +++ b/resources/js/components/AllProjects.vue @@ -1,141 +1,188 @@ - - diff --git a/resources/js/components/shared/DataTable.vue b/resources/js/components/shared/DataTable.vue index b9dad53453..2bab3c5607 100644 --- a/resources/js/components/shared/DataTable.vue +++ b/resources/js/components/shared/DataTable.vue @@ -58,7 +58,7 @@ v-if="Object.hasOwn(row[column.name], 'href')" :href="row[column.name].href" > - {{ row[column.name].value }} + {{ row[column.name].text }}